feat(plugins): implementation of the Navidrome Plugin Development Kit with generated client wrappers and service interfaces - Phase 2 (2)

Signed-off-by: Deluan <deluan@navidrome.org>
This commit is contained in:
Deluan 2025-12-29 23:04:30 -05:00
parent 6fa9ef0dfe
commit 6d3c29912b
141 changed files with 131 additions and 9883 deletions

View File

@ -1,433 +0,0 @@
# hostgen
A code generator for Navidrome's plugin host functions. It reads Go interface definitions with special annotations and generates Extism host function wrappers.
## Usage
```bash
hostgen -input <dir> -output <dir> -package <name> [-v] [-dry-run] [-host-only] [-plugin-only]
```
### Flags
| Flag | Description | Default |
|----------------|----------------------------------------------------------------|----------|
| `-input` | Directory containing Go source files with annotated interfaces | Required |
| `-output` | Directory where generated files will be written | Required |
| `-package` | Package name for generated files | Required |
| `-v` | Verbose output | `false` |
| `-dry-run` | Parse and validate without writing files | `false` |
| `-host-only` | Generate only host-side wrapper code | `false` |
| `-plugin-only` | Generate only plugin/client-side wrapper code | `false` |
| `-go` | Generate Go client wrappers | `true`* |
| `-python` | Generate Python client wrappers | `false` |
| `-rust` | Generate Rust client wrappers | `false` |
\* `-go` is enabled by default when neither `-python` nor `-rust` is specified. Use combinations like `-go -python -rust` to generate multiple languages.
By default, both host and Go plugin code are generated. Use `-host-only` or `-plugin-only` to generate only one type. Use `-python` to generate Python wrappers and `-rust` to generate Rust wrappers.
### Example
```bash
go run ./plugins/cmd/hostgen \
-input ./plugins/host \
-output ./plugins/host \
-package host
```
Or via `go generate` (recommended):
```go
//go:generate go run ../cmd/hostgen -input . -output . -package host
package host
```
## Annotations
### `//nd:hostservice`
Marks an interface as a host service that will have wrappers generated.
```go
//nd:hostservice name=<ServiceName> permission=<permission>
type MyService interface { ... }
```
| Parameter | Description | Required |
|--------------|-----------------------------------------------------------------|----------|
| `name` | Service name used in generated type names and function prefixes | Yes |
| `permission` | Permission required by plugins to use this service | Yes |
### `//nd:hostfunc`
Marks a method within a host service interface for export to plugins.
```go
//nd:hostfunc [name=<export_name>]
MethodName(ctx context.Context, ...) (result Type, err error)
```
| Parameter | Description | Required |
|-----------|-------------------------------------------------------------------------|----------|
| `name` | Custom export name (default: `<servicename>_<methodname>` in lowercase) | No |
## Input Format
Host service interfaces must follow these conventions:
1. **First parameter must be `context.Context`** - Required for all methods
2. **Last return value should be `error`** - For proper error handling
3. **Annotations must be on consecutive lines** - No blank comment lines between doc and annotation
### Example Interface
```go
package host
import "context"
// SubsonicAPIService provides access to Navidrome's Subsonic API.
// This documentation becomes part of the generated code.
//nd:hostservice name=SubsonicAPI permission=subsonicapi
type SubsonicAPIService interface {
// Call executes a Subsonic API request and returns the response.
//nd:hostfunc
Call(ctx context.Context, uri string) (response string, err error)
}
```
## Generated Output
For each annotated interface, hostgen generates:
### Request/Response Types
```go
// SubsonicAPICallRequest is the request type for SubsonicAPI.Call.
type SubsonicAPICallRequest struct {
Uri string `json:"uri"`
}
// SubsonicAPICallResponse is the response type for SubsonicAPI.Call.
type SubsonicAPICallResponse struct {
Response string `json:"response,omitempty"`
Error string `json:"error,omitempty"`
}
```
### Registration Function
```go
// RegisterSubsonicAPIHostFunctions registers SubsonicAPI service host functions.
func RegisterSubsonicAPIHostFunctions(service SubsonicAPIService) []extism.HostFunction {
return []extism.HostFunction{
newSubsonicAPICallHostFunction(service),
}
}
```
### Host Function Wrappers
Each method gets a wrapper that:
1. Reads JSON request from plugin memory
2. Unmarshals to the request type
3. Calls the service method
4. Marshals the response
5. Writes JSON response to plugin memory
## Supported Types
hostgen supports these Go types in method signatures:
| Type | JSON Representation |
|-------------------------------|------------------------------------------|
| `string`, `int`, `bool`, etc. | Native JSON types |
| `[]T` (slices) | JSON arrays |
| `map[K]V` (maps) | JSON objects |
| `*T` (pointers) | Nullable fields |
| `interface{}` / `any` | Converts to `any` |
| Custom structs | JSON objects (must be JSON-serializable) |
### Multiple Return Values
Methods can return multiple values (plus error):
```go
//nd:hostfunc
Search(ctx context.Context, query string) (results []string, total int, hasMore bool, err error)
```
Generates:
```go
type ServiceSearchResponse struct {
Results []string `json:"results,omitempty"`
Total int `json:"total,omitempty"`
HasMore bool `json:"hasMore,omitempty"`
Error string `json:"error,omitempty"`
}
```
## Output Files
### Host Code (Navidrome-side)
Generated files are named `<servicename>_gen.go` (lowercase) and placed in the output directory. Each file includes:
- `// Code generated by hostgen. DO NOT EDIT.` header
- Required imports (`context`, `encoding/json`, `extism`)
- Request/response struct types
- Registration function
- Host function wrappers
- Helper functions (`writeResponse`, `writeErrorResponse`)
### Go Client Library (Go/TinyGo WASM)
Generated files are named `nd_host_<servicename>.go` (lowercase) and placed in the `go/` subdirectory of the output directory. The `go/` directory is a complete Go module (`github.com/navidrome/navidrome/plugins/host/go`) with package name `ndhost`, intended for import by Navidrome plugins built with TinyGo.
The generator also creates:
- `doc.go` - Package documentation listing all available services
- `go.mod` - Go module file with required dependencies
Each service file includes:
- `// Code generated by hostgen. DO NOT EDIT.` header
- Required imports (`encoding/json`, `errors`, `github.com/extism/go-pdk`)
- `//go:wasmimport` declarations for each host function
- Response struct types and any struct definitions from the service
- Wrapper functions that handle memory allocation and JSON parsing
#### Using the Go SDK
Import the SDK in your plugin:
```go
import ndhost "github.com/navidrome/navidrome/plugins/host/go"
// Use host services with the ndhost prefix
result, err := ndhost.CacheGetString("my-key")
scheduleID, err := ndhost.SchedulerScheduleOneTime(60, "payload", "")
```
Add to your `go.mod`:
```
require github.com/navidrome/navidrome/plugins/host/go v0.0.0
replace github.com/navidrome/navidrome/plugins/host/go => ../../host/go
```
See [plugins/host/go/README.md](../../host/go/README.md) for detailed documentation.
### Example Output Structure
```
output/
├── subsonicapi_gen.go # Host-side code (for Navidrome)
├── go/
│ ├── doc.go # Package documentation
│ ├── go.mod # Go module file
│ └── nd_host_subsonicapi.go # Plugin-side code (for TinyGo plugins)
├── python/
│ └── nd_host_subsonicapi.py # Plugin-side code (for Python plugins)
└── rust/
└── nd_host_subsonicapi.rs # Plugin-side code (for Rust plugins)
```
### Python Client Code (extism-py WASM)
Generated files are named `nd_host_<servicename>.py` (lowercase) and placed in the `python/` subdirectory of the output directory. These files are intended for use in Navidrome plugins built with extism-py. Each file includes:
- `# Code generated by hostgen. DO NOT EDIT.` header
- Required imports (`dataclasses`, `typing`, `extism`, `json`)
- `HostFunctionError` exception class for error handling
- `@extism.import_fn` declarations for raw host functions
- `@dataclass` types for methods with multiple return values
- Wrapper functions with type hints, docstrings, and snake_case names
#### Python Type Mapping
| Go Type | Python Type |
|-------------------------|-------------|
| `string` | `str` |
| `int`, `int32`, `int64` | `int` |
| `float32`, `float64` | `float` |
| `bool` | `bool` |
| `[]byte` | `bytes` |
| Unknown | `Any` |
#### Python Function Naming
Functions follow PEP 8 snake_case convention:
| Go Method | Python Function |
|-------------------------------|----------------------------------|
| `SubsonicAPI.Call` | `subsonicapi_call()` |
| `Scheduler.ScheduleRecurring` | `scheduler_schedule_recurring()` |
| `Cache.GetString` | `cache_get_string()` |
#### Multi-Value Returns
Methods with multiple return values use dataclasses:
```python
@dataclass
class CacheGetStringResult:
value: str
exists: bool
def cache_get_string(key: str) -> CacheGetStringResult:
...
```
#### Python Plugin Usage
> **Important:** Due to a limitation in extism-py, you cannot directly import the generated Python wrappers.
> The `@extism.import_fn` decorators are only detected when defined in the plugin's main `__init__.py` file.
> Generated Python files serve as **reference/template code** - copy the needed functions into your plugin.
Example of copying the generated wrapper into your plugin's `__init__.py`:
```python
import extism
import json
# Copy host function declarations from generated files into your __init__.py
@extism.import_fn("extism:host/user", "subsonicapi_call")
def _host_subsonicapi_call(input_ptr: extism.JsonI64) -> extism.JsonI64:
pass
def subsonicapi_call(endpoint: str) -> str:
"""Call the SubsonicAPI with the given endpoint."""
result = _host_subsonicapi_call(endpoint)
return result
# Now use it in your plugin
@extism.plugin_fn
def my_plugin_function():
try:
response = subsonicapi_call("getAlbumList2?type=random&size=10")
data = json.loads(response)
except Exception as e:
extism.log(extism.LogLevel.Error, f"API error: {e}")
```
### Rust Client Code (extism-pdk WASM)
Generated files are named `nd_host_<servicename>.rs` (lowercase) and placed in the `rust/` subdirectory of the output directory. These files are intended for use in Navidrome plugins built with `extism-pdk`. Each file includes:
- `// Code generated by hostgen. DO NOT EDIT.` header
- Required imports (`extism_pdk::*`, `serde`)
- Request/response struct types with `#[derive(Serialize, Deserialize)]`
- `#[host_fn]` extern blocks for raw host function imports
- Public wrapper functions with `Result<T, Error>` returns and snake_case names
#### Rust Type Mapping
| Go Type | Rust Type | Notes |
|-------------------------------|-------------------------------|------------------------------|
| `string` | `String` / `&str` | `&str` for params |
| `int`, `int32` | `i32` | |
| `int64` | `i64` | |
| `float32` | `f32` | |
| `float64` | `f64` | |
| `bool` | `bool` | |
| `[]byte` | `Vec<u8>` | |
| `[]T` | `Vec<T>` | |
| `map[K]V` | `HashMap<K, V>` | From `std::collections` |
| `*T` | `Option<T>` | |
| `interface{}` / `any` | `serde_json::Value` | |
#### Rust Function Naming
Functions follow Rust snake_case convention:
| Go Method | Rust Function |
|-------------------------------|----------------------------------|
| `SubsonicAPI.Call` | `subsonicapi_call()` |
| `Scheduler.ScheduleRecurring` | `scheduler_schedule_recurring()` |
| `Cache.GetString` | `cache_get_string()` |
#### Rust Plugin Usage
The generated Rust wrappers form a library crate (`nd-host`) that plugins can depend on. Add the dependency to your plugin's `Cargo.toml`:
```toml
[dependencies]
extism-pdk = "1.2"
nd-host = { path = "../../host/rust" }
```
Then import and use the services:
```rust
use nd_host::{cache, scheduler, artwork};
#[plugin_fn]
pub fn my_callback(input: String) -> FnResult<String> {
// Use cache service
cache::cache_set_string("key", "value", 3600)?;
let value = cache::cache_get_string("key")?;
// Schedule a task
scheduler::scheduler_schedule_one_time(60, "payload", "task-id")?;
// Get artwork URL
let url = artwork::artwork_get_track_url("track-id", 300)?;
Ok("done")
}
```
See [discord-rich-presence-rs](../examples/discord-rich-presence-rs/) for a complete example using all Rust host wrappers.
## Troubleshooting
### Annotations Not Detected
Ensure annotations are on consecutive lines with no blank `//` lines:
```go
// ✅ Correct
// Documentation for the service.
//nd:hostservice name=Test permission=test
// ❌ Wrong - blank comment line breaks detection
// Documentation for the service.
//
//nd:hostservice name=Test permission=test
```
### Methods Not Exported
Methods without `//nd:hostfunc` annotation are skipped. Ensure the annotation is directly above the method:
```go
// ✅ Correct
// Method documentation.
//nd:hostfunc
MyMethod(ctx context.Context) error
// ❌ Wrong - annotation not directly above method
//nd:hostfunc
MyMethod(ctx context.Context) error
```
### Generated Files Skipped
Files ending in `_gen.go` are automatically skipped during parsing to avoid processing previously generated code.
## Development
Run tests:
```bash
go test -v ./plugins/cmd/hostgen/...
```
The test suite includes:
- CLI integration tests
- Complex type handling (structs, slices, maps, pointers)
- Multiple return value scenarios
- Error cases and edge conditions

View File

@ -1,597 +0,0 @@
package main
import (
"fmt"
"go/format"
"os"
"os/exec"
"path/filepath"
"strings"
. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
)
var _ = Describe("hostgen CLI", Ordered, func() {
var (
testDir string
outputDir string
hostgenBin string
)
BeforeAll(func() {
// Set testdata directory
testdataDir = filepath.Join(mustGetWd(GinkgoT()), "plugins", "cmd", "hostgen", "testdata")
// Build the hostgen binary
hostgenBin = filepath.Join(os.TempDir(), "hostgen-test")
cmd := exec.Command("go", "build", "-o", hostgenBin, ".")
cmd.Dir = filepath.Join(mustGetWd(GinkgoT()), "plugins", "cmd", "hostgen")
output, err := cmd.CombinedOutput()
Expect(err).ToNot(HaveOccurred(), "Failed to build hostgen: %s", output)
DeferCleanup(func() {
os.Remove(hostgenBin)
})
})
BeforeEach(func() {
var err error
testDir, err = os.MkdirTemp("", "hostgen-test-input-*")
Expect(err).ToNot(HaveOccurred())
outputDir, err = os.MkdirTemp("", "hostgen-test-output-*")
Expect(err).ToNot(HaveOccurred())
})
AfterEach(func() {
os.RemoveAll(testDir)
os.RemoveAll(outputDir)
})
Describe("CLI flags and behavior", func() {
BeforeEach(func() {
serviceCode := `package testpkg
import "context"
//nd:hostservice name=Test permission=test
type TestService interface {
//nd:hostfunc
DoAction(ctx context.Context, input string) (output string, err error)
}
`
Expect(os.WriteFile(filepath.Join(testDir, "service.go"), []byte(serviceCode), 0600)).To(Succeed())
})
It("supports verbose mode", func() {
cmd := exec.Command(hostgenBin, "-input", testDir, "-output", outputDir, "-package", "testpkg", "-v")
output, err := cmd.CombinedOutput()
Expect(err).ToNot(HaveOccurred(), "Command failed: %s", output)
outputStr := string(output)
Expect(outputStr).To(ContainSubstring("Input directory:"))
Expect(outputStr).To(ContainSubstring("Output directory:"))
Expect(outputStr).To(ContainSubstring("Found 1 host service(s)"))
Expect(outputStr).To(ContainSubstring("Generated"))
})
It("supports dry-run mode", func() {
cmd := exec.Command(hostgenBin, "-input", testDir, "-output", outputDir, "-package", "testpkg", "-dry-run")
output, err := cmd.CombinedOutput()
Expect(err).ToNot(HaveOccurred(), "Command failed: %s", output)
Expect(string(output)).To(ContainSubstring("RegisterTestHostFunctions"))
Expect(filepath.Join(outputDir, "test_gen.go")).ToNot(BeAnExistingFile())
})
It("infers package name from output directory", func() {
customOutput, err := os.MkdirTemp("", "mypkg")
Expect(err).ToNot(HaveOccurred())
defer os.RemoveAll(customOutput)
cmd := exec.Command(hostgenBin, "-input", testDir, "-output", customOutput)
_, err = cmd.CombinedOutput()
Expect(err).ToNot(HaveOccurred())
content, err := os.ReadFile(filepath.Join(customOutput, "test_gen.go"))
Expect(err).ToNot(HaveOccurred())
Expect(string(content)).To(ContainSubstring("package mypkg"))
})
It("returns error for invalid input directory", func() {
cmd := exec.Command(hostgenBin, "-input", "/nonexistent/path")
output, err := cmd.CombinedOutput()
Expect(err).To(HaveOccurred())
Expect(string(output)).To(ContainSubstring("parsing source files"))
})
It("handles no annotated services gracefully", func() {
Expect(os.WriteFile(filepath.Join(testDir, "service.go"), []byte("package testpkg\n"), 0600)).To(Succeed())
cmd := exec.Command(hostgenBin, "-input", testDir, "-output", outputDir, "-v")
output, err := cmd.CombinedOutput()
Expect(err).ToNot(HaveOccurred(), "Command failed: %s", output)
Expect(string(output)).To(ContainSubstring("No host services found"))
})
It("generates separate files for multiple services", func() {
// Remove service.go created by BeforeEach
Expect(os.Remove(filepath.Join(testDir, "service.go"))).To(Succeed())
service1 := `package testpkg
import "context"
//nd:hostservice name=ServiceA permission=a
type ServiceA interface {
//nd:hostfunc
MethodA(ctx context.Context) error
}
`
service2 := `package testpkg
import "context"
//nd:hostservice name=ServiceB permission=b
type ServiceB interface {
//nd:hostfunc
MethodB(ctx context.Context) error
}
`
Expect(os.WriteFile(filepath.Join(testDir, "a.go"), []byte(service1), 0600)).To(Succeed())
Expect(os.WriteFile(filepath.Join(testDir, "b.go"), []byte(service2), 0600)).To(Succeed())
cmd := exec.Command(hostgenBin, "-input", testDir, "-output", outputDir, "-package", "testpkg", "-v")
output, err := cmd.CombinedOutput()
Expect(err).ToNot(HaveOccurred(), "Command failed: %s", output)
Expect(string(output)).To(ContainSubstring("Found 2 host service(s)"))
Expect(filepath.Join(outputDir, "servicea_gen.go")).To(BeAnExistingFile())
Expect(filepath.Join(outputDir, "serviceb_gen.go")).To(BeAnExistingFile())
})
It("generates only host code with -host-only flag", func() {
cmd := exec.Command(hostgenBin, "-input", testDir, "-output", outputDir, "-package", "testpkg", "-host-only")
output, err := cmd.CombinedOutput()
Expect(err).ToNot(HaveOccurred(), "Command failed: %s", output)
Expect(filepath.Join(outputDir, "test_gen.go")).To(BeAnExistingFile())
Expect(filepath.Join(outputDir, "go")).ToNot(BeADirectory())
})
It("generates only client code with -plugin-only flag", func() {
cmd := exec.Command(hostgenBin, "-input", testDir, "-output", outputDir, "-package", "main", "-plugin-only")
output, err := cmd.CombinedOutput()
Expect(err).ToNot(HaveOccurred(), "Command failed: %s", output)
// Host code should not exist in output root
entries, err := os.ReadDir(outputDir)
Expect(err).ToNot(HaveOccurred())
var genFiles []string
for _, e := range entries {
if e.Name() != "go" {
genFiles = append(genFiles, e.Name())
}
}
Expect(genFiles).To(BeEmpty(), "Expected no host code files, found: %v", genFiles)
// Client code should exist in go/ subdirectory
Expect(filepath.Join(outputDir, "go")).To(BeADirectory())
Expect(filepath.Join(outputDir, "go", "nd_host_test.go")).To(BeAnExistingFile())
})
It("generates both host and client code by default", func() {
cmd := exec.Command(hostgenBin, "-input", testDir, "-output", outputDir, "-package", "testpkg")
output, err := cmd.CombinedOutput()
Expect(err).ToNot(HaveOccurred(), "Command failed: %s", output)
// Host code in output root
Expect(filepath.Join(outputDir, "test_gen.go")).To(BeAnExistingFile())
// Client code in go/ subdirectory
Expect(filepath.Join(outputDir, "go")).To(BeADirectory())
Expect(filepath.Join(outputDir, "go", "nd_host_test.go")).To(BeAnExistingFile())
})
It("rejects using both -host-only and -plugin-only together", func() {
cmd := exec.Command(hostgenBin, "-input", testDir, "-output", outputDir, "-package", "testpkg", "-host-only", "-plugin-only")
output, err := cmd.CombinedOutput()
Expect(err).To(HaveOccurred())
Expect(string(output)).To(ContainSubstring("-host-only and -plugin-only cannot be used together"))
})
})
Describe("code generation", func() {
DescribeTable("generates correct host and client output",
func(serviceFile, hostExpectedFile, goClientExpectedFile, pyClientExpectedFile, rsClientExpectedFile string) {
serviceCode := readTestdata(serviceFile)
hostExpected := readTestdata(hostExpectedFile)
goClientExpected := readTestdata(goClientExpectedFile)
pyClientExpected := readTestdata(pyClientExpectedFile)
rsClientExpected := readTestdata(rsClientExpectedFile)
Expect(os.WriteFile(filepath.Join(testDir, "service.go"), []byte(serviceCode), 0600)).To(Succeed())
// Generate host and all client code (Go, Python, Rust)
cmd := exec.Command(hostgenBin, "-input", testDir, "-output", outputDir, "-package", "testpkg", "-go", "-python", "-rust")
output, err := cmd.CombinedOutput()
Expect(err).ToNot(HaveOccurred(), "Command failed: %s", output)
// Verify host code
entries, err := os.ReadDir(outputDir)
Expect(err).ToNot(HaveOccurred())
var hostFiles []string
for _, e := range entries {
if e.Name() != "go" && e.Name() != "python" && e.Name() != "rust" && !e.IsDir() {
hostFiles = append(hostFiles, e.Name())
}
}
Expect(hostFiles).To(HaveLen(1), "Expected exactly one host file, got: %v", hostFiles)
hostActual, err := os.ReadFile(filepath.Join(outputDir, hostFiles[0]))
Expect(err).ToNot(HaveOccurred())
formattedHostActual, err := format.Source(hostActual)
Expect(err).ToNot(HaveOccurred(), "Generated host code is not valid Go:\n%s", hostActual)
formattedHostExpected, err := format.Source([]byte(hostExpected))
Expect(err).ToNot(HaveOccurred(), "Expected host code is not valid Go")
Expect(string(formattedHostActual)).To(Equal(string(formattedHostExpected)), "Host code mismatch")
// Verify Go client code
goDir := filepath.Join(outputDir, "go")
goClientEntries, err := os.ReadDir(goDir)
Expect(err).ToNot(HaveOccurred())
Expect(goClientEntries).To(HaveLen(4), "Expected Go client file, stub file, doc.go, and go.mod")
// Find the client file (not doc.go, go.mod, or stub)
var goClientName string
for _, entry := range goClientEntries {
name := entry.Name()
if name != "doc.go" && name != "go.mod" && !strings.HasSuffix(name, "_stub.go") {
goClientName = name
break
}
}
Expect(goClientName).ToNot(BeEmpty(), "Expected to find Go client file")
goClientActual, err := os.ReadFile(filepath.Join(goDir, goClientName))
Expect(err).ToNot(HaveOccurred())
formattedGoClientActual, err := format.Source(goClientActual)
Expect(err).ToNot(HaveOccurred(), "Generated Go client code is not valid Go:\n%s", goClientActual)
formattedGoClientExpected, err := format.Source([]byte(goClientExpected))
Expect(err).ToNot(HaveOccurred(), "Expected Go client code is not valid Go")
Expect(string(formattedGoClientActual)).To(Equal(string(formattedGoClientExpected)), "Go client code mismatch")
// Verify Python client code
pythonDir := filepath.Join(outputDir, "python")
pyClientEntries, err := os.ReadDir(pythonDir)
Expect(err).ToNot(HaveOccurred())
Expect(pyClientEntries).To(HaveLen(1), "Expected exactly one Python client file")
pyClientActual, err := os.ReadFile(filepath.Join(pythonDir, pyClientEntries[0].Name()))
Expect(err).ToNot(HaveOccurred())
Expect(string(pyClientActual)).To(Equal(pyClientExpected), "Python client code mismatch")
// Verify Rust client code
rustDir := filepath.Join(outputDir, "rust")
rsClientEntries, err := os.ReadDir(rustDir)
Expect(err).ToNot(HaveOccurred())
Expect(rsClientEntries).To(HaveLen(2), "Expected Rust client file and lib.rs")
// Find the client file (not lib.rs)
var rsClientName string
for _, entry := range rsClientEntries {
if entry.Name() != "lib.rs" {
rsClientName = entry.Name()
break
}
}
Expect(rsClientName).ToNot(BeEmpty(), "Expected to find Rust client file")
rsClientActual, err := os.ReadFile(filepath.Join(rustDir, rsClientName))
Expect(err).ToNot(HaveOccurred())
Expect(string(rsClientActual)).To(Equal(rsClientExpected), "Rust client code mismatch")
},
Entry("simple string params",
"echo_service.go", "echo_expected.go", "echo_client_expected.go", "echo_client_expected.py", "echo_client_expected.rs"),
Entry("multiple simple params (int32)",
"math_service.go", "math_expected.go", "math_client_expected.go", "math_client_expected.py", "math_client_expected.rs"),
Entry("struct param with request type",
"store_service.go", "store_expected.go", "store_client_expected.go", "store_client_expected.py", "store_client_expected.rs"),
Entry("mixed simple and complex params",
"list_service.go", "list_expected.go", "list_client_expected.go", "list_client_expected.py", "list_client_expected.rs"),
Entry("method without error",
"counter_service.go", "counter_expected.go", "counter_client_expected.go", "counter_client_expected.py", "counter_client_expected.rs"),
Entry("no params, error only",
"ping_service.go", "ping_expected.go", "ping_client_expected.go", "ping_client_expected.py", "ping_client_expected.rs"),
Entry("map and interface types",
"meta_service.go", "meta_expected.go", "meta_client_expected.go", "meta_client_expected.py", "meta_client_expected.rs"),
Entry("pointer types",
"users_service.go", "users_expected.go", "users_client_expected.go", "users_client_expected.py", "users_client_expected.rs"),
Entry("multiple returns",
"search_service.go", "search_expected.go", "search_client_expected.go", "search_client_expected.py", "search_client_expected.rs"),
Entry("bytes",
"codec_service.go", "codec_expected.go", "codec_client_expected.go", "codec_client_expected.py", "codec_client_expected.rs"),
)
It("generates compilable host code for comprehensive service", func() {
serviceCode := readTestdata("comprehensive_service.go")
Expect(os.WriteFile(filepath.Join(testDir, "service.go"), []byte(serviceCode), 0600)).To(Succeed())
// Create go.mod
goMod := "module testpkg\n\ngo 1.23\n\nrequire github.com/extism/go-sdk v1.7.1\n"
Expect(os.WriteFile(filepath.Join(testDir, "go.mod"), []byte(goMod), 0600)).To(Succeed())
// Generate host code only
cmd := exec.Command(hostgenBin, "-input", testDir, "-output", testDir, "-package", "testpkg", "-host-only")
output, err := cmd.CombinedOutput()
Expect(err).ToNot(HaveOccurred(), "Generation failed: %s", output)
// Tidy dependencies
goGetCmd := exec.Command("go", "mod", "tidy")
goGetCmd.Dir = testDir
goGetOutput, err := goGetCmd.CombinedOutput()
Expect(err).ToNot(HaveOccurred(), "go mod tidy failed: %s", goGetOutput)
// Build
buildCmd := exec.Command("go", "build", ".")
buildCmd.Dir = testDir
buildOutput, err := buildCmd.CombinedOutput()
Expect(err).ToNot(HaveOccurred(), "Build failed: %s", buildOutput)
})
It("generates compilable client code for comprehensive service", func() {
serviceCode := readTestdata("comprehensive_service.go")
Expect(os.WriteFile(filepath.Join(testDir, "service.go"), []byte(serviceCode), 0600)).To(Succeed())
// Generate client code only to a separate client directory
clientDir := filepath.Join(outputDir, "client")
cmd := exec.Command(hostgenBin, "-input", testDir, "-output", clientDir, "-package", "main", "-plugin-only")
output, err := cmd.CombinedOutput()
Expect(err).ToNot(HaveOccurred(), "Generation failed: %s", output)
// Read generated client code
goDir := filepath.Join(clientDir, "go")
entries, err := os.ReadDir(goDir)
Expect(err).ToNot(HaveOccurred())
Expect(entries).To(HaveLen(4), "Expected Go client file, stub file, doc.go, and go.mod")
// Find the client file (not doc.go, go.mod, or stub)
var clientFileName string
for _, entry := range entries {
name := entry.Name()
if name != "doc.go" && name != "go.mod" && !strings.HasSuffix(name, "_stub.go") {
clientFileName = name
break
}
}
Expect(clientFileName).ToNot(BeEmpty(), "Expected to find Go client file")
content, err := os.ReadFile(filepath.Join(goDir, clientFileName))
Expect(err).ToNot(HaveOccurred())
// Verify key expected content first
contentStr := string(content)
// Should have wasmimport declarations for all methods
Expect(contentStr).To(ContainSubstring("//go:wasmimport extism:host/user comprehensive_simpleparams"))
Expect(contentStr).To(ContainSubstring("//go:wasmimport extism:host/user comprehensive_structparam"))
Expect(contentStr).To(ContainSubstring("//go:wasmimport extism:host/user comprehensive_noerror"))
Expect(contentStr).To(ContainSubstring("//go:wasmimport extism:host/user comprehensive_noparams"))
Expect(contentStr).To(ContainSubstring("//go:wasmimport extism:host/user comprehensive_noparamsnoreturns"))
// Should have response types for methods with complex returns
Expect(contentStr).To(ContainSubstring("type ComprehensiveSimpleParamsResponse struct"))
Expect(contentStr).To(ContainSubstring("type ComprehensiveMultipleReturnsResponse struct"))
// Should have wrapper functions
Expect(contentStr).To(ContainSubstring("func ComprehensiveSimpleParams("))
Expect(contentStr).To(ContainSubstring("func ComprehensiveNoParams()"))
Expect(contentStr).To(ContainSubstring("func ComprehensiveNoParamsNoReturns()"))
// The generated code is now package ndhost, so we need to import it
// Create a plugin directory with proper import structure
pluginDir := filepath.Join(clientDir, "plugin")
Expect(os.MkdirAll(pluginDir, 0750)).To(Succeed())
// Create go.mod for the plugin that imports the generated library
goMod := fmt.Sprintf(`module testplugin
go 1.24
require github.com/navidrome/navidrome/plugins/host/go v0.0.0
replace github.com/navidrome/navidrome/plugins/host/go => %s
`, goDir)
Expect(os.WriteFile(filepath.Join(pluginDir, "go.mod"), []byte(goMod), 0600)).To(Succeed())
// Add a simple main function that imports and uses the ndhost package
mainGo := `package main
import ndhost "github.com/navidrome/navidrome/plugins/host/go"
func main() {}
// Use some functions to ensure import is not unused
var _ = ndhost.ComprehensiveNoParams
`
Expect(os.WriteFile(filepath.Join(pluginDir, "main.go"), []byte(mainGo), 0600)).To(Succeed())
// Tidy dependencies for the generated go library
goTidyLibCmd := exec.Command("go", "mod", "tidy")
goTidyLibCmd.Dir = goDir
goTidyLibOutput, err := goTidyLibCmd.CombinedOutput()
Expect(err).ToNot(HaveOccurred(), "go mod tidy (library) failed: %s", goTidyLibOutput)
// Tidy dependencies for the plugin
goTidyCmd := exec.Command("go", "mod", "tidy")
goTidyCmd.Dir = pluginDir
goTidyOutput, err := goTidyCmd.CombinedOutput()
Expect(err).ToNot(HaveOccurred(), "go mod tidy (plugin) failed: %s", goTidyOutput)
// Build as WASM plugin - this validates the client code compiles correctly
buildCmd := exec.Command("go", "build", "-buildmode=c-shared", "-o", "plugin.wasm", ".")
buildCmd.Dir = pluginDir
buildCmd.Env = append(os.Environ(), "GOOS=wasip1", "GOARCH=wasm")
buildOutput, err := buildCmd.CombinedOutput()
Expect(err).ToNot(HaveOccurred(), "WASM build failed: %s", buildOutput)
// Verify .wasm file was created
Expect(filepath.Join(pluginDir, "plugin.wasm")).To(BeAnExistingFile())
})
It("generates Python client code with -python flag", func() {
serviceCode := `package testpkg
import "context"
//nd:hostservice name=Test permission=test
type TestService interface {
//nd:hostfunc
DoAction(ctx context.Context, input string) (output string, err error)
}
`
Expect(os.WriteFile(filepath.Join(testDir, "service.go"), []byte(serviceCode), 0600)).To(Succeed())
cmd := exec.Command(hostgenBin, "-input", testDir, "-output", outputDir, "-package", "testpkg", "-python", "-plugin-only")
output, err := cmd.CombinedOutput()
Expect(err).ToNot(HaveOccurred(), "Command failed: %s", output)
// Verify Python client code exists
pythonDir := filepath.Join(outputDir, "python")
Expect(pythonDir).To(BeADirectory())
pythonFile := filepath.Join(pythonDir, "nd_host_test.py")
Expect(pythonFile).To(BeAnExistingFile())
content, err := os.ReadFile(pythonFile)
Expect(err).ToNot(HaveOccurred())
contentStr := string(content)
Expect(contentStr).To(ContainSubstring("Code generated by hostgen. DO NOT EDIT."))
Expect(contentStr).To(ContainSubstring("class HostFunctionError(Exception):"))
Expect(contentStr).To(ContainSubstring(`@extism.import_fn("extism:host/user", "test_doaction")`))
Expect(contentStr).To(ContainSubstring("def test_do_action(input: str) -> str:"))
})
It("generates both Go and Python client code with -go -python flags", func() {
serviceCode := `package testpkg
import "context"
//nd:hostservice name=Test permission=test
type TestService interface {
//nd:hostfunc
DoAction(ctx context.Context, input string) (output string, err error)
}
`
Expect(os.WriteFile(filepath.Join(testDir, "service.go"), []byte(serviceCode), 0600)).To(Succeed())
cmd := exec.Command(hostgenBin, "-input", testDir, "-output", outputDir, "-package", "testpkg", "-go", "-python", "-plugin-only")
output, err := cmd.CombinedOutput()
Expect(err).ToNot(HaveOccurred(), "Command failed: %s", output)
// Verify both Go and Python client code exist
goDir := filepath.Join(outputDir, "go")
Expect(goDir).To(BeADirectory())
Expect(filepath.Join(goDir, "nd_host_test.go")).To(BeAnExistingFile())
pythonDir := filepath.Join(outputDir, "python")
Expect(pythonDir).To(BeADirectory())
Expect(filepath.Join(pythonDir, "nd_host_test.py")).To(BeAnExistingFile())
})
It("generates Python code with dataclass for multi-value returns", func() {
serviceCode := `package testpkg
import "context"
//nd:hostservice name=Cache permission=cache
type CacheService interface {
//nd:hostfunc
GetString(ctx context.Context, key string) (value string, exists bool, err error)
}
`
Expect(os.WriteFile(filepath.Join(testDir, "service.go"), []byte(serviceCode), 0600)).To(Succeed())
cmd := exec.Command(hostgenBin, "-input", testDir, "-output", outputDir, "-package", "testpkg", "-python", "-plugin-only")
output, err := cmd.CombinedOutput()
Expect(err).ToNot(HaveOccurred(), "Command failed: %s", output)
content, err := os.ReadFile(filepath.Join(outputDir, "python", "nd_host_cache.py"))
Expect(err).ToNot(HaveOccurred())
contentStr := string(content)
Expect(contentStr).To(ContainSubstring("@dataclass"))
Expect(contentStr).To(ContainSubstring("class CacheGetStringResult:"))
Expect(contentStr).To(ContainSubstring("value: str"))
Expect(contentStr).To(ContainSubstring("exists: bool"))
Expect(contentStr).To(ContainSubstring("def cache_get_string(key: str) -> CacheGetStringResult:"))
})
It("generates Python code for methods with no parameters", func() {
serviceCode := `package testpkg
import "context"
//nd:hostservice name=Test permission=test
type TestService interface {
//nd:hostfunc
Ping(ctx context.Context) (status string, err error)
}
`
Expect(os.WriteFile(filepath.Join(testDir, "service.go"), []byte(serviceCode), 0600)).To(Succeed())
cmd := exec.Command(hostgenBin, "-input", testDir, "-output", outputDir, "-package", "testpkg", "-python", "-plugin-only")
output, err := cmd.CombinedOutput()
Expect(err).ToNot(HaveOccurred(), "Command failed: %s", output)
content, err := os.ReadFile(filepath.Join(outputDir, "python", "nd_host_test.py"))
Expect(err).ToNot(HaveOccurred())
contentStr := string(content)
Expect(contentStr).To(ContainSubstring("def test_ping() -> str:"))
Expect(contentStr).To(ContainSubstring(`request_bytes = b"{}"`))
})
})
})
var testdataDir string
func readTestdata(filename string) string {
content, err := os.ReadFile(filepath.Join(testdataDir, filename))
Expect(err).ToNot(HaveOccurred(), "Failed to read testdata file: %s", filename)
return string(content)
}
func mustGetWd(t FullGinkgoTInterface) string {
dir, err := os.Getwd()
if err != nil {
t.Fatal(err)
}
for {
if _, err := os.Stat(filepath.Join(dir, "go.mod")); err == nil {
return dir
}
parent := filepath.Dir(dir)
if parent == dir {
t.Fatal("could not find project root")
}
dir = parent
}
}

View File

@ -1,301 +0,0 @@
package internal
import (
"bytes"
"embed"
"fmt"
"strings"
"text/template"
)
//go:embed templates/*.tmpl
var templatesFS embed.FS
// hostFuncMap returns the template functions for host code generation.
func hostFuncMap(svc Service) template.FuncMap {
return template.FuncMap{
"lower": strings.ToLower,
"title": strings.Title,
"exportName": func(m Method) string { return m.FunctionName(svc.ExportPrefix()) },
"requestType": func(m Method) string { return m.RequestTypeName(svc.Name) },
"responseType": func(m Method) string { return m.ResponseTypeName(svc.Name) },
}
}
// clientFuncMap returns the template functions for client code generation.
func clientFuncMap(svc Service) template.FuncMap {
return template.FuncMap{
"lower": strings.ToLower,
"title": strings.Title,
"exportName": func(m Method) string { return m.FunctionName(svc.ExportPrefix()) },
"requestType": func(m Method) string { return m.RequestTypeName(svc.Name) },
"responseType": func(m Method) string { return m.ResponseTypeName(svc.Name) },
"formatDoc": formatDoc,
}
}
// pythonFuncMap returns the template functions for Python client code generation.
func pythonFuncMap(svc Service) template.FuncMap {
return template.FuncMap{
"lower": strings.ToLower,
"exportName": func(m Method) string { return m.FunctionName(svc.ExportPrefix()) },
"pythonFunc": func(m Method) string { return m.PythonFunctionName(svc.ExportPrefix()) },
"pythonResultType": func(m Method) string { return m.PythonResultTypeName(svc.Name) },
"pythonDefault": pythonDefaultValue,
}
}
// GenerateHost generates the host function wrapper code for a service.
func GenerateHost(svc Service, pkgName string) ([]byte, error) {
tmplContent, err := templatesFS.ReadFile("templates/host.go.tmpl")
if err != nil {
return nil, fmt.Errorf("reading host template: %w", err)
}
tmpl, err := template.New("host").Funcs(hostFuncMap(svc)).Parse(string(tmplContent))
if err != nil {
return nil, fmt.Errorf("parsing template: %w", err)
}
data := templateData{
Package: pkgName,
Service: svc,
}
var buf bytes.Buffer
if err := tmpl.Execute(&buf, data); err != nil {
return nil, fmt.Errorf("executing template: %w", err)
}
return buf.Bytes(), nil
}
// GenerateClientGo generates client wrapper code for plugins to call host functions.
func GenerateClientGo(svc Service) ([]byte, error) {
tmplContent, err := templatesFS.ReadFile("templates/client.go.tmpl")
if err != nil {
return nil, fmt.Errorf("reading client template: %w", err)
}
tmpl, err := template.New("client").Funcs(clientFuncMap(svc)).Parse(string(tmplContent))
if err != nil {
return nil, fmt.Errorf("parsing template: %w", err)
}
data := templateData{
Service: svc,
}
var buf bytes.Buffer
if err := tmpl.Execute(&buf, data); err != nil {
return nil, fmt.Errorf("executing template: %w", err)
}
return buf.Bytes(), nil
}
// GenerateClientGoStub generates stub code for non-WASM platforms.
// These stubs provide type definitions and function signatures for IDE support,
// but panic at runtime since host functions are only available in WASM plugins.
func GenerateClientGoStub(svc Service) ([]byte, error) {
tmplContent, err := templatesFS.ReadFile("templates/client_stub.go.tmpl")
if err != nil {
return nil, fmt.Errorf("reading client stub template: %w", err)
}
tmpl, err := template.New("client_stub").Funcs(clientFuncMap(svc)).Parse(string(tmplContent))
if err != nil {
return nil, fmt.Errorf("parsing template: %w", err)
}
data := templateData{
Service: svc,
}
var buf bytes.Buffer
if err := tmpl.Execute(&buf, data); err != nil {
return nil, fmt.Errorf("executing template: %w", err)
}
return buf.Bytes(), nil
}
type templateData struct {
Package string
Service Service
}
// formatDoc formats a documentation string for Go comments.
// It prefixes each line with "// " and trims trailing whitespace.
func formatDoc(doc string) string {
if doc == "" {
return ""
}
lines := strings.Split(strings.TrimSpace(doc), "\n")
var result []string
for _, line := range lines {
result = append(result, "// "+strings.TrimRight(line, " \t"))
}
return strings.Join(result, "\n")
}
// GenerateClientPython generates Python client wrapper code for plugins.
func GenerateClientPython(svc Service) ([]byte, error) {
tmplContent, err := templatesFS.ReadFile("templates/client.py.tmpl")
if err != nil {
return nil, fmt.Errorf("reading Python client template: %w", err)
}
tmpl, err := template.New("client_py").Funcs(pythonFuncMap(svc)).Parse(string(tmplContent))
if err != nil {
return nil, fmt.Errorf("parsing template: %w", err)
}
data := templateData{
Service: svc,
}
var buf bytes.Buffer
if err := tmpl.Execute(&buf, data); err != nil {
return nil, fmt.Errorf("executing template: %w", err)
}
return buf.Bytes(), nil
}
// pythonDefaultValue returns a Python default value for response.get() calls.
func pythonDefaultValue(p Param) string {
switch p.Type {
case "string":
return `, ""`
case "int", "int32", "int64":
return ", 0"
case "float32", "float64":
return ", 0.0"
case "bool":
return ", False"
case "[]byte":
return ", b\"\""
default:
return ", None"
}
}
// rustFuncMap returns the template functions for Rust client code generation.
func rustFuncMap(svc Service) template.FuncMap {
knownStructs := svc.KnownStructs()
return template.FuncMap{
"lower": strings.ToLower,
"exportName": func(m Method) string { return m.FunctionName(svc.ExportPrefix()) },
"requestType": func(m Method) string { return m.RequestTypeName(svc.Name) },
"responseType": func(m Method) string { return m.ResponseTypeName(svc.Name) },
"rustFunc": func(m Method) string { return m.RustFunctionName(svc.ExportPrefix()) },
"rustDocComment": RustDocComment,
"rustType": func(p Param) string { return p.RustTypeWithStructs(knownStructs) },
"rustParamType": func(p Param) string { return p.RustParamTypeWithStructs(knownStructs) },
"fieldRustType": func(f FieldDef) string { return f.RustType(knownStructs) },
}
}
// GenerateClientRust generates Rust client wrapper code for plugins.
func GenerateClientRust(svc Service) ([]byte, error) {
tmplContent, err := templatesFS.ReadFile("templates/client.rs.tmpl")
if err != nil {
return nil, fmt.Errorf("reading Rust client template: %w", err)
}
tmpl, err := template.New("client_rs").Funcs(rustFuncMap(svc)).Parse(string(tmplContent))
if err != nil {
return nil, fmt.Errorf("parsing template: %w", err)
}
data := templateData{
Service: svc,
}
var buf bytes.Buffer
if err := tmpl.Execute(&buf, data); err != nil {
return nil, fmt.Errorf("executing template: %w", err)
}
return buf.Bytes(), nil
}
// firstLine returns the first line of a multi-line string, with the first word removed.
func firstLine(s string) string {
line := s
if idx := strings.Index(s, "\n"); idx >= 0 {
line = s[:idx]
}
// Remove the first word (service name like "ArtworkService")
if idx := strings.Index(line, " "); idx >= 0 {
line = line[idx+1:]
}
return line
}
// GenerateRustLib generates the lib.rs file that exposes all service modules.
func GenerateRustLib(services []Service) ([]byte, error) {
tmplContent, err := templatesFS.ReadFile("templates/lib.rs.tmpl")
if err != nil {
return nil, fmt.Errorf("reading Rust lib template: %w", err)
}
tmpl, err := template.New("lib_rs").Funcs(template.FuncMap{
"lower": strings.ToLower,
"firstLine": firstLine,
}).Parse(string(tmplContent))
if err != nil {
return nil, fmt.Errorf("parsing template: %w", err)
}
data := struct {
Services []Service
}{
Services: services,
}
var buf bytes.Buffer
if err := tmpl.Execute(&buf, data); err != nil {
return nil, fmt.Errorf("executing template: %w", err)
}
return buf.Bytes(), nil
}
// GenerateGoDoc generates the doc.go file that provides package documentation.
func GenerateGoDoc(services []Service) ([]byte, error) {
tmplContent, err := templatesFS.ReadFile("templates/doc.go.tmpl")
if err != nil {
return nil, fmt.Errorf("reading Go doc template: %w", err)
}
tmpl, err := template.New("doc_go").Funcs(template.FuncMap{
"firstLine": firstLine,
}).Parse(string(tmplContent))
if err != nil {
return nil, fmt.Errorf("parsing template: %w", err)
}
data := struct {
Services []Service
}{
Services: services,
}
var buf bytes.Buffer
if err := tmpl.Execute(&buf, data); err != nil {
return nil, fmt.Errorf("executing template: %w", err)
}
return buf.Bytes(), nil
}
// GenerateGoMod generates the go.mod file for the Go client library.
func GenerateGoMod() ([]byte, error) {
tmplContent, err := templatesFS.ReadFile("templates/go.mod.tmpl")
if err != nil {
return nil, fmt.Errorf("reading go.mod template: %w", err)
}
return tmplContent, nil
}

View File

@ -1,679 +0,0 @@
package internal
import (
"go/format"
"os"
. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
)
var _ = Describe("Generator", func() {
Describe("GenerateHost", func() {
It("should generate valid Go code for a simple service with strings", func() {
// All methods use JSON request/response types
svc := Service{
Name: "SubsonicAPI",
Permission: "subsonicapi",
Interface: "SubsonicAPIService",
Methods: []Method{
{
Name: "Call",
HasError: true,
Params: []Param{NewParam("uri", "string")},
Returns: []Param{NewParam("response", "string")},
},
},
}
code, err := GenerateHost(svc, "host")
Expect(err).NotTo(HaveOccurred())
// Verify the code is valid Go
_, err = format.Source(code)
Expect(err).NotTo(HaveOccurred())
codeStr := string(code)
// Check for generated header
Expect(codeStr).To(ContainSubstring("Code generated by hostgen. DO NOT EDIT."))
// Check for package declaration
Expect(codeStr).To(ContainSubstring("package host"))
// All methods now use request type for JSON protocol
Expect(codeStr).To(ContainSubstring("type SubsonicAPICallRequest struct"))
Expect(codeStr).To(ContainSubstring(`Uri string `))
// Response type with error handling
Expect(codeStr).To(ContainSubstring("type SubsonicAPICallResponse struct"))
Expect(codeStr).To(ContainSubstring(`Response string `))
Expect(codeStr).To(ContainSubstring(`Error string `))
// Check for registration function
Expect(codeStr).To(ContainSubstring("func RegisterSubsonicAPIHostFunctions(service SubsonicAPIService)"))
// Check for host function name
Expect(codeStr).To(ContainSubstring(`"subsonicapi_call"`))
// Check for JSON unmarshal (all methods use JSON now)
Expect(codeStr).To(ContainSubstring("json.Unmarshal"))
})
It("should generate code for methods without parameters", func() {
svc := Service{
Name: "Test",
Permission: "test",
Interface: "TestService",
Methods: []Method{
{
Name: "NoParams",
HasError: true,
Returns: []Param{NewParam("result", "string")},
},
},
}
code, err := GenerateHost(svc, "host")
Expect(err).NotTo(HaveOccurred())
_, err = format.Source(code)
Expect(err).NotTo(HaveOccurred())
codeStr := string(code)
// Methods without params don't need a request type - no params to serialize
Expect(codeStr).NotTo(ContainSubstring("type TestNoParamsRequest struct"))
// But still uses PTR input/output for consistency
Expect(codeStr).To(MatchRegexp(`\[\]extism\.ValueType\{extism\.ValueTypePTR\},\s*\[\]extism\.ValueType\{extism\.ValueTypePTR\}`))
})
It("should generate code for methods without return values", func() {
svc := Service{
Name: "Test",
Permission: "test",
Interface: "TestService",
Methods: []Method{
{
Name: "NoReturn",
HasError: true,
Params: []Param{NewParam("input", "string")},
},
},
}
code, err := GenerateHost(svc, "host")
Expect(err).NotTo(HaveOccurred())
_, err = format.Source(code)
Expect(err).NotTo(HaveOccurred())
})
It("should generate code for multiple methods", func() {
svc := Service{
Name: "Scheduler",
Permission: "scheduler",
Interface: "SchedulerService",
Methods: []Method{
{
Name: "ScheduleRecurring",
HasError: true,
Params: []Param{NewParam("cronExpression", "string")},
Returns: []Param{NewParam("scheduleID", "string")},
},
{
Name: "ScheduleOneTime",
HasError: true,
Params: []Param{NewParam("delaySeconds", "int32")},
Returns: []Param{NewParam("scheduleID", "string")},
},
{
Name: "CancelSchedule",
HasError: true,
Params: []Param{NewParam("scheduleID", "string")},
Returns: []Param{NewParam("canceled", "bool")},
},
},
}
code, err := GenerateHost(svc, "host")
Expect(err).NotTo(HaveOccurred())
_, err = format.Source(code)
Expect(err).NotTo(HaveOccurred())
codeStr := string(code)
Expect(codeStr).To(ContainSubstring("scheduler_schedulerecurring"))
Expect(codeStr).To(ContainSubstring("scheduler_scheduleonetime"))
Expect(codeStr).To(ContainSubstring("scheduler_cancelschedule"))
})
It("should handle multiple simple parameters with JSON", func() {
// All params use JSON - single PTR input
svc := Service{
Name: "Test",
Permission: "test",
Interface: "TestService",
Methods: []Method{
{
Name: "MultiParam",
HasError: true,
Params: []Param{
NewParam("name", "string"),
NewParam("count", "int32"),
NewParam("enabled", "bool"),
},
Returns: []Param{NewParam("result", "string")},
},
},
}
code, err := GenerateHost(svc, "host")
Expect(err).NotTo(HaveOccurred())
_, err = format.Source(code)
Expect(err).NotTo(HaveOccurred())
codeStr := string(code)
// All methods use request type with JSON protocol
Expect(codeStr).To(ContainSubstring("type TestMultiParamRequest struct"))
// Check for JSON unmarshal (all methods use JSON now)
Expect(codeStr).To(ContainSubstring("json.Unmarshal"))
// Check that input/output ValueType both use PTR (JSON)
Expect(codeStr).To(MatchRegexp(`\[\]extism\.ValueType\{extism\.ValueTypePTR\},\s*\[\]extism\.ValueType\{extism\.ValueTypePTR\}`))
})
It("should use single PTR for mixed simple and complex params", func() {
// When any param needs JSON, all are bundled into one request struct
svc := Service{
Name: "Test",
Permission: "test",
Interface: "TestService",
Methods: []Method{
{
Name: "MixedParam",
HasError: true,
Params: []Param{
NewParam("id", "string"), // simple (PTR for string)
NewParam("tags", "[]string"), // complex - needs JSON
},
Returns: []Param{NewParam("count", "int32")}, // simple
},
},
}
code, err := GenerateHost(svc, "host")
Expect(err).NotTo(HaveOccurred())
_, err = format.Source(code)
Expect(err).NotTo(HaveOccurred())
codeStr := string(code)
// Request type IS needed because of complex param
Expect(codeStr).To(ContainSubstring("type TestMixedParamRequest struct"))
// When using request type, only ONE PTR for input (the JSON request)
Expect(codeStr).To(MatchRegexp(`\[\]extism\.ValueType\{extism\.ValueTypePTR\},\s*\[\]extism\.ValueType\{extism\.ValueTypePTR\}`))
})
It("should generate proper JSON tags for complex types", func() {
// Complex types (structs, slices, maps) need JSON serialization
svc := Service{
Name: "Test",
Permission: "test",
Interface: "TestService",
Methods: []Method{
{
Name: "Method",
HasError: true,
Params: []Param{NewParam("inputValue", "[]string")}, // slice needs JSON
Returns: []Param{NewParam("outputValue", "map[string]string")}, // map needs JSON
},
},
}
code, err := GenerateHost(svc, "host")
Expect(err).NotTo(HaveOccurred())
codeStr := string(code)
// Complex params need request type with JSON tags
Expect(codeStr).To(ContainSubstring(`json:"inputValue"`))
// Complex returns need response type with JSON tags
Expect(codeStr).To(ContainSubstring(`json:"outputValue,omitempty"`))
})
It("should include required imports", func() {
// Service with complex types needs JSON import
svc := Service{
Name: "Test",
Permission: "test",
Interface: "TestService",
Methods: []Method{
{
Name: "Method",
HasError: true,
Params: []Param{NewParam("data", "MyStruct")}, // struct needs JSON
},
},
}
code, err := GenerateHost(svc, "host")
Expect(err).NotTo(HaveOccurred())
codeStr := string(code)
Expect(codeStr).To(ContainSubstring(`"context"`))
Expect(codeStr).To(ContainSubstring(`"encoding/json"`))
Expect(codeStr).To(ContainSubstring(`extism "github.com/extism/go-sdk"`))
})
It("should always include json import for JSON protocol", func() {
// All services use JSON protocol, so json import is always needed
svc := Service{
Name: "Test",
Permission: "test",
Interface: "TestService",
Methods: []Method{
{
Name: "Method",
Params: []Param{NewParam("count", "int32")},
Returns: []Param{NewParam("result", "int64")},
},
},
}
code, err := GenerateHost(svc, "host")
Expect(err).NotTo(HaveOccurred())
codeStr := string(code)
Expect(codeStr).To(ContainSubstring(`"context"`))
Expect(codeStr).To(ContainSubstring(`"encoding/json"`))
Expect(codeStr).To(ContainSubstring(`extism "github.com/extism/go-sdk"`))
})
})
Describe("toJSONName", func() {
It("should convert to camelCase matching Rust serde behavior", func() {
Expect(toJSONName("InputValue")).To(Equal("inputValue"))
Expect(toJSONName("URI")).To(Equal("uri"))
Expect(toJSONName("id")).To(Equal("id"))
Expect(toJSONName("ID")).To(Equal("id"))
Expect(toJSONName("ConnectionID")).To(Equal("connectionId"))
Expect(toJSONName("NewConnectionID")).To(Equal("newConnectionId"))
Expect(toJSONName("XMLHTTPRequest")).To(Equal("xmlhttpRequest"))
Expect(toJSONName("APIKey")).To(Equal("apiKey"))
})
It("should handle empty string", func() {
Expect(toJSONName("")).To(Equal(""))
})
})
Describe("NewParam", func() {
It("should create param with auto-generated JSON name", func() {
p := NewParam("MyParam", "string")
Expect(p.Name).To(Equal("MyParam"))
Expect(p.Type).To(Equal("string"))
Expect(p.JSONName).To(Equal("myParam"))
})
})
Describe("Python type and name helpers", func() {
Describe("ToPythonType", func() {
It("should map Go types to Python types", func() {
Expect(ToPythonType("string")).To(Equal("str"))
Expect(ToPythonType("int")).To(Equal("int"))
Expect(ToPythonType("int32")).To(Equal("int"))
Expect(ToPythonType("int64")).To(Equal("int"))
Expect(ToPythonType("float32")).To(Equal("float"))
Expect(ToPythonType("float64")).To(Equal("float"))
Expect(ToPythonType("bool")).To(Equal("bool"))
Expect(ToPythonType("[]byte")).To(Equal("bytes"))
Expect(ToPythonType("unknown")).To(Equal("Any"))
})
})
Describe("ToSnakeCase", func() {
It("should convert PascalCase to snake_case", func() {
Expect(ToSnakeCase("ScheduleRecurring")).To(Equal("schedule_recurring"))
Expect(ToSnakeCase("GetString")).To(Equal("get_string"))
Expect(ToSnakeCase("simple")).To(Equal("simple"))
})
It("should handle acronyms correctly", func() {
Expect(ToSnakeCase("ID")).To(Equal("id"))
Expect(ToSnakeCase("ScheduleID")).To(Equal("schedule_id"))
Expect(ToSnakeCase("NewScheduleID")).To(Equal("new_schedule_id"))
Expect(ToSnakeCase("XMLParser")).To(Equal("xml_parser"))
Expect(ToSnakeCase("GetHTTPResponse")).To(Equal("get_http_response"))
})
})
Describe("Method.PythonFunctionName", func() {
It("should generate snake_case function name with service prefix", func() {
m := Method{Name: "GetString"}
Expect(m.PythonFunctionName("cache")).To(Equal("cache_get_string"))
})
})
Describe("Param.PythonType", func() {
It("should return Python type for parameter", func() {
p := NewParam("value", "string")
Expect(p.PythonType()).To(Equal("str"))
})
})
Describe("Param.PythonName", func() {
It("should return snake_case name for parameter", func() {
p := NewParam("ttlSeconds", "int64")
Expect(p.PythonName()).To(Equal("ttl_seconds"))
})
})
})
Describe("GenerateClientPython", func() {
It("should generate valid Python code for a simple service", func() {
svc := Service{
Name: "SubsonicAPI",
Permission: "subsonicapi",
Interface: "SubsonicAPIService",
Methods: []Method{
{
Name: "Call",
HasError: true,
Params: []Param{NewParam("uri", "string")},
Returns: []Param{NewParam("responseJSON", "string")},
},
},
}
code, err := GenerateClientPython(svc)
Expect(err).NotTo(HaveOccurred())
codeStr := string(code)
// Check for generated header
Expect(codeStr).To(ContainSubstring("Code generated by hostgen. DO NOT EDIT."))
// Check for imports
Expect(codeStr).To(ContainSubstring("from dataclasses import dataclass"))
Expect(codeStr).To(ContainSubstring("import extism"))
Expect(codeStr).To(ContainSubstring("import json"))
// Check for exception class
Expect(codeStr).To(ContainSubstring("class HostFunctionError(Exception):"))
// Check for raw import function
Expect(codeStr).To(ContainSubstring(`@extism.import_fn("extism:host/user", "subsonicapi_call")`))
Expect(codeStr).To(ContainSubstring("def _subsonicapi_call(offset: int) -> int:"))
// Check for wrapper function with type hints
Expect(codeStr).To(ContainSubstring("def subsonicapi_call(uri: str) -> str:"))
// Check for error handling
Expect(codeStr).To(ContainSubstring("raise HostFunctionError(response["))
})
It("should generate dataclass for multi-value returns", func() {
svc := Service{
Name: "Cache",
Permission: "cache",
Interface: "CacheService",
Methods: []Method{
{
Name: "GetString",
HasError: true,
Params: []Param{NewParam("key", "string")},
Returns: []Param{
NewParam("value", "string"),
NewParam("exists", "bool"),
},
},
},
}
code, err := GenerateClientPython(svc)
Expect(err).NotTo(HaveOccurred())
codeStr := string(code)
// Check for dataclass
Expect(codeStr).To(ContainSubstring("@dataclass"))
Expect(codeStr).To(ContainSubstring("class CacheGetStringResult:"))
Expect(codeStr).To(ContainSubstring("value: str"))
Expect(codeStr).To(ContainSubstring("exists: bool"))
// Check that function returns dataclass
Expect(codeStr).To(ContainSubstring("def cache_get_string(key: str) -> CacheGetStringResult:"))
Expect(codeStr).To(ContainSubstring("return CacheGetStringResult("))
})
It("should handle methods with no parameters", func() {
svc := Service{
Name: "Test",
Permission: "test",
Interface: "TestService",
Methods: []Method{
{
Name: "NoParams",
HasError: true,
Returns: []Param{NewParam("result", "string")},
},
},
}
code, err := GenerateClientPython(svc)
Expect(err).NotTo(HaveOccurred())
codeStr := string(code)
// Function with no params
Expect(codeStr).To(ContainSubstring("def test_no_params() -> str:"))
// Empty request
Expect(codeStr).To(ContainSubstring(`request_bytes = b"{}"`))
})
It("should handle methods with no return values", func() {
svc := Service{
Name: "Test",
Permission: "test",
Interface: "TestService",
Methods: []Method{
{
Name: "NoReturn",
HasError: true,
Params: []Param{NewParam("input", "string")},
},
},
}
code, err := GenerateClientPython(svc)
Expect(err).NotTo(HaveOccurred())
codeStr := string(code)
// Function returns None
Expect(codeStr).To(ContainSubstring("def test_no_return(input: str) -> None:"))
})
It("should generate correct Python defaults for different types", func() {
svc := Service{
Name: "Test",
Permission: "test",
Interface: "TestService",
Methods: []Method{
{
Name: "AllTypes",
HasError: true,
Returns: []Param{
NewParam("strVal", "string"),
NewParam("intVal", "int64"),
NewParam("floatVal", "float64"),
NewParam("boolVal", "bool"),
},
},
},
}
code, err := GenerateClientPython(svc)
Expect(err).NotTo(HaveOccurred())
codeStr := string(code)
// Check defaults in response.get() calls
Expect(codeStr).To(ContainSubstring(`response.get("strVal", "")`))
Expect(codeStr).To(ContainSubstring(`response.get("intVal", 0)`))
Expect(codeStr).To(ContainSubstring(`response.get("floatVal", 0.0)`))
Expect(codeStr).To(ContainSubstring(`response.get("boolVal", False)`))
})
})
Describe("GenerateGoDoc", func() {
It("should generate valid doc.go content for multiple services", func() {
services := []Service{
{
Name: "Cache",
Permission: "cache",
Interface: "CacheService",
Doc: "CacheService provides temporary key-value storage with TTL.",
},
{
Name: "Scheduler",
Permission: "scheduler",
Interface: "SchedulerService",
Doc: "SchedulerService manages scheduled tasks.",
},
}
code, err := GenerateGoDoc(services)
Expect(err).NotTo(HaveOccurred())
// Verify it's valid Go code
_, err = format.Source(code)
Expect(err).NotTo(HaveOccurred())
codeStr := string(code)
// Check for generated header
Expect(codeStr).To(ContainSubstring("Code generated by hostgen. DO NOT EDIT."))
// Check for package declaration
Expect(codeStr).To(ContainSubstring("package ndhost"))
// Check for package documentation
Expect(codeStr).To(ContainSubstring("Package ndhost provides Navidrome host function wrappers"))
// Check that services are listed
Expect(codeStr).To(ContainSubstring("Cache:"))
Expect(codeStr).To(ContainSubstring("Scheduler:"))
})
})
Describe("GenerateGoMod", func() {
It("should generate valid go.mod content", func() {
code, err := GenerateGoMod()
Expect(err).NotTo(HaveOccurred())
codeStr := string(code)
// Check for module declaration
Expect(codeStr).To(ContainSubstring("module github.com/navidrome/navidrome/plugins/host/go"))
// Check for Go version
Expect(codeStr).To(ContainSubstring("go 1.24"))
// Check for extism-go-pdk dependency
Expect(codeStr).To(ContainSubstring("github.com/extism/go-pdk"))
})
})
Describe("GenerateClientGoStub", func() {
It("should generate valid stub code with panic functions", func() {
svc := Service{
Name: "Cache",
Permission: "cache",
Interface: "CacheService",
Doc: "CacheService provides caching capabilities.",
Methods: []Method{
{
Name: "Get",
Doc: "Get retrieves a value from the cache.",
Params: []Param{
{Name: "key", Type: "string"},
},
Returns: []Param{
{Name: "value", Type: "string"},
{Name: "exists", Type: "bool"},
},
},
},
}
code, err := GenerateClientGoStub(svc)
Expect(err).NotTo(HaveOccurred())
// Verify it's valid Go code
_, err = format.Source(code)
Expect(err).NotTo(HaveOccurred())
codeStr := string(code)
// Check for build tag (non-WASM)
Expect(codeStr).To(ContainSubstring("//go:build !wasip1"))
// Check for package declaration
Expect(codeStr).To(ContainSubstring("package ndhost"))
// Check for stub comment
Expect(codeStr).To(ContainSubstring("stub implementations for non-WASM builds"))
// Check for panic in function body
Expect(codeStr).To(ContainSubstring(`panic("ndhost: CacheGet is only available in WASM plugins")`))
// Check that types are defined (needed for IDE support)
Expect(codeStr).To(ContainSubstring("type CacheGetResponse struct"))
})
})
Describe("Integration", func() {
It("should generate compilable code from parsed source", func() {
// This is an integration test that verifies the full pipeline
src := `package host
import "context"
// TestService is a test service.
//nd:hostservice name=Test permission=test
type TestService interface {
// DoSomething does something.
//nd:hostfunc
DoSomething(ctx context.Context, input string) (output string, err error)
}
`
// Create temporary directory
tmpDir := GinkgoT().TempDir()
path := tmpDir + "/test.go"
err := writeFile(path, src)
Expect(err).NotTo(HaveOccurred())
// Parse
services, err := ParseDirectory(tmpDir)
Expect(err).NotTo(HaveOccurred())
Expect(services).To(HaveLen(1))
// Generate
code, err := GenerateHost(services[0], "host")
Expect(err).NotTo(HaveOccurred())
// Format (validates syntax)
formatted, err := format.Source(code)
Expect(err).NotTo(HaveOccurred())
// Verify key elements
codeStr := string(formatted)
Expect(codeStr).To(ContainSubstring("RegisterTestHostFunctions"))
Expect(codeStr).To(ContainSubstring(`"test_dosomething"`))
})
})
})
func writeFile(path, content string) error {
return os.WriteFile(path, []byte(content), 0600)
}

View File

@ -1,13 +0,0 @@
package internal
import (
"testing"
. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
)
func TestInternal(t *testing.T) {
RegisterFailHandler(Fail)
RunSpecs(t, "Hostgen Internal Suite")
}

View File

@ -1,474 +0,0 @@
package internal
import (
"fmt"
"go/ast"
"go/parser"
"go/token"
"os"
"path/filepath"
"regexp"
"strings"
)
// Annotation patterns
var (
// //nd:hostservice name=ServiceName permission=key
hostServicePattern = regexp.MustCompile(`//nd:hostservice\s+(.*)`)
// //nd:hostfunc [name=CustomName]
hostFuncPattern = regexp.MustCompile(`//nd:hostfunc(?:\s+(.*))?`)
// key=value pairs
keyValuePattern = regexp.MustCompile(`(\w+)=(\S+)`)
)
// ParseDirectory parses all Go source files in a directory and extracts host services.
func ParseDirectory(dir string) ([]Service, error) {
entries, err := os.ReadDir(dir)
if err != nil {
return nil, fmt.Errorf("reading directory: %w", err)
}
var services []Service
fset := token.NewFileSet()
for _, entry := range entries {
if entry.IsDir() || !strings.HasSuffix(entry.Name(), ".go") {
continue
}
// Skip generated files and test files
if strings.HasSuffix(entry.Name(), "_gen.go") || strings.HasSuffix(entry.Name(), "_test.go") {
continue
}
path := filepath.Join(dir, entry.Name())
parsed, err := parseFile(fset, path)
if err != nil {
return nil, fmt.Errorf("parsing %s: %w", entry.Name(), err)
}
services = append(services, parsed...)
}
return services, nil
}
// parseFile parses a single Go source file and extracts host services.
func parseFile(fset *token.FileSet, path string) ([]Service, error) {
f, err := parser.ParseFile(fset, path, nil, parser.ParseComments)
if err != nil {
return nil, err
}
// First pass: collect all struct definitions in the file
allStructs := parseStructs(f)
structMap := make(map[string]StructDef)
for _, s := range allStructs {
structMap[s.Name] = s
}
var services []Service
for _, decl := range f.Decls {
genDecl, ok := decl.(*ast.GenDecl)
if !ok || genDecl.Tok != token.TYPE {
continue
}
for _, spec := range genDecl.Specs {
typeSpec, ok := spec.(*ast.TypeSpec)
if !ok {
continue
}
interfaceType, ok := typeSpec.Type.(*ast.InterfaceType)
if !ok {
continue
}
// Check for //nd:hostservice annotation in doc comment
docText, rawDoc := getDocComment(genDecl, typeSpec)
svcAnnotation := parseHostServiceAnnotation(rawDoc)
if svcAnnotation == nil {
continue
}
service := Service{
Name: svcAnnotation["name"],
Permission: svcAnnotation["permission"],
Interface: typeSpec.Name.Name,
Doc: cleanDoc(docText),
}
// Parse methods and collect referenced types
referencedTypes := make(map[string]bool)
for _, method := range interfaceType.Methods.List {
if len(method.Names) == 0 {
continue // Embedded interface
}
funcType, ok := method.Type.(*ast.FuncType)
if !ok {
continue
}
// Check for //nd:hostfunc annotation
methodDocText, methodRawDoc := getMethodDocComment(method)
methodAnnotation := parseHostFuncAnnotation(methodRawDoc)
if methodAnnotation == nil {
continue
}
m, err := parseMethod(method.Names[0].Name, funcType, methodAnnotation, cleanDoc(methodDocText))
if err != nil {
return nil, fmt.Errorf("parsing method %s.%s: %w", typeSpec.Name.Name, method.Names[0].Name, err)
}
service.Methods = append(service.Methods, m)
// Collect referenced types from params and returns
for _, p := range m.Params {
collectReferencedTypes(p.Type, referencedTypes)
}
for _, r := range m.Returns {
collectReferencedTypes(r.Type, referencedTypes)
}
}
// Attach referenced structs to the service
for typeName := range referencedTypes {
if s, exists := structMap[typeName]; exists {
service.Structs = append(service.Structs, s)
}
}
if len(service.Methods) > 0 {
services = append(services, service)
}
}
}
return services, nil
}
// parseStructs extracts all struct type definitions from a parsed Go file.
func parseStructs(f *ast.File) []StructDef {
var structs []StructDef
for _, decl := range f.Decls {
genDecl, ok := decl.(*ast.GenDecl)
if !ok || genDecl.Tok != token.TYPE {
continue
}
for _, spec := range genDecl.Specs {
typeSpec, ok := spec.(*ast.TypeSpec)
if !ok {
continue
}
structType, ok := typeSpec.Type.(*ast.StructType)
if !ok {
continue
}
docText, _ := getDocComment(genDecl, typeSpec)
s := StructDef{
Name: typeSpec.Name.Name,
Doc: cleanDoc(docText),
}
// Parse struct fields
for _, field := range structType.Fields.List {
if len(field.Names) == 0 {
continue // Embedded field
}
fieldDef := parseStructField(field)
s.Fields = append(s.Fields, fieldDef...)
}
structs = append(structs, s)
}
}
return structs
}
// parseStructField parses a struct field and returns FieldDef for each name.
func parseStructField(field *ast.Field) []FieldDef {
var fields []FieldDef
typeName := typeToString(field.Type)
// Parse struct tag for JSON field name and omitempty
jsonTag := ""
omitEmpty := false
if field.Tag != nil {
tag := field.Tag.Value
// Remove backticks
tag = strings.Trim(tag, "`")
// Parse json tag
jsonTag, omitEmpty = parseJSONTag(tag)
}
// Get doc comment
var doc string
if field.Doc != nil {
doc = cleanDoc(field.Doc.Text())
}
for _, name := range field.Names {
fieldJSONTag := jsonTag
if fieldJSONTag == "" {
// Default to field name with camelCase
fieldJSONTag = toJSONName(name.Name)
}
fields = append(fields, FieldDef{
Name: name.Name,
Type: typeName,
JSONTag: fieldJSONTag,
OmitEmpty: omitEmpty,
Doc: doc,
})
}
return fields
}
// parseJSONTag extracts the json field name and omitempty flag from a struct tag.
func parseJSONTag(tag string) (name string, omitEmpty bool) {
// Find json:"..." in the tag
for _, part := range strings.Split(tag, " ") {
if strings.HasPrefix(part, `json:"`) {
value := strings.TrimPrefix(part, `json:"`)
value = strings.TrimSuffix(value, `"`)
parts := strings.Split(value, ",")
if len(parts) > 0 && parts[0] != "-" {
name = parts[0]
}
for _, opt := range parts[1:] {
if opt == "omitempty" {
omitEmpty = true
}
}
return
}
}
return "", false
}
// collectReferencedTypes extracts custom type names from a Go type string.
// It handles pointers, slices, and maps, collecting base type names.
func collectReferencedTypes(goType string, refs map[string]bool) {
// Strip pointer
if strings.HasPrefix(goType, "*") {
collectReferencedTypes(goType[1:], refs)
return
}
// Strip slice
if strings.HasPrefix(goType, "[]") {
if goType != "[]byte" {
collectReferencedTypes(goType[2:], refs)
}
return
}
// Handle map
if strings.HasPrefix(goType, "map[") {
rest := goType[4:] // Remove "map["
depth := 1
keyEnd := 0
for i, r := range rest {
if r == '[' {
depth++
} else if r == ']' {
depth--
if depth == 0 {
keyEnd = i
break
}
}
}
keyType := rest[:keyEnd]
valueType := rest[keyEnd+1:]
collectReferencedTypes(keyType, refs)
collectReferencedTypes(valueType, refs)
return
}
// Check if it's a custom type (starts with uppercase, not a builtin)
if len(goType) > 0 && goType[0] >= 'A' && goType[0] <= 'Z' {
switch goType {
case "String", "Bool", "Int", "Int32", "Int64", "Float32", "Float64":
// Not custom types (just capitalized for some reason)
default:
refs[goType] = true
}
}
}
// toJSONName is imported from types.go via the same package
// getDocComment extracts the doc comment for a type spec.
// Returns both the readable doc text and the raw comment text (which includes pragma-style comments).
func getDocComment(genDecl *ast.GenDecl, typeSpec *ast.TypeSpec) (docText, rawText string) {
var docGroup *ast.CommentGroup
// First check the TypeSpec's own doc (when multiple types in one block)
if typeSpec.Doc != nil {
docGroup = typeSpec.Doc
} else if genDecl.Doc != nil {
// Fall back to GenDecl doc (single type declaration)
docGroup = genDecl.Doc
}
if docGroup == nil {
return "", ""
}
return docGroup.Text(), commentGroupRaw(docGroup)
}
// commentGroupRaw returns all comment text including pragma-style comments (//nd:...).
// Go's ast.CommentGroup.Text() strips comments without a space after //, so we need this.
func commentGroupRaw(cg *ast.CommentGroup) string {
if cg == nil {
return ""
}
var lines []string
for _, c := range cg.List {
lines = append(lines, c.Text)
}
return strings.Join(lines, "\n")
}
// getMethodDocComment extracts the doc comment for a method.
func getMethodDocComment(field *ast.Field) (docText, rawText string) {
if field.Doc == nil {
return "", ""
}
return field.Doc.Text(), commentGroupRaw(field.Doc)
}
// parseHostServiceAnnotation extracts //nd:hostservice annotation parameters.
func parseHostServiceAnnotation(doc string) map[string]string {
for _, line := range strings.Split(doc, "\n") {
line = strings.TrimSpace(line)
match := hostServicePattern.FindStringSubmatch(line)
if match != nil {
return parseKeyValuePairs(match[1])
}
}
return nil
}
// parseHostFuncAnnotation extracts //nd:hostfunc annotation parameters.
func parseHostFuncAnnotation(doc string) map[string]string {
for _, line := range strings.Split(doc, "\n") {
line = strings.TrimSpace(line)
match := hostFuncPattern.FindStringSubmatch(line)
if match != nil {
params := parseKeyValuePairs(match[1])
if params == nil {
params = make(map[string]string)
}
return params
}
}
return nil
}
// parseKeyValuePairs extracts key=value pairs from annotation text.
func parseKeyValuePairs(text string) map[string]string {
matches := keyValuePattern.FindAllStringSubmatch(text, -1)
if len(matches) == 0 {
return nil
}
result := make(map[string]string)
for _, m := range matches {
result[m[1]] = m[2]
}
return result
}
// parseMethod parses a method signature into a Method struct.
func parseMethod(name string, funcType *ast.FuncType, annotation map[string]string, doc string) (Method, error) {
m := Method{
Name: name,
ExportName: annotation["name"],
Doc: doc,
}
// Parse parameters (skip context.Context)
if funcType.Params != nil {
for _, field := range funcType.Params.List {
typeName := typeToString(field.Type)
if typeName == "context.Context" {
continue // Skip context parameter
}
for _, name := range field.Names {
m.Params = append(m.Params, NewParam(name.Name, typeName))
}
}
}
// Parse return values
if funcType.Results != nil {
for _, field := range funcType.Results.List {
typeName := typeToString(field.Type)
if typeName == "error" {
m.HasError = true
continue // Track error but don't include in Returns
}
// Handle anonymous returns
if len(field.Names) == 0 {
// Generate a name based on position
m.Returns = append(m.Returns, NewParam("result", typeName))
} else {
for _, name := range field.Names {
m.Returns = append(m.Returns, NewParam(name.Name, typeName))
}
}
}
}
return m, nil
}
// typeToString converts an AST type expression to a string.
func typeToString(expr ast.Expr) string {
switch t := expr.(type) {
case *ast.Ident:
return t.Name
case *ast.SelectorExpr:
return typeToString(t.X) + "." + t.Sel.Name
case *ast.StarExpr:
return "*" + typeToString(t.X)
case *ast.ArrayType:
if t.Len == nil {
return "[]" + typeToString(t.Elt)
}
return fmt.Sprintf("[%s]%s", typeToString(t.Len), typeToString(t.Elt))
case *ast.MapType:
return fmt.Sprintf("map[%s]%s", typeToString(t.Key), typeToString(t.Value))
case *ast.BasicLit:
return t.Value
case *ast.InterfaceType:
// Empty interface (interface{} or any)
if t.Methods == nil || len(t.Methods.List) == 0 {
return "any"
}
// Non-empty interfaces can't be easily represented
return "any"
default:
return fmt.Sprintf("%T", expr)
}
}
// cleanDoc removes annotation lines from documentation.
func cleanDoc(doc string) string {
var lines []string
for _, line := range strings.Split(doc, "\n") {
trimmed := strings.TrimSpace(line)
if strings.HasPrefix(trimmed, "//nd:") {
continue
}
lines = append(lines, line)
}
return strings.TrimSpace(strings.Join(lines, "\n"))
}

View File

@ -1,292 +0,0 @@
package internal
import (
"os"
"path/filepath"
. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
)
var _ = Describe("Parser", func() {
var tmpDir string
BeforeEach(func() {
var err error
tmpDir, err = os.MkdirTemp("", "hostgen-test-*")
Expect(err).NotTo(HaveOccurred())
})
AfterEach(func() {
os.RemoveAll(tmpDir)
})
Describe("ParseDirectory", func() {
It("should parse a simple host service interface", func() {
src := `package host
import "context"
// SubsonicAPIService provides access to Navidrome's Subsonic API.
//nd:hostservice name=SubsonicAPI permission=subsonicapi
type SubsonicAPIService interface {
// Call executes a Subsonic API request.
//nd:hostfunc
Call(ctx context.Context, uri string) (response string, err error)
}
`
err := os.WriteFile(filepath.Join(tmpDir, "service.go"), []byte(src), 0600)
Expect(err).NotTo(HaveOccurred())
services, err := ParseDirectory(tmpDir)
Expect(err).NotTo(HaveOccurred())
Expect(services).To(HaveLen(1))
svc := services[0]
Expect(svc.Name).To(Equal("SubsonicAPI"))
Expect(svc.Permission).To(Equal("subsonicapi"))
Expect(svc.Interface).To(Equal("SubsonicAPIService"))
Expect(svc.Methods).To(HaveLen(1))
m := svc.Methods[0]
Expect(m.Name).To(Equal("Call"))
Expect(m.HasError).To(BeTrue())
Expect(m.Params).To(HaveLen(1))
Expect(m.Params[0].Name).To(Equal("uri"))
Expect(m.Params[0].Type).To(Equal("string"))
Expect(m.Returns).To(HaveLen(1))
Expect(m.Returns[0].Name).To(Equal("response"))
Expect(m.Returns[0].Type).To(Equal("string"))
})
It("should parse multiple methods", func() {
src := `package host
import "context"
// SchedulerService provides scheduling capabilities.
//nd:hostservice name=Scheduler permission=scheduler
type SchedulerService interface {
//nd:hostfunc
ScheduleRecurring(ctx context.Context, cronExpression string) (scheduleID string, err error)
//nd:hostfunc
ScheduleOneTime(ctx context.Context, delaySeconds int32) (scheduleID string, err error)
//nd:hostfunc
CancelSchedule(ctx context.Context, scheduleID string) (canceled bool, err error)
}
`
err := os.WriteFile(filepath.Join(tmpDir, "scheduler.go"), []byte(src), 0600)
Expect(err).NotTo(HaveOccurred())
services, err := ParseDirectory(tmpDir)
Expect(err).NotTo(HaveOccurred())
Expect(services).To(HaveLen(1))
svc := services[0]
Expect(svc.Name).To(Equal("Scheduler"))
Expect(svc.Methods).To(HaveLen(3))
Expect(svc.Methods[0].Name).To(Equal("ScheduleRecurring"))
Expect(svc.Methods[0].Params[0].Type).To(Equal("string"))
Expect(svc.Methods[1].Name).To(Equal("ScheduleOneTime"))
Expect(svc.Methods[1].Params[0].Type).To(Equal("int32"))
Expect(svc.Methods[2].Name).To(Equal("CancelSchedule"))
Expect(svc.Methods[2].Returns[0].Type).To(Equal("bool"))
})
It("should skip methods without hostfunc annotation", func() {
src := `package host
import "context"
//nd:hostservice name=Test permission=test
type TestService interface {
//nd:hostfunc
Exported(ctx context.Context) error
// This method is not exported
NotExported(ctx context.Context) error
}
`
err := os.WriteFile(filepath.Join(tmpDir, "test.go"), []byte(src), 0600)
Expect(err).NotTo(HaveOccurred())
services, err := ParseDirectory(tmpDir)
Expect(err).NotTo(HaveOccurred())
Expect(services).To(HaveLen(1))
Expect(services[0].Methods).To(HaveLen(1))
Expect(services[0].Methods[0].Name).To(Equal("Exported"))
})
It("should handle custom export name", func() {
src := `package host
import "context"
//nd:hostservice name=Test permission=test
type TestService interface {
//nd:hostfunc name=custom_export_name
MyMethod(ctx context.Context) error
}
`
err := os.WriteFile(filepath.Join(tmpDir, "test.go"), []byte(src), 0600)
Expect(err).NotTo(HaveOccurred())
services, err := ParseDirectory(tmpDir)
Expect(err).NotTo(HaveOccurred())
Expect(services[0].Methods[0].ExportName).To(Equal("custom_export_name"))
Expect(services[0].Methods[0].FunctionName("test")).To(Equal("custom_export_name"))
})
It("should skip generated files", func() {
regularSrc := `package host
import "context"
//nd:hostservice name=Test permission=test
type TestService interface {
//nd:hostfunc
Method(ctx context.Context) error
}
`
genSrc := `// Code generated. DO NOT EDIT.
package host
//nd:hostservice name=Generated permission=gen
type GeneratedService interface {
//nd:hostfunc
Method() error
}
`
err := os.WriteFile(filepath.Join(tmpDir, "test.go"), []byte(regularSrc), 0600)
Expect(err).NotTo(HaveOccurred())
err = os.WriteFile(filepath.Join(tmpDir, "test_gen.go"), []byte(genSrc), 0600)
Expect(err).NotTo(HaveOccurred())
services, err := ParseDirectory(tmpDir)
Expect(err).NotTo(HaveOccurred())
Expect(services).To(HaveLen(1))
Expect(services[0].Name).To(Equal("Test"))
})
It("should skip interfaces without hostservice annotation", func() {
src := `package host
import "context"
// Regular interface without annotation
type RegularInterface interface {
Method(ctx context.Context) error
}
//nd:hostservice name=Annotated permission=annotated
type AnnotatedService interface {
//nd:hostfunc
Method(ctx context.Context) error
}
`
err := os.WriteFile(filepath.Join(tmpDir, "test.go"), []byte(src), 0600)
Expect(err).NotTo(HaveOccurred())
services, err := ParseDirectory(tmpDir)
Expect(err).NotTo(HaveOccurred())
Expect(services).To(HaveLen(1))
Expect(services[0].Name).To(Equal("Annotated"))
})
It("should return empty slice for directory with no host services", func() {
src := `package host
type RegularInterface interface {
Method() error
}
`
err := os.WriteFile(filepath.Join(tmpDir, "test.go"), []byte(src), 0600)
Expect(err).NotTo(HaveOccurred())
services, err := ParseDirectory(tmpDir)
Expect(err).NotTo(HaveOccurred())
Expect(services).To(BeEmpty())
})
})
Describe("parseKeyValuePairs", func() {
It("should parse key=value pairs", func() {
result := parseKeyValuePairs("name=Test permission=test")
Expect(result).To(HaveKeyWithValue("name", "Test"))
Expect(result).To(HaveKeyWithValue("permission", "test"))
})
It("should return nil for empty input", func() {
result := parseKeyValuePairs("")
Expect(result).To(BeNil())
})
})
Describe("typeToString", func() {
It("should handle basic types", func() {
src := `package test
type T interface {
Method(s string, i int, b bool) ([]byte, error)
}
`
err := os.WriteFile(filepath.Join(tmpDir, "types.go"), []byte(src), 0600)
Expect(err).NotTo(HaveOccurred())
// Parse and verify type conversion works
// This is implicitly tested through ParseDirectory
})
It("should convert interface{} to any", func() {
src := `package test
import "context"
//nd:hostservice name=Test permission=test
type TestService interface {
//nd:hostfunc
GetMetadata(ctx context.Context) (data map[string]interface{}, err error)
}
`
err := os.WriteFile(filepath.Join(tmpDir, "test.go"), []byte(src), 0600)
Expect(err).NotTo(HaveOccurred())
services, err := ParseDirectory(tmpDir)
Expect(err).NotTo(HaveOccurred())
Expect(services).To(HaveLen(1))
Expect(services[0].Methods[0].Returns[0].Type).To(Equal("map[string]any"))
})
})
Describe("Method helpers", func() {
It("should generate correct function names", func() {
m := Method{Name: "Call"}
Expect(m.FunctionName("subsonicapi")).To(Equal("subsonicapi_call"))
m.ExportName = "custom_name"
Expect(m.FunctionName("subsonicapi")).To(Equal("custom_name"))
})
It("should generate correct type names", func() {
m := Method{Name: "Call"}
Expect(m.RequestTypeName("SubsonicAPI")).To(Equal("SubsonicAPICallRequest"))
Expect(m.ResponseTypeName("SubsonicAPI")).To(Equal("SubsonicAPICallResponse"))
})
})
Describe("Service helpers", func() {
It("should generate correct output file name", func() {
s := Service{Name: "SubsonicAPI"}
Expect(s.OutputFileName()).To(Equal("subsonicapi_gen.go"))
})
It("should generate correct export prefix", func() {
s := Service{Name: "SubsonicAPI"}
Expect(s.ExportPrefix()).To(Equal("subsonicapi"))
})
})
})

View File

@ -1,108 +0,0 @@
// Code generated by hostgen. DO NOT EDIT.
//
// This file contains client wrappers for the {{.Service.Name}} host service.
// It is intended for use in Navidrome plugins built with TinyGo.
//
//go:build wasip1
package ndhost
import (
"encoding/json"
"errors"
"github.com/extism/go-pdk"
)
{{- /* Generate struct definitions */ -}}
{{- range .Service.Structs}}
// {{.Name}} represents the {{.Name}} data structure.
{{- if .Doc}}
{{formatDoc .Doc}}
{{- end}}
type {{.Name}} struct {
{{- range .Fields}}
{{.Name}} {{.Type}} `json:"{{.JSONTag}}"`
{{- end}}
}
{{- end}}
{{- /* Generate wasmimport declarations for each method */ -}}
{{range .Service.Methods}}
// {{exportName .}} is the host function provided by Navidrome.
//
//go:wasmimport extism:host/user {{exportName .}}
func {{exportName .}}(uint64) uint64
{{- end}}
{{- /* Generate request/response types for all methods */ -}}
{{range .Service.Methods}}
{{- if .HasParams}}
// {{requestType .}} is the request type for {{$.Service.Name}}.{{.Name}}.
type {{requestType .}} struct {
{{- range .Params}}
{{title .Name}} {{.Type}} `json:"{{.JSONName}}"`
{{- end}}
}
{{- end}}
// {{responseType .}} is the response type for {{$.Service.Name}}.{{.Name}}.
type {{responseType .}} struct {
{{- range .Returns}}
{{title .Name}} {{.Type}} `json:"{{.JSONName}},omitempty"`
{{- end}}
Error string `json:"error,omitempty"`
}
{{- end}}
{{- /* Generate wrapper functions */ -}}
{{range .Service.Methods}}
// {{$.Service.Name}}{{.Name}} calls the {{exportName .}} host function.
{{- if .Doc}}
{{formatDoc .Doc}}
{{- end}}
func {{$.Service.Name}}{{.Name}}({{range $i, $p := .Params}}{{if $i}}, {{end}}{{$p.Name}} {{$p.Type}}{{end}}) (*{{responseType .}}, error) {
{{- if .HasParams}}
// Marshal request to JSON
req := {{requestType .}}{
{{- range .Params}}
{{title .Name}}: {{.Name}},
{{- end}}
}
reqBytes, err := json.Marshal(req)
if err != nil {
return nil, err
}
reqMem := pdk.AllocateBytes(reqBytes)
defer reqMem.Free()
{{- else}}
// No parameters - allocate empty JSON object
reqMem := pdk.AllocateBytes([]byte("{}"))
defer reqMem.Free()
{{- end}}
// Call the host function
responsePtr := {{exportName .}}(reqMem.Offset())
// Read the response from memory
responseMem := pdk.FindMemory(responsePtr)
responseBytes := responseMem.ReadBytes()
// Parse the response
var response {{responseType .}}
if err := json.Unmarshal(responseBytes, &response); err != nil {
return nil, err
}
// Convert Error field to Go error
if response.Error != "" {
return nil, errors.New(response.Error)
}
return &response, nil
}
{{- end}}

View File

@ -1,95 +0,0 @@
# Code generated by hostgen. DO NOT EDIT.
#
# This file contains client wrappers for the {{.Service.Name}} host service.
# It is intended for use in Navidrome plugins built with extism-py.
#
# IMPORTANT: Due to a limitation in extism-py, you cannot import this file directly.
# The @extism.import_fn decorators are only detected when defined in the plugin's
# main __init__.py file. Copy the needed functions from this file into your plugin.
from dataclasses import dataclass
from typing import Any
import extism
import json
class HostFunctionError(Exception):
"""Raised when a host function returns an error."""
pass
{{- /* Generate raw host function imports */ -}}
{{range .Service.Methods}}
@extism.import_fn("extism:host/user", "{{exportName .}}")
def _{{exportName .}}(offset: int) -> int:
"""Raw host function - do not call directly."""
...
{{- end}}
{{- /* Generate dataclasses for multi-value returns */ -}}
{{range .Service.Methods}}
{{- if .NeedsResultClass}}
@dataclass
class {{pythonResultType .}}:
"""Result type for {{pythonFunc .}}."""
{{- range .Returns}}
{{.PythonName}}: {{.PythonType}}
{{- end}}
{{- end}}
{{- end}}
{{- /* Generate wrapper functions */ -}}
{{range .Service.Methods}}
def {{pythonFunc .}}({{range $i, $p := .Params}}{{if $i}}, {{end}}{{$p.PythonName}}: {{$p.PythonType}}{{end}}){{if .NeedsResultClass}} -> {{pythonResultType .}}{{else if .HasReturns}} -> {{(index .Returns 0).PythonType}}{{else}} -> None{{end}}:
"""{{if .Doc}}{{.Doc}}{{else}}Call the {{exportName .}} host function.{{end}}
{{- if .HasParams}}
Args:
{{- range .Params}}
{{.PythonName}}: {{.PythonType}} parameter.
{{- end}}
{{- end}}
{{- if .HasReturns}}
Returns:
{{- if .NeedsResultClass}}
{{pythonResultType .}} containing{{range .Returns}} {{.PythonName}},{{end}}.
{{- else}}
{{(index .Returns 0).PythonType}}: The result value.
{{- end}}
{{- end}}
Raises:
HostFunctionError: If the host function returns an error.
"""
{{- if .HasParams}}
request = {
{{- range .Params}}
"{{.JSONName}}": {{.PythonName}},
{{- end}}
}
request_bytes = json.dumps(request).encode("utf-8")
{{- else}}
request_bytes = b"{}"
{{- end}}
request_mem = extism.memory.alloc(request_bytes)
response_offset = _{{exportName .}}(request_mem.offset)
response_mem = extism.memory.find(response_offset)
response = json.loads(extism.memory.string(response_mem))
if response.get("error"):
raise HostFunctionError(response["error"])
{{if .NeedsResultClass}}
return {{pythonResultType .}}(
{{- range .Returns}}
{{.PythonName}}=response.get("{{.JSONName}}"{{pythonDefault .}}),
{{- end}}
)
{{- else if .HasReturns}}
return response.get("{{(index .Returns 0).JSONName}}"{{pythonDefault (index .Returns 0)}})
{{- end}}
{{- end}}

View File

@ -1,103 +0,0 @@
// Code generated by hostgen. DO NOT EDIT.
//
// This file contains client wrappers for the {{.Service.Name}} host service.
// It is intended for use in Navidrome plugins built with extism-pdk.
use extism_pdk::*;
use serde::{Deserialize, Serialize};
{{- /* Generate struct definitions */ -}}
{{- range .Service.Structs}}
{{if .Doc}}
{{rustDocComment .Doc}}
{{else}}
{{end}}#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct {{.Name}} {
{{- range .Fields}}
{{- if .NeedsDefault}}
#[serde(default)]
{{- end}}
pub {{.RustName}}: {{fieldRustType .}},
{{- end}}
}
{{- end}}
{{- /* Generate request/response types */ -}}
{{- range .Service.Methods}}
{{- if .HasParams}}
#[derive(Debug, Clone, Serialize)]
#[serde(rename_all = "camelCase")]
struct {{requestType .}} {
{{- range .Params}}
{{.RustName}}: {{rustType .}},
{{- end}}
}
{{- end}}
#[derive(Debug, Clone, Deserialize)]
#[serde(rename_all = "camelCase")]
struct {{responseType .}} {
{{- range .Returns}}
#[serde(default)]
{{.RustName}}: {{rustType .}},
{{- end}}
#[serde(default)]
error: Option<String>,
}
{{- end}}
#[host_fn]
extern "ExtismHost" {
{{- range .Service.Methods}}
fn {{exportName .}}(input: Json<{{if .HasParams}}{{requestType .}}{{else}}serde_json::Value{{end}}>) -> Json<{{responseType .}}>;
{{- end}}
}
{{- /* Generate wrapper functions */ -}}
{{range .Service.Methods}}
{{if .Doc}}{{rustDocComment .Doc}}{{else}}/// Calls the {{exportName .}} host function.{{end}}
{{- if .HasParams}}
///
/// # Arguments
{{- range .Params}}
/// * `{{.RustName}}` - {{rustType .}} parameter.
{{- end}}
{{- end}}
{{- if .HasReturns}}
///
/// # Returns
{{- if eq (len .Returns) 1}}
/// The {{(index .Returns 0).RustName}} value.
{{- else}}
/// A tuple of ({{range $i, $r := .Returns}}{{if $i}}, {{end}}{{$r.RustName}}{{end}}).
{{- end}}
{{- end}}
///
/// # Errors
/// Returns an error if the host function call fails.
pub fn {{rustFunc .}}({{range $i, $p := .Params}}{{if $i}}, {{end}}{{$p.RustName}}: {{rustParamType $p}}{{end}}) -> Result<{{if eq (len .Returns) 0}}(){{else if eq (len .Returns) 1}}{{rustType (index .Returns 0)}}{{else}}({{range $i, $r := .Returns}}{{if $i}}, {{end}}{{rustType $r}}{{end}}){{end}}, Error> {
let response = unsafe {
{{- if .HasParams}}
{{exportName .}}(Json({{requestType .}} {
{{- range .Params}}
{{.RustName}}: {{.RustName}}{{if .NeedsToOwned}}.to_owned(){{end}},
{{- end}}
}))?
{{- else}}
{{exportName .}}(Json(serde_json::json!({})))?
{{- end}}
};
if let Some(err) = response.0.error {
return Err(Error::msg(err));
}
{{if eq (len .Returns) 0}}
Ok(())
{{- else if eq (len .Returns) 1}}
Ok(response.0.{{(index .Returns 0).RustName}})
{{- else}}
Ok(({{range $i, $r := .Returns}}{{if $i}}, {{end}}response.0.{{$r.RustName}}{{end}}))
{{- end}}
}
{{- end}}

View File

@ -1,56 +0,0 @@
// Code generated by hostgen. DO NOT EDIT.
//
// This file contains stub implementations for non-WASM builds.
// These stubs allow IDE support and compilation on non-WASM platforms.
// They panic at runtime since host functions are only available in WASM plugins.
//
//go:build !wasip1
package ndhost
{{- /* Generate struct definitions (same as main file, needed for type references) */ -}}
{{- range .Service.Structs}}
// {{.Name}} represents the {{.Name}} data structure.
{{- if .Doc}}
{{formatDoc .Doc}}
{{- end}}
type {{.Name}} struct {
{{- range .Fields}}
{{.Name}} {{.Type}} `json:"{{.JSONTag}}"`
{{- end}}
}
{{- end}}
{{- /* Generate request/response types (same as main file) */ -}}
{{range .Service.Methods}}
{{- if .HasParams}}
// {{requestType .}} is the request type for {{$.Service.Name}}.{{.Name}}.
type {{requestType .}} struct {
{{- range .Params}}
{{title .Name}} {{.Type}} `json:"{{.JSONName}}"`
{{- end}}
}
{{- end}}
// {{responseType .}} is the response type for {{$.Service.Name}}.{{.Name}}.
type {{responseType .}} struct {
{{- range .Returns}}
{{title .Name}} {{.Type}} `json:"{{.JSONName}},omitempty"`
{{- end}}
Error string `json:"error,omitempty"`
}
{{- end}}
{{- /* Generate stub wrapper functions that panic */ -}}
{{range .Service.Methods}}
// {{$.Service.Name}}{{.Name}} is a stub that panics on non-WASM platforms.
{{- if .Doc}}
{{formatDoc .Doc}}
{{- end}}
func {{$.Service.Name}}{{.Name}}({{range $i, $p := .Params}}{{if $i}}, {{end}}{{$p.Name}} {{$p.Type}}{{end}}) (*{{responseType .}}, error) {
panic("ndhost: {{$.Service.Name}}{{.Name}} is only available in WASM plugins")
}
{{- end}}

View File

@ -1,49 +0,0 @@
// Code generated by hostgen. DO NOT EDIT.
/*
Package ndhost provides Navidrome host function wrappers for Go/TinyGo plugins.
This package is auto-generated by the hostgen tool and should not be edited manually.
# Usage
Add this module as a dependency in your plugin's go.mod:
require github.com/navidrome/navidrome/plugins/host/go v0.0.0
Then import the package in your plugin code:
import ndhost "github.com/navidrome/navidrome/plugins/host/go"
func myPluginFunction() error {
// Use the cache service
_, err := ndhost.CacheSetString("my_key", "my_value", 3600)
if err != nil {
return err
}
// Schedule a recurring task
_, err = ndhost.SchedulerScheduleRecurring("@every 5m", "payload", "task_id")
if err != nil {
return err
}
return nil
}
# Available Services
The following host services are available:
{{range .Services}}
- {{.Name}}: {{if .Doc}}{{.Doc | firstLine}}{{else}}{{.Name}} service{{end}}
{{- end}}
# Building Plugins
Go plugins must be compiled to WebAssembly using TinyGo:
tinygo build -o plugin.wasm -target=wasip1 -buildmode=c-shared .
See the examples directory for complete plugin implementations.
*/
package ndhost

View File

@ -1,5 +0,0 @@
module github.com/navidrome/navidrome/plugins/host/go
go 1.24
require github.com/extism/go-pdk v1.1.3

View File

@ -1,119 +0,0 @@
// Code generated by hostgen. DO NOT EDIT.
package {{.Package}}
import (
"context"
"encoding/json"
extism "github.com/extism/go-sdk"
)
{{- /* Generate request/response types for all methods */ -}}
{{range .Service.Methods}}
{{- if .HasParams}}
// {{requestType .}} is the request type for {{$.Service.Name}}.{{.Name}}.
type {{requestType .}} struct {
{{- range .Params}}
{{title .Name}} {{.Type}} `json:"{{.JSONName}}"`
{{- end}}
}
{{- end}}
// {{responseType .}} is the response type for {{$.Service.Name}}.{{.Name}}.
type {{responseType .}} struct {
{{- range .Returns}}
{{title .Name}} {{.Type}} `json:"{{.JSONName}},omitempty"`
{{- end}}
Error string `json:"error,omitempty"`
}
{{end}}
// Register{{.Service.Name}}HostFunctions registers {{.Service.Name}} service host functions.
// The returned host functions should be added to the plugin's configuration.
func Register{{.Service.Name}}HostFunctions(service {{.Service.Interface}}) []extism.HostFunction {
return []extism.HostFunction{
{{- range .Service.Methods}}
new{{$.Service.Name}}{{.Name}}HostFunction(service),
{{- end}}
}
}
{{range .Service.Methods}}
func new{{$.Service.Name}}{{.Name}}HostFunction(service {{$.Service.Interface}}) extism.HostFunction {
return extism.NewHostFunctionWithStack(
"{{exportName .}}",
func(ctx context.Context, p *extism.CurrentPlugin, stack []uint64) {
{{- if .HasParams}}
// Read JSON request from plugin memory
reqBytes, err := p.ReadBytes(stack[0])
if err != nil {
{{$.Service.Name | lower}}WriteError(p, stack, err)
return
}
var req {{requestType .}}
if err := json.Unmarshal(reqBytes, &req); err != nil {
{{$.Service.Name | lower}}WriteError(p, stack, err)
return
}
{{- end}}
// Call the service method
{{- if .HasReturns}}
{{- if .HasError}}
{{range $i, $r := .Returns}}{{if $i}}, {{end}}{{lower $r.Name}}{{end}}, svcErr := service.{{.Name}}(ctx{{range .Params}}, req.{{title .Name}}{{end}})
if svcErr != nil {
{{$.Service.Name | lower}}WriteError(p, stack, svcErr)
return
}
{{- else}}
{{range $i, $r := .Returns}}{{if $i}}, {{end}}{{lower $r.Name}}{{end}} := service.{{.Name}}(ctx{{range .Params}}, req.{{title .Name}}{{end}})
{{- end}}
{{- else if .HasError}}
if svcErr := service.{{.Name}}(ctx{{range .Params}}, req.{{title .Name}}{{end}}); svcErr != nil {
{{$.Service.Name | lower}}WriteError(p, stack, svcErr)
return
}
{{- else}}
service.{{.Name}}(ctx{{range .Params}}, req.{{title .Name}}{{end}})
{{- end}}
// Write JSON response to plugin memory
resp := {{responseType .}}{
{{- range .Returns}}
{{title .Name}}: {{lower .Name}},
{{- end}}
}
{{$.Service.Name | lower}}WriteResponse(p, stack, resp)
},
[]extism.ValueType{extism.ValueTypePTR},
[]extism.ValueType{extism.ValueTypePTR},
)
}
{{end}}
// {{.Service.Name | lower}}WriteResponse writes a JSON response to plugin memory.
func {{.Service.Name | lower}}WriteResponse(p *extism.CurrentPlugin, stack []uint64, resp any) {
respBytes, err := json.Marshal(resp)
if err != nil {
{{.Service.Name | lower}}WriteError(p, stack, err)
return
}
respPtr, err := p.WriteBytes(respBytes)
if err != nil {
stack[0] = 0
return
}
stack[0] = respPtr
}
// {{.Service.Name | lower}}WriteError writes an error response to plugin memory.
func {{.Service.Name | lower}}WriteError(p *extism.CurrentPlugin, stack []uint64, err error) {
errResp := struct {
Error string `json:"error"`
}{Error: err.Error()}
respBytes, _ := json.Marshal(errResp)
respPtr, _ := p.WriteBytes(respBytes)
stack[0] = respPtr
}

View File

@ -1,43 +0,0 @@
// Code generated by hostgen. DO NOT EDIT.
//
//! Navidrome Host Function Wrappers for Rust Plugins
//!
//! This crate provides idiomatic Rust wrappers for all Navidrome host services.
//! It is auto-generated by the hostgen tool and should not be edited manually.
//!
//! # Usage
//!
//! Add this crate as a dependency in your plugin's Cargo.toml:
//!
//! ```toml
//! [dependencies]
//! nd-host = { path = "../../host/rust" }
//! ```
//!
//! Then import the services you need:
//!
//! ```ignore
//! use nd_host::{cache, scheduler};
//!
//! fn my_plugin_function() -> Result<(), extism_pdk::Error> {
//! // Use the cache service
//! cache::set_string("my_key", "my_value", 3600)?;
//!
//! // Schedule a recurring task
//! scheduler::schedule_recurring("@every 5m", "payload", "task_id")?;
//!
//! Ok(())
//! }
//! ```
//!
//! # Available Services
//!
{{- range .Services}}
//! - [`{{.Name | lower}}`] - {{if .Doc}}{{.Doc | firstLine}}{{else}}{{.Name}} service{{end}}
{{- end}}
{{range .Services}}
#[path = "nd_host_{{.Name | lower}}.rs"]
pub mod {{.Name | lower}};
{{end}}
// Re-export commonly used types from extism-pdk for convenience
pub use extism_pdk::Error;

View File

@ -1,384 +0,0 @@
package internal
import (
"strings"
"unicode"
)
// Service represents a parsed host service interface.
type Service struct {
Name string // Service name from annotation (e.g., "SubsonicAPI")
Permission string // Manifest permission key (e.g., "subsonicapi")
Interface string // Go interface name (e.g., "SubsonicAPIService")
Methods []Method // Methods marked with //nd:hostfunc
Doc string // Documentation comment for the service
Structs []StructDef // Structs used by this service
}
// StructDef represents a Go struct type definition.
type StructDef struct {
Name string // Go struct name (e.g., "Library")
Fields []FieldDef // Struct fields
Doc string // Documentation comment
}
// FieldDef represents a field within a struct.
type FieldDef struct {
Name string // Go field name (e.g., "TotalSongs")
Type string // Go type (e.g., "int32", "*string", "[]User")
JSONTag string // JSON tag value (e.g., "totalSongs,omitempty")
OmitEmpty bool // Whether the field has omitempty tag
Doc string // Field documentation
}
// OutputFileName returns the generated file name for this service.
func (s Service) OutputFileName() string {
return strings.ToLower(s.Name) + "_gen.go"
}
// ExportPrefix returns the prefix for exported host function names.
func (s Service) ExportPrefix() string {
return strings.ToLower(s.Name)
}
// KnownStructs returns a map of struct names defined in this service.
func (s Service) KnownStructs() map[string]bool {
result := make(map[string]bool)
for _, st := range s.Structs {
result[st.Name] = true
}
return result
}
// Method represents a host function method within a service.
type Method struct {
Name string // Go method name (e.g., "Call")
ExportName string // Optional override for export name
Params []Param // Method parameters (excluding context.Context)
Returns []Param // Return values (excluding error)
HasError bool // Whether the method returns an error
Doc string // Documentation comment for the method
}
// FunctionName returns the Extism host function export name.
func (m Method) FunctionName(servicePrefix string) string {
if m.ExportName != "" {
return m.ExportName
}
return servicePrefix + "_" + strings.ToLower(m.Name)
}
// RequestTypeName returns the generated request type name.
func (m Method) RequestTypeName(serviceName string) string {
return serviceName + m.Name + "Request"
}
// ResponseTypeName returns the generated response type name.
func (m Method) ResponseTypeName(serviceName string) string {
return serviceName + m.Name + "Response"
}
// HasParams returns true if the method has input parameters.
func (m Method) HasParams() bool {
return len(m.Params) > 0
}
// HasReturns returns true if the method has return values (excluding error).
func (m Method) HasReturns() bool {
return len(m.Returns) > 0
}
// Param represents a method parameter or return value.
type Param struct {
Name string // Parameter name
Type string // Go type (e.g., "string", "int32", "[]byte")
JSONName string // JSON field name (camelCase)
}
// NewParam creates a Param with auto-generated JSON name.
func NewParam(name, typ string) Param {
return Param{
Name: name,
Type: typ,
JSONName: toJSONName(name),
}
}
// toJSONName converts a Go identifier to camelCase JSON field name.
// This matches Rust serde's rename_all = "camelCase" behavior.
// Examples: "ConnectionID" -> "connectionId", "NewConnectionID" -> "newConnectionId"
func toJSONName(name string) string {
if name == "" {
return ""
}
runes := []rune(name)
result := make([]rune, 0, len(runes))
for i, r := range runes {
if i == 0 {
// First character is always lowercase
result = append(result, unicode.ToLower(r))
} else if unicode.IsUpper(r) {
// Check if this is part of an acronym (consecutive uppercase)
// or a word boundary
prevIsUpper := unicode.IsUpper(runes[i-1])
nextIsLower := i+1 < len(runes) && unicode.IsLower(runes[i+1])
if prevIsUpper && !nextIsLower {
// Middle of an acronym - lowercase it
result = append(result, unicode.ToLower(r))
} else if prevIsUpper && nextIsLower {
// End of acronym followed by lowercase - this starts a new word
// Keep uppercase
result = append(result, r)
} else {
// Regular word boundary - keep uppercase
result = append(result, r)
}
} else {
result = append(result, r)
}
}
return string(result)
}
// ToPythonType converts a Go type to its Python equivalent.
func ToPythonType(goType string) string {
switch goType {
case "string":
return "str"
case "int", "int32", "int64":
return "int"
case "float32", "float64":
return "float"
case "bool":
return "bool"
case "[]byte":
return "bytes"
default:
return "Any"
}
}
// ToSnakeCase converts a PascalCase or camelCase string to snake_case.
// It handles consecutive uppercase letters correctly (e.g., "ScheduleID" -> "schedule_id").
func ToSnakeCase(s string) string {
var result strings.Builder
runes := []rune(s)
for i, r := range runes {
if i > 0 && r >= 'A' && r <= 'Z' {
// Add underscore before uppercase, but not if:
// - Previous char was uppercase AND next char is uppercase or end of string
// (this handles acronyms like "ID" in "NewScheduleID")
prevUpper := runes[i-1] >= 'A' && runes[i-1] <= 'Z'
nextUpper := i+1 < len(runes) && runes[i+1] >= 'A' && runes[i+1] <= 'Z'
atEnd := i+1 == len(runes)
// Only skip underscore if we're in the middle of an acronym
if !prevUpper || (!nextUpper && !atEnd) {
result.WriteByte('_')
}
}
result.WriteRune(r)
}
return strings.ToLower(result.String())
}
// PythonFunctionName returns the Python function name for a method.
func (m Method) PythonFunctionName(servicePrefix string) string {
return ToSnakeCase(servicePrefix + m.Name)
}
// PythonResultTypeName returns the Python dataclass name for multi-value returns.
func (m Method) PythonResultTypeName(serviceName string) string {
return serviceName + m.Name + "Result"
}
// NeedsResultClass returns true if the method needs a dataclass for returns.
func (m Method) NeedsResultClass() bool {
return len(m.Returns) > 1
}
// PythonType returns the Python type for this parameter.
func (p Param) PythonType() string {
return ToPythonType(p.Type)
}
// PythonName returns the snake_case Python name for this parameter.
func (p Param) PythonName() string {
return ToSnakeCase(p.Name)
}
// ToRustType converts a Go type to its Rust equivalent.
func ToRustType(goType string) string {
return ToRustTypeWithStructs(goType, nil)
}
// RustParamType returns the Rust type for a function parameter (uses &str for strings).
func RustParamType(goType string) string {
if goType == "string" {
return "&str"
}
return ToRustType(goType)
}
// RustDefaultValue returns the default value for a Rust type.
func RustDefaultValue(goType string) string {
switch goType {
case "string":
return `String::new()`
case "int", "int32":
return "0"
case "int64":
return "0"
case "float32", "float64":
return "0.0"
case "bool":
return "false"
default:
if strings.HasPrefix(goType, "[]") {
return "Vec::new()"
}
if strings.HasPrefix(goType, "map[") {
return "std::collections::HashMap::new()"
}
if strings.HasPrefix(goType, "*") {
return "None"
}
return "serde_json::Value::Null"
}
}
// RustFunctionName returns the Rust function name for a method (snake_case).
// Uses just the method name without service prefix since the module provides namespacing.
func (m Method) RustFunctionName(_ string) string {
return ToSnakeCase(m.Name)
}
// RustDocComment returns a properly formatted Rust doc comment.
// Each line of the input doc string is prefixed with "/// ".
func RustDocComment(doc string) string {
if doc == "" {
return ""
}
lines := strings.Split(doc, "\n")
var result []string
for _, line := range lines {
result = append(result, "/// "+line)
}
return strings.Join(result, "\n")
}
// RustType returns the Rust type for this parameter.
func (p Param) RustType() string {
return ToRustType(p.Type)
}
// RustTypeWithStructs returns the Rust type using known struct names.
func (p Param) RustTypeWithStructs(knownStructs map[string]bool) string {
return ToRustTypeWithStructs(p.Type, knownStructs)
}
// RustParamType returns the Rust type for this parameter when used as a function argument.
func (p Param) RustParamType() string {
return RustParamType(p.Type)
}
// RustParamTypeWithStructs returns the Rust param type using known struct names.
func (p Param) RustParamTypeWithStructs(knownStructs map[string]bool) string {
if p.Type == "string" {
return "&str"
}
return ToRustTypeWithStructs(p.Type, knownStructs)
}
// RustName returns the snake_case Rust name for this parameter.
func (p Param) RustName() string {
return ToSnakeCase(p.Name)
}
// NeedsToOwned returns true if the parameter needs .to_owned() when used.
func (p Param) NeedsToOwned() bool {
return p.Type == "string"
}
// RustType returns the Rust type for this field, using known struct names.
func (f FieldDef) RustType(knownStructs map[string]bool) string {
return ToRustTypeWithStructs(f.Type, knownStructs)
}
// RustName returns the snake_case Rust name for this field.
func (f FieldDef) RustName() string {
return ToSnakeCase(f.Name)
}
// NeedsDefault returns true if the field needs #[serde(default)] attribute.
// This is true for fields with omitempty tag.
func (f FieldDef) NeedsDefault() bool {
return f.OmitEmpty
}
// ToRustTypeWithStructs converts a Go type to its Rust equivalent,
// using known struct names instead of serde_json::Value.
func ToRustTypeWithStructs(goType string, knownStructs map[string]bool) string {
// Handle pointer types
if strings.HasPrefix(goType, "*") {
inner := ToRustTypeWithStructs(goType[1:], knownStructs)
return "Option<" + inner + ">"
}
// Handle slice types
if strings.HasPrefix(goType, "[]") {
if goType == "[]byte" {
return "Vec<u8>"
}
inner := ToRustTypeWithStructs(goType[2:], knownStructs)
return "Vec<" + inner + ">"
}
// Handle map types
if strings.HasPrefix(goType, "map[") {
// Extract key and value types from map[K]V
rest := goType[4:] // Remove "map["
depth := 1
keyEnd := 0
for i, r := range rest {
if r == '[' {
depth++
} else if r == ']' {
depth--
if depth == 0 {
keyEnd = i
break
}
}
}
keyType := rest[:keyEnd]
valueType := rest[keyEnd+1:]
return "std::collections::HashMap<" + ToRustTypeWithStructs(keyType, knownStructs) + ", " + ToRustTypeWithStructs(valueType, knownStructs) + ">"
}
switch goType {
case "string":
return "String"
case "int", "int32":
return "i32"
case "int64":
return "i64"
case "float32":
return "f32"
case "float64":
return "f64"
case "bool":
return "bool"
case "interface{}", "any":
return "serde_json::Value"
default:
// Check if this is a known struct type
if knownStructs != nil && knownStructs[goType] {
return goType
}
// For unknown custom types, fall back to Value
return "serde_json::Value"
}
}

View File

@ -1,455 +0,0 @@
// hostgen generates Extism host function wrappers from annotated Go interfaces.
//
// Usage:
//
// hostgen -input=./plugins/host -output=./plugins/host
//
// Flags:
//
// -input Input directory containing Go source files with annotated interfaces
// -output Output directory for generated files (default: same as input)
// -package Output package name (default: inferred from output directory)
// -host-only Generate only host-side code (default: false)
// -plugin-only Generate only plugin/client-side code (default: false)
// -go Generate Go client wrappers (default: true when not using -python/-rust)
// -python Generate Python client wrappers (default: false)
// -rust Generate Rust client wrappers (default: false)
// -v Verbose output
// -dry-run Preview generated code without writing files
package main
import (
"flag"
"fmt"
"go/format"
"os"
"path/filepath"
"strings"
"github.com/navidrome/navidrome/plugins/cmd/hostgen/internal"
)
// config holds the parsed command-line configuration.
type config struct {
inputDir string
outputDir string
pkgName string
generateHost bool
generateGoClient bool
generatePyClient bool
generateRsClient bool
verbose bool
dryRun bool
}
func main() {
cfg, err := parseConfig()
if err != nil {
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
os.Exit(1)
}
services, err := parseServices(cfg)
if err != nil {
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
os.Exit(1)
}
if len(services) == 0 {
return
}
if err := generateAllCode(cfg, services); err != nil {
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
os.Exit(1)
}
}
// parseConfig parses command-line flags and returns the configuration.
func parseConfig() (*config, error) {
var (
inputDir = flag.String("input", ".", "Input directory containing Go source files")
outputDir = flag.String("output", "", "Output directory for generated files (default: same as input)")
pkgName = flag.String("package", "", "Output package name (default: inferred from output directory)")
hostOnly = flag.Bool("host-only", false, "Generate only host-side code")
pluginOnly = flag.Bool("plugin-only", false, "Generate only plugin/client-side code")
goClient = flag.Bool("go", false, "Generate Go client wrappers")
pyClient = flag.Bool("python", false, "Generate Python client wrappers")
rsClient = flag.Bool("rust", false, "Generate Rust client wrappers")
verbose = flag.Bool("v", false, "Verbose output")
dryRun = flag.Bool("dry-run", false, "Preview generated code without writing files")
)
flag.Parse()
if *hostOnly && *pluginOnly {
return nil, fmt.Errorf("-host-only and -plugin-only cannot be used together")
}
if *outputDir == "" {
*outputDir = *inputDir
}
absInput, err := filepath.Abs(*inputDir)
if err != nil {
return nil, fmt.Errorf("resolving input path: %w", err)
}
absOutput, err := filepath.Abs(*outputDir)
if err != nil {
return nil, fmt.Errorf("resolving output path: %w", err)
}
if *pkgName == "" {
*pkgName = filepath.Base(absOutput)
}
// Determine what to generate
// Default: generate Go clients if no language flag is specified
anyLangFlag := *goClient || *pyClient || *rsClient
return &config{
inputDir: absInput,
outputDir: absOutput,
pkgName: *pkgName,
generateHost: !*pluginOnly,
generateGoClient: !*hostOnly && (*goClient || !anyLangFlag),
generatePyClient: !*hostOnly && *pyClient,
generateRsClient: !*hostOnly && *rsClient,
verbose: *verbose,
dryRun: *dryRun,
}, nil
}
// parseServices parses source files and returns discovered services.
func parseServices(cfg *config) ([]internal.Service, error) {
if cfg.verbose {
fmt.Printf("Input directory: %s\n", cfg.inputDir)
fmt.Printf("Output directory: %s\n", cfg.outputDir)
fmt.Printf("Package name: %s\n", cfg.pkgName)
fmt.Printf("Generate host code: %v\n", cfg.generateHost)
fmt.Printf("Generate Go client code: %v\n", cfg.generateGoClient)
fmt.Printf("Generate Python client code: %v\n", cfg.generatePyClient)
fmt.Printf("Generate Rust client code: %v\n", cfg.generateRsClient)
}
services, err := internal.ParseDirectory(cfg.inputDir)
if err != nil {
return nil, fmt.Errorf("parsing source files: %w", err)
}
if len(services) == 0 {
if cfg.verbose {
fmt.Println("No host services found")
}
return nil, nil
}
if cfg.verbose {
fmt.Printf("Found %d host service(s)\n", len(services))
for _, svc := range services {
fmt.Printf(" - %s (%d methods)\n", svc.Name, len(svc.Methods))
}
}
return services, nil
}
// generateAllCode generates all requested code for the services.
func generateAllCode(cfg *config, services []internal.Service) error {
for _, svc := range services {
if cfg.generateHost {
if err := generateHostCode(svc, cfg.pkgName, cfg.outputDir, cfg.dryRun, cfg.verbose); err != nil {
return fmt.Errorf("generating host code for %s: %w", svc.Name, err)
}
}
if cfg.generateGoClient {
if err := generateGoClientCode(svc, cfg.outputDir, cfg.dryRun, cfg.verbose); err != nil {
return fmt.Errorf("generating Go client code for %s: %w", svc.Name, err)
}
}
if cfg.generatePyClient {
if err := generatePythonClientCode(svc, cfg.outputDir, cfg.dryRun, cfg.verbose); err != nil {
return fmt.Errorf("generating Python client code for %s: %w", svc.Name, err)
}
}
if cfg.generateRsClient {
if err := generateRustClientCode(svc, cfg.outputDir, cfg.dryRun, cfg.verbose); err != nil {
return fmt.Errorf("generating Rust client code for %s: %w", svc.Name, err)
}
}
}
if cfg.generateRsClient && len(services) > 0 {
if err := generateRustLibFile(services, cfg.outputDir, cfg.dryRun, cfg.verbose); err != nil {
return fmt.Errorf("generating Rust lib.rs: %w", err)
}
}
if cfg.generateGoClient && len(services) > 0 {
if err := generateGoDocFile(services, cfg.outputDir, cfg.dryRun, cfg.verbose); err != nil {
return fmt.Errorf("generating Go doc.go: %w", err)
}
if err := generateGoModFile(cfg.outputDir, cfg.dryRun, cfg.verbose); err != nil {
return fmt.Errorf("generating Go go.mod: %w", err)
}
}
return nil
}
// generateHostCode generates host-side code for a service.
func generateHostCode(svc internal.Service, pkgName, outputDir string, dryRun, verbose bool) error {
code, err := internal.GenerateHost(svc, pkgName)
if err != nil {
return fmt.Errorf("generating code: %w", err)
}
formatted, err := format.Source(code)
if err != nil {
return fmt.Errorf("formatting code: %w\nRaw code:\n%s", err, code)
}
outputFile := filepath.Join(outputDir, svc.OutputFileName())
if dryRun {
fmt.Printf("=== %s ===\n%s\n", outputFile, formatted)
return nil
}
if err := os.WriteFile(outputFile, formatted, 0600); err != nil {
return fmt.Errorf("writing file: %w", err)
}
if verbose {
fmt.Printf("Generated host code: %s\n", outputFile)
}
return nil
}
// generateGoClientCode generates Go client-side code for a service.
func generateGoClientCode(svc internal.Service, outputDir string, dryRun, verbose bool) error {
code, err := internal.GenerateClientGo(svc)
if err != nil {
return fmt.Errorf("generating code: %w", err)
}
formatted, err := format.Source(code)
if err != nil {
return fmt.Errorf("formatting code: %w\nRaw code:\n%s", err, code)
}
// Client code goes in go/ subdirectory
clientDir := filepath.Join(outputDir, "go")
clientFile := filepath.Join(clientDir, "nd_host_"+strings.ToLower(svc.Name)+".go")
if dryRun {
fmt.Printf("=== %s ===\n%s\n", clientFile, formatted)
} else {
// Create go/ subdirectory if needed
if err := os.MkdirAll(clientDir, 0755); err != nil {
return fmt.Errorf("creating client directory: %w", err)
}
if err := os.WriteFile(clientFile, formatted, 0600); err != nil {
return fmt.Errorf("writing file: %w", err)
}
if verbose {
fmt.Printf("Generated Go client code: %s\n", clientFile)
}
}
// Also generate stub file for non-WASM platforms
return generateGoClientStubCode(svc, outputDir, dryRun, verbose)
}
// generateGoClientStubCode generates stub code for non-WASM platforms.
func generateGoClientStubCode(svc internal.Service, outputDir string, dryRun, verbose bool) error {
code, err := internal.GenerateClientGoStub(svc)
if err != nil {
return fmt.Errorf("generating stub code: %w", err)
}
formatted, err := format.Source(code)
if err != nil {
return fmt.Errorf("formatting stub code: %w\nRaw code:\n%s", err, code)
}
// Stub code goes in go/ subdirectory with _stub suffix
clientDir := filepath.Join(outputDir, "go")
stubFile := filepath.Join(clientDir, "nd_host_"+strings.ToLower(svc.Name)+"_stub.go")
if dryRun {
fmt.Printf("=== %s ===\n%s\n", stubFile, formatted)
return nil
}
// Create go/ subdirectory if needed
if err := os.MkdirAll(clientDir, 0755); err != nil {
return fmt.Errorf("creating client directory: %w", err)
}
if err := os.WriteFile(stubFile, formatted, 0600); err != nil {
return fmt.Errorf("writing stub file: %w", err)
}
if verbose {
fmt.Printf("Generated Go client stub: %s\n", stubFile)
}
return nil
}
// generatePythonClientCode generates Python client-side code for a service.
func generatePythonClientCode(svc internal.Service, outputDir string, dryRun, verbose bool) error {
code, err := internal.GenerateClientPython(svc)
if err != nil {
return fmt.Errorf("generating code: %w", err)
}
// Python code goes in python/ subdirectory
clientDir := filepath.Join(outputDir, "python")
clientFile := filepath.Join(clientDir, "nd_host_"+strings.ToLower(svc.Name)+".py")
if dryRun {
fmt.Printf("=== %s ===\n%s\n", clientFile, code)
return nil
}
// Create python/ subdirectory if needed
if err := os.MkdirAll(clientDir, 0755); err != nil {
return fmt.Errorf("creating python client directory: %w", err)
}
if err := os.WriteFile(clientFile, code, 0600); err != nil {
return fmt.Errorf("writing file: %w", err)
}
if verbose {
fmt.Printf("Generated Python client code: %s\n", clientFile)
}
return nil
}
// generateRustClientCode generates Rust client-side code for a service.
func generateRustClientCode(svc internal.Service, outputDir string, dryRun, verbose bool) error {
code, err := internal.GenerateClientRust(svc)
if err != nil {
return fmt.Errorf("generating code: %w", err)
}
// Rust code goes in rust/ subdirectory
clientDir := filepath.Join(outputDir, "rust")
clientFile := filepath.Join(clientDir, "nd_host_"+strings.ToLower(svc.Name)+".rs")
if dryRun {
fmt.Printf("=== %s ===\n%s\n", clientFile, code)
return nil
}
// Create rust/ subdirectory if needed
if err := os.MkdirAll(clientDir, 0755); err != nil {
return fmt.Errorf("creating rust client directory: %w", err)
}
if err := os.WriteFile(clientFile, code, 0600); err != nil {
return fmt.Errorf("writing file: %w", err)
}
if verbose {
fmt.Printf("Generated Rust client code: %s\n", clientFile)
}
return nil
}
// generateRustLibFile generates the lib.rs file that exposes all Rust modules.
func generateRustLibFile(services []internal.Service, outputDir string, dryRun, verbose bool) error {
code, err := internal.GenerateRustLib(services)
if err != nil {
return fmt.Errorf("generating lib.rs: %w", err)
}
clientDir := filepath.Join(outputDir, "rust")
libFile := filepath.Join(clientDir, "lib.rs")
if dryRun {
fmt.Printf("=== %s ===\n%s\n", libFile, code)
return nil
}
// Create rust/ subdirectory if needed
if err := os.MkdirAll(clientDir, 0755); err != nil {
return fmt.Errorf("creating rust client directory: %w", err)
}
if err := os.WriteFile(libFile, code, 0600); err != nil {
return fmt.Errorf("writing file: %w", err)
}
if verbose {
fmt.Printf("Generated Rust lib.rs: %s\n", libFile)
}
return nil
}
// generateGoDocFile generates the doc.go file for the Go library.
func generateGoDocFile(services []internal.Service, outputDir string, dryRun, verbose bool) error {
code, err := internal.GenerateGoDoc(services)
if err != nil {
return fmt.Errorf("generating doc.go: %w", err)
}
formatted, err := format.Source(code)
if err != nil {
return fmt.Errorf("formatting doc.go: %w\nRaw code:\n%s", err, code)
}
clientDir := filepath.Join(outputDir, "go")
docFile := filepath.Join(clientDir, "doc.go")
if dryRun {
fmt.Printf("=== %s ===\n%s\n", docFile, formatted)
return nil
}
// Create go/ subdirectory if needed
if err := os.MkdirAll(clientDir, 0755); err != nil {
return fmt.Errorf("creating go client directory: %w", err)
}
if err := os.WriteFile(docFile, formatted, 0600); err != nil {
return fmt.Errorf("writing file: %w", err)
}
if verbose {
fmt.Printf("Generated Go doc.go: %s\n", docFile)
}
return nil
}
// generateGoModFile generates the go.mod file for the Go library.
func generateGoModFile(outputDir string, dryRun, verbose bool) error {
code, err := internal.GenerateGoMod()
if err != nil {
return fmt.Errorf("generating go.mod: %w", err)
}
clientDir := filepath.Join(outputDir, "go")
modFile := filepath.Join(clientDir, "go.mod")
if dryRun {
fmt.Printf("=== %s ===\n%s\n", modFile, code)
return nil
}
// Create go/ subdirectory if needed
if err := os.MkdirAll(clientDir, 0755); err != nil {
return fmt.Errorf("creating go client directory: %w", err)
}
if err := os.WriteFile(modFile, code, 0600); err != nil {
return fmt.Errorf("writing file: %w", err)
}
if verbose {
fmt.Printf("Generated Go go.mod: %s\n", modFile)
}
return nil
}

View File

@ -12,12 +12,9 @@ import (
. "github.com/onsi/gomega"
)
// normalizeGeneratedCode normalizes differences between hostgen and ndpgen output
// so we can reuse hostgen's testdata for ndpgen verification.
// normalizeGeneratedCode normalizes generated code for comparison with expected output.
func normalizeGeneratedCode(code string) string {
// Replace tool name references
code = strings.ReplaceAll(code, "Code generated by hostgen.", "Code generated by ndpgen.")
// Replace package names
// Replace package names (generated uses ndpdk, testdata may use ndhost)
code = strings.ReplaceAll(code, "package ndhost", "package ndpdk")
return code
}
@ -30,8 +27,8 @@ var _ = Describe("ndpgen CLI", Ordered, func() {
)
BeforeAll(func() {
// Set testdata directory (reuse hostgen's testdata)
testdataDir = filepath.Join(mustGetWd(GinkgoT()), "plugins", "cmd", "hostgen", "testdata")
// Set testdata directory
testdataDir = filepath.Join(mustGetWd(GinkgoT()), "plugins", "cmd", "ndpgen", "testdata")
// Build the ndpgen binary
ndpgenBin = filepath.Join(os.TempDir(), "ndpgen-test")
@ -252,38 +249,38 @@ type ServiceB interface {
},
Entry("simple string params",
"echo_service.go", "echo_client_expected.go", "echo_client_expected.py", "echo_client_expected.rs"),
"echo_service.go.txt", "echo_client_expected.go.txt", "echo_client_expected.py", "echo_client_expected.rs"),
Entry("multiple simple params (int32)",
"math_service.go", "math_client_expected.go", "math_client_expected.py", "math_client_expected.rs"),
"math_service.go.txt", "math_client_expected.go.txt", "math_client_expected.py", "math_client_expected.rs"),
Entry("struct param with request type",
"store_service.go", "store_client_expected.go", "store_client_expected.py", "store_client_expected.rs"),
"store_service.go.txt", "store_client_expected.go.txt", "store_client_expected.py", "store_client_expected.rs"),
Entry("mixed simple and complex params",
"list_service.go", "list_client_expected.go", "list_client_expected.py", "list_client_expected.rs"),
"list_service.go.txt", "list_client_expected.go.txt", "list_client_expected.py", "list_client_expected.rs"),
Entry("method without error",
"counter_service.go", "counter_client_expected.go", "counter_client_expected.py", "counter_client_expected.rs"),
"counter_service.go.txt", "counter_client_expected.go.txt", "counter_client_expected.py", "counter_client_expected.rs"),
Entry("no params, error only",
"ping_service.go", "ping_client_expected.go", "ping_client_expected.py", "ping_client_expected.rs"),
"ping_service.go.txt", "ping_client_expected.go.txt", "ping_client_expected.py", "ping_client_expected.rs"),
Entry("map and interface types",
"meta_service.go", "meta_client_expected.go", "meta_client_expected.py", "meta_client_expected.rs"),
"meta_service.go.txt", "meta_client_expected.go.txt", "meta_client_expected.py", "meta_client_expected.rs"),
Entry("pointer types",
"users_service.go", "users_client_expected.go", "users_client_expected.py", "users_client_expected.rs"),
"users_service.go.txt", "users_client_expected.go.txt", "users_client_expected.py", "users_client_expected.rs"),
Entry("multiple returns",
"search_service.go", "search_client_expected.go", "search_client_expected.py", "search_client_expected.rs"),
"search_service.go.txt", "search_client_expected.go.txt", "search_client_expected.py", "search_client_expected.rs"),
Entry("bytes",
"codec_service.go", "codec_client_expected.go", "codec_client_expected.py", "codec_client_expected.rs"),
"codec_service.go.txt", "codec_client_expected.go.txt", "codec_client_expected.py", "codec_client_expected.rs"),
)
It("generates compilable client code for comprehensive service", func() {
serviceCode := readTestdata("comprehensive_service.go")
serviceCode := readTestdata("comprehensive_service.go.txt")
Expect(os.WriteFile(filepath.Join(testDir, "service.go"), []byte(serviceCode), 0600)).To(Succeed())
@ -409,7 +406,7 @@ type TestService interface {
Expect(err).ToNot(HaveOccurred())
contentStr := string(content)
Expect(contentStr).To(ContainSubstring("Code generated by hostgen. DO NOT EDIT."))
Expect(contentStr).To(ContainSubstring("Code generated by ndpgen. DO NOT EDIT."))
Expect(contentStr).To(ContainSubstring("class HostFunctionError(Exception):"))
Expect(contentStr).To(ContainSubstring(`@extism.import_fn("extism:host/user", "test_doaction")`))
Expect(contentStr).To(ContainSubstring("def test_do_action(input: str) -> str:"))

View File

@ -36,7 +36,7 @@ var _ = Describe("Generator", func() {
codeStr := string(code)
// Check for generated header
Expect(codeStr).To(ContainSubstring("Code generated by hostgen. DO NOT EDIT."))
Expect(codeStr).To(ContainSubstring("Code generated by ndpgen. DO NOT EDIT."))
// Check for package declaration
Expect(codeStr).To(ContainSubstring("package host"))
@ -390,7 +390,7 @@ var _ = Describe("Generator", func() {
codeStr := string(code)
// Check for generated header
Expect(codeStr).To(ContainSubstring("Code generated by hostgen. DO NOT EDIT."))
Expect(codeStr).To(ContainSubstring("Code generated by ndpgen. DO NOT EDIT."))
// Check for imports
Expect(codeStr).To(ContainSubstring("from dataclasses import dataclass"))

View File

@ -1,4 +1,4 @@
# Code generated by hostgen. DO NOT EDIT.
# Code generated by ndpgen. DO NOT EDIT.
#
# This file contains client wrappers for the {{.Service.Name}} host service.
# It is intended for use in Navidrome plugins built with extism-py.

View File

@ -1,4 +1,4 @@
// Code generated by hostgen. DO NOT EDIT.
// Code generated by ndpgen. DO NOT EDIT.
//
// This file contains client wrappers for the {{.Service.Name}} host service.
// It is intended for use in Navidrome plugins built with extism-pdk.

View File

@ -1,4 +1,4 @@
// Code generated by hostgen. DO NOT EDIT.
// Code generated by ndpgen. DO NOT EDIT.
package {{.Package}}

View File

@ -1,9 +1,9 @@
// Code generated by hostgen. DO NOT EDIT.
// Code generated by ndpgen. DO NOT EDIT.
//
//! Navidrome Host Function Wrappers for Rust Plugins
//!
//! This crate provides idiomatic Rust wrappers for all Navidrome host services.
//! It is auto-generated by the hostgen tool and should not be edited manually.
//! It is auto-generated by the ndpgen tool and should not be edited manually.
//!
//! # Usage
//!

View File

@ -1,4 +1,4 @@
// Code generated by hostgen. DO NOT EDIT.
// Code generated by ndpgen. DO NOT EDIT.
//
// This file contains client wrappers for the Codec host service.
// It is intended for use in Navidrome plugins built with TinyGo.

View File

@ -1,4 +1,4 @@
# Code generated by hostgen. DO NOT EDIT.
# Code generated by ndpgen. DO NOT EDIT.
#
# This file contains client wrappers for the Codec host service.
# It is intended for use in Navidrome plugins built with extism-py.

View File

@ -1,4 +1,4 @@
// Code generated by hostgen. DO NOT EDIT.
// Code generated by ndpgen. DO NOT EDIT.
//
// This file contains client wrappers for the Codec host service.
// It is intended for use in Navidrome plugins built with extism-pdk.

View File

@ -1,4 +1,4 @@
// Code generated by hostgen. DO NOT EDIT.
// Code generated by ndpgen. DO NOT EDIT.
package testpkg

View File

@ -1,4 +1,4 @@
# Code generated by hostgen. DO NOT EDIT.
# Code generated by ndpgen. DO NOT EDIT.
#
# This file contains client wrappers for the Comprehensive host service.
# It is intended for use in Navidrome plugins built with extism-py.

View File

@ -1,4 +1,4 @@
// Code generated by hostgen. DO NOT EDIT.
// Code generated by ndpgen. DO NOT EDIT.
//
// This file contains client wrappers for the Comprehensive host service.
// It is intended for use in Navidrome plugins built with extism-pdk.

View File

@ -1,4 +1,4 @@
// Code generated by hostgen. DO NOT EDIT.
// Code generated by ndpgen. DO NOT EDIT.
//
// This file contains client wrappers for the Counter host service.
// It is intended for use in Navidrome plugins built with TinyGo.

View File

@ -1,4 +1,4 @@
# Code generated by hostgen. DO NOT EDIT.
# Code generated by ndpgen. DO NOT EDIT.
#
# This file contains client wrappers for the Counter host service.
# It is intended for use in Navidrome plugins built with extism-py.

View File

@ -1,4 +1,4 @@
// Code generated by hostgen. DO NOT EDIT.
// Code generated by ndpgen. DO NOT EDIT.
//
// This file contains client wrappers for the Counter host service.
// It is intended for use in Navidrome plugins built with extism-pdk.

View File

@ -1,4 +1,4 @@
// Code generated by hostgen. DO NOT EDIT.
// Code generated by ndpgen. DO NOT EDIT.
package testpkg

View File

@ -1,4 +1,4 @@
// Code generated by hostgen. DO NOT EDIT.
// Code generated by ndpgen. DO NOT EDIT.
//
// This file contains client wrappers for the Echo host service.
// It is intended for use in Navidrome plugins built with TinyGo.

View File

@ -1,4 +1,4 @@
# Code generated by hostgen. DO NOT EDIT.
# Code generated by ndpgen. DO NOT EDIT.
#
# This file contains client wrappers for the Echo host service.
# It is intended for use in Navidrome plugins built with extism-py.

View File

@ -1,4 +1,4 @@
// Code generated by hostgen. DO NOT EDIT.
// Code generated by ndpgen. DO NOT EDIT.
//
// This file contains client wrappers for the Echo host service.
// It is intended for use in Navidrome plugins built with extism-pdk.

View File

@ -1,4 +1,4 @@
// Code generated by hostgen. DO NOT EDIT.
// Code generated by ndpgen. DO NOT EDIT.
package testpkg

View File

@ -1,4 +1,4 @@
// Code generated by hostgen. DO NOT EDIT.
// Code generated by ndpgen. DO NOT EDIT.
//
// This file contains client wrappers for the List host service.
// It is intended for use in Navidrome plugins built with TinyGo.

View File

@ -1,4 +1,4 @@
# Code generated by hostgen. DO NOT EDIT.
# Code generated by ndpgen. DO NOT EDIT.
#
# This file contains client wrappers for the List host service.
# It is intended for use in Navidrome plugins built with extism-py.

View File

@ -1,4 +1,4 @@
// Code generated by hostgen. DO NOT EDIT.
// Code generated by ndpgen. DO NOT EDIT.
//
// This file contains client wrappers for the List host service.
// It is intended for use in Navidrome plugins built with extism-pdk.

View File

@ -1,4 +1,4 @@
// Code generated by hostgen. DO NOT EDIT.
// Code generated by ndpgen. DO NOT EDIT.
package testpkg

View File

@ -1,4 +1,4 @@
// Code generated by hostgen. DO NOT EDIT.
// Code generated by ndpgen. DO NOT EDIT.
//
// This file contains client wrappers for the Math host service.
// It is intended for use in Navidrome plugins built with TinyGo.

View File

@ -1,4 +1,4 @@
# Code generated by hostgen. DO NOT EDIT.
# Code generated by ndpgen. DO NOT EDIT.
#
# This file contains client wrappers for the Math host service.
# It is intended for use in Navidrome plugins built with extism-py.

View File

@ -1,4 +1,4 @@
// Code generated by hostgen. DO NOT EDIT.
// Code generated by ndpgen. DO NOT EDIT.
//
// This file contains client wrappers for the Math host service.
// It is intended for use in Navidrome plugins built with extism-pdk.

View File

@ -1,4 +1,4 @@
// Code generated by hostgen. DO NOT EDIT.
// Code generated by ndpgen. DO NOT EDIT.
package testpkg

View File

@ -1,4 +1,4 @@
// Code generated by hostgen. DO NOT EDIT.
// Code generated by ndpgen. DO NOT EDIT.
//
// This file contains client wrappers for the Meta host service.
// It is intended for use in Navidrome plugins built with TinyGo.

View File

@ -1,4 +1,4 @@
# Code generated by hostgen. DO NOT EDIT.
# Code generated by ndpgen. DO NOT EDIT.
#
# This file contains client wrappers for the Meta host service.
# It is intended for use in Navidrome plugins built with extism-py.

View File

@ -1,4 +1,4 @@
// Code generated by hostgen. DO NOT EDIT.
// Code generated by ndpgen. DO NOT EDIT.
//
// This file contains client wrappers for the Meta host service.
// It is intended for use in Navidrome plugins built with extism-pdk.

View File

@ -1,4 +1,4 @@
// Code generated by hostgen. DO NOT EDIT.
// Code generated by ndpgen. DO NOT EDIT.
package testpkg

View File

@ -1,4 +1,4 @@
// Code generated by hostgen. DO NOT EDIT.
// Code generated by ndpgen. DO NOT EDIT.
//
// This file contains client wrappers for the Ping host service.
// It is intended for use in Navidrome plugins built with TinyGo.

View File

@ -1,4 +1,4 @@
# Code generated by hostgen. DO NOT EDIT.
# Code generated by ndpgen. DO NOT EDIT.
#
# This file contains client wrappers for the Ping host service.
# It is intended for use in Navidrome plugins built with extism-py.

View File

@ -1,4 +1,4 @@
// Code generated by hostgen. DO NOT EDIT.
// Code generated by ndpgen. DO NOT EDIT.
//
// This file contains client wrappers for the Ping host service.
// It is intended for use in Navidrome plugins built with extism-pdk.

View File

@ -1,4 +1,4 @@
// Code generated by hostgen. DO NOT EDIT.
// Code generated by ndpgen. DO NOT EDIT.
package testpkg

View File

@ -1,4 +1,4 @@
// Code generated by hostgen. DO NOT EDIT.
// Code generated by ndpgen. DO NOT EDIT.
//
// This file contains client wrappers for the Search host service.
// It is intended for use in Navidrome plugins built with TinyGo.

View File

@ -1,4 +1,4 @@
# Code generated by hostgen. DO NOT EDIT.
# Code generated by ndpgen. DO NOT EDIT.
#
# This file contains client wrappers for the Search host service.
# It is intended for use in Navidrome plugins built with extism-py.

View File

@ -1,4 +1,4 @@
// Code generated by hostgen. DO NOT EDIT.
// Code generated by ndpgen. DO NOT EDIT.
//
// This file contains client wrappers for the Search host service.
// It is intended for use in Navidrome plugins built with extism-pdk.

View File

@ -1,4 +1,4 @@
// Code generated by hostgen. DO NOT EDIT.
// Code generated by ndpgen. DO NOT EDIT.
package testpkg

View File

@ -1,4 +1,4 @@
// Code generated by hostgen. DO NOT EDIT.
// Code generated by ndpgen. DO NOT EDIT.
//
// This file contains client wrappers for the Store host service.
// It is intended for use in Navidrome plugins built with TinyGo.

View File

@ -1,4 +1,4 @@
# Code generated by hostgen. DO NOT EDIT.
# Code generated by ndpgen. DO NOT EDIT.
#
# This file contains client wrappers for the Store host service.
# It is intended for use in Navidrome plugins built with extism-py.

View File

@ -1,4 +1,4 @@
// Code generated by hostgen. DO NOT EDIT.
// Code generated by ndpgen. DO NOT EDIT.
//
// This file contains client wrappers for the Store host service.
// It is intended for use in Navidrome plugins built with extism-pdk.

View File

@ -1,4 +1,4 @@
// Code generated by hostgen. DO NOT EDIT.
// Code generated by ndpgen. DO NOT EDIT.
package testpkg

View File

@ -1,4 +1,4 @@
// Code generated by hostgen. DO NOT EDIT.
// Code generated by ndpgen. DO NOT EDIT.
//
// This file contains client wrappers for the Users host service.
// It is intended for use in Navidrome plugins built with TinyGo.

View File

@ -1,4 +1,4 @@
# Code generated by hostgen. DO NOT EDIT.
# Code generated by ndpgen. DO NOT EDIT.
#
# This file contains client wrappers for the Users host service.
# It is intended for use in Navidrome plugins built with extism-py.

View File

@ -1,4 +1,4 @@
// Code generated by hostgen. DO NOT EDIT.
// Code generated by ndpgen. DO NOT EDIT.
//
// This file contains client wrappers for the Users host service.
// It is intended for use in Navidrome plugins built with extism-pdk.

View File

@ -1,4 +1,4 @@
// Code generated by hostgen. DO NOT EDIT.
// Code generated by ndpgen. DO NOT EDIT.
package testpkg

View File

@ -4,7 +4,7 @@ go 1.24
require (
github.com/extism/go-pdk v1.1.3
github.com/navidrome/navidrome/plugins/host/go v0.0.0
github.com/navidrome/navidrome/plugins/pdk/go/host v0.0.0
)
replace github.com/navidrome/navidrome/plugins/host/go => ../../host/go
replace github.com/navidrome/navidrome/plugins/pdk/go/host => ../../pdk/go/host

View File

@ -13,7 +13,7 @@ import (
"strings"
pdk "github.com/extism/go-pdk"
ndhost "github.com/navidrome/navidrome/plugins/host/go"
host "github.com/navidrome/navidrome/plugins/pdk/go/host"
)
const (
@ -116,7 +116,7 @@ func parseTickerSymbols(tickerConfig string) []string {
// connectAndSubscribe connects to Coinbase WebSocket and subscribes to tickers
func connectAndSubscribe(tickers []string) error {
// Connect to WebSocket using host function
resp, err := ndhost.WebSocketConnect(coinbaseWSEndpoint, nil, connectionID)
resp, err := host.WebSocketConnect(coinbaseWSEndpoint, nil, connectionID)
if err != nil {
return fmt.Errorf("WebSocket connection error: %w", err)
}
@ -135,7 +135,7 @@ func connectAndSubscribe(tickers []string) error {
}
// Send subscription message
_, err = ndhost.WebSocketSendText(connectionID, string(subscriptionJSON))
_, err = host.WebSocketSendText(connectionID, string(subscriptionJSON))
if err != nil {
return fmt.Errorf("WebSocket send error: %w", err)
}
@ -206,7 +206,7 @@ func NdWebsocketOnClose(input OnCloseInput) (OnCloseOutput, error) {
pdk.Log(pdk.LogInfo, "Scheduling reconnection attempt in 5 seconds...")
// Schedule a one-time reconnection attempt
_, err := ndhost.SchedulerScheduleOneTime(5, "reconnect", reconnectScheduleID)
_, err := host.SchedulerScheduleOneTime(5, "reconnect", reconnectScheduleID)
if err != nil {
pdk.Log(pdk.LogError, fmt.Sprintf("Failed to schedule reconnection: %v", err))
}
@ -257,7 +257,7 @@ func ndSchedulerCallback() int32 {
pdk.Log(pdk.LogError, fmt.Sprintf("Reconnection failed: %v - will retry in 10 seconds", err))
// Schedule another attempt
_, err := ndhost.SchedulerScheduleOneTime(10, "reconnect", reconnectScheduleID)
_, err := host.SchedulerScheduleOneTime(10, "reconnect", reconnectScheduleID)
if err != nil {
pdk.Log(pdk.LogError, fmt.Sprintf("Failed to schedule retry: %v", err))
}

View File

@ -13,4 +13,4 @@ crate-type = ["cdylib"]
extism-pdk = "1.2"
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
nd-host = { path = "../../host/rust" }
nd-host = { path = "../../pdk/rust/host" }

View File

@ -4,7 +4,7 @@ go 1.24
require (
github.com/extism/go-pdk v1.1.3
github.com/navidrome/navidrome/plugins/host/go v0.0.0
github.com/navidrome/navidrome/plugins/pdk/go/host v0.0.0
)
replace github.com/navidrome/navidrome/plugins/host/go => ../../host/go
replace github.com/navidrome/navidrome/plugins/pdk/go/host => ../../pdk/go/host

View File

@ -16,7 +16,7 @@ import (
"time"
"github.com/extism/go-pdk"
ndhost "github.com/navidrome/navidrome/plugins/host/go"
host "github.com/navidrome/navidrome/plugins/pdk/go/host"
)
// Configuration keys
@ -52,7 +52,7 @@ func getConfig() (clientID string, users map[string]string, err error) {
// getImageURL retrieves the track artwork URL.
func getImageURL(trackID string) string {
resp, err := ndhost.ArtworkGetTrackUrl(trackID, 300)
resp, err := host.ArtworkGetTrackUrl(trackID, 300)
if err != nil {
pdk.Log(pdk.LogWarn, fmt.Sprintf("Failed to get artwork URL: %v", err))
return ""
@ -105,7 +105,7 @@ func NdScrobblerNowPlaying(input NowPlayingInput) (ScrobblerOutput, error) {
}
// Cancel any existing completion schedule
_, _ = ndhost.SchedulerCancelSchedule(fmt.Sprintf("%s-clear", input.Username))
_, _ = host.SchedulerCancelSchedule(fmt.Sprintf("%s-clear", input.Username))
// Calculate timestamps
now := time.Now().Unix()
@ -134,7 +134,7 @@ func NdScrobblerNowPlaying(input NowPlayingInput) (ScrobblerOutput, error) {
// Schedule a timer to clear the activity after the track completes
remainingSeconds := int32(input.Track.Duration) - input.Position + 5
_, err = ndhost.SchedulerScheduleOneTime(remainingSeconds, payloadClearActivity, fmt.Sprintf("%s-clear", input.Username))
_, err = host.SchedulerScheduleOneTime(remainingSeconds, payloadClearActivity, fmt.Sprintf("%s-clear", input.Username))
if err != nil {
pdk.Log(pdk.LogWarn, fmt.Sprintf("Failed to schedule completion timer: %v", err))
}

View File

@ -11,7 +11,7 @@ import (
"time"
"github.com/extism/go-pdk"
ndhost "github.com/navidrome/navidrome/plugins/host/go"
host "github.com/navidrome/navidrome/plugins/pdk/go/host"
)
// Discord WebSocket Gateway constants
@ -89,7 +89,7 @@ func processImage(imageURL, clientID, token string, isDefaultImage bool) (string
// Check cache first
cacheKey := fmt.Sprintf("discord.image.%x", imageURL)
cacheResp, err := ndhost.CacheGetString(cacheKey)
cacheResp, err := host.CacheGetString(cacheKey)
if err == nil && cacheResp.Exists {
pdk.Log(pdk.LogDebug, fmt.Sprintf("Cache hit for image URL: %s", imageURL))
return cacheResp.Value, nil
@ -141,7 +141,7 @@ func processImage(imageURL, clientID, token string, isDefaultImage bool) (string
ttl = 48 * 60 * 60 // 48 hours for default image
}
_, _ = ndhost.CacheSetString(cacheKey, processedImage, ttl)
_, _ = host.CacheSetString(cacheKey, processedImage, ttl)
pdk.Log(pdk.LogDebug, fmt.Sprintf("Cached processed image URL for %s (TTL: %ds)", imageURL, ttl))
return processedImage, nil
@ -184,7 +184,7 @@ func sendMessage(username string, opCode int, payload any) error {
return fmt.Errorf("failed to marshal message: %w", err)
}
_, err = ndhost.WebSocketSendText(username, string(b))
_, err = host.WebSocketSendText(username, string(b))
if err != nil {
return fmt.Errorf("failed to send message: %w", err)
}
@ -208,7 +208,7 @@ func getDiscordGateway() (string, error) {
// sendHeartbeat sends a heartbeat to Discord.
func sendHeartbeat(username string) error {
cacheResp, err := ndhost.CacheGetInt(fmt.Sprintf("discord.seq.%s", username))
cacheResp, err := host.CacheGetInt(fmt.Sprintf("discord.seq.%s", username))
if err != nil {
return fmt.Errorf("failed to get sequence number: %w", err)
}
@ -222,17 +222,17 @@ func cleanupFailedConnection(username string) {
pdk.Log(pdk.LogInfo, fmt.Sprintf("Cleaning up failed connection for user %s", username))
// Cancel the heartbeat schedule
if _, err := ndhost.SchedulerCancelSchedule(username); err != nil {
if _, err := host.SchedulerCancelSchedule(username); err != nil {
pdk.Log(pdk.LogWarn, fmt.Sprintf("Failed to cancel heartbeat schedule for user %s: %v", username, err))
}
// Close the WebSocket connection
if _, err := ndhost.WebSocketCloseConnection(username, 1000, "Connection lost"); err != nil {
if _, err := host.WebSocketCloseConnection(username, 1000, "Connection lost"); err != nil {
pdk.Log(pdk.LogWarn, fmt.Sprintf("Failed to close WebSocket connection for user %s: %v", username, err))
}
// Clean up cache entries
_, _ = ndhost.CacheRemove(fmt.Sprintf("discord.seq.%s", username))
_, _ = host.CacheRemove(fmt.Sprintf("discord.seq.%s", username))
pdk.Log(pdk.LogInfo, fmt.Sprintf("Cleaned up connection for user %s", username))
}
@ -263,7 +263,7 @@ func connect(username, token string) error {
pdk.Log(pdk.LogDebug, fmt.Sprintf("Using gateway: %s", gateway))
// Connect to Discord Gateway
_, err = ndhost.WebSocketConnect(gateway, nil, username)
_, err = host.WebSocketConnect(gateway, nil, username)
if err != nil {
return fmt.Errorf("failed to connect to WebSocket: %w", err)
}
@ -284,7 +284,7 @@ func connect(username, token string) error {
// Schedule heartbeats for this user/connection
cronExpr := fmt.Sprintf("@every %ds", heartbeatInterval)
schedResp, err := ndhost.SchedulerScheduleRecurring(cronExpr, payloadHeartbeat, username)
schedResp, err := host.SchedulerScheduleRecurring(cronExpr, payloadHeartbeat, username)
if err != nil {
return fmt.Errorf("failed to schedule heartbeat: %w", err)
}
@ -296,11 +296,11 @@ func connect(username, token string) error {
// disconnect closes the Discord connection for a user.
func disconnect(username string) error {
if _, err := ndhost.SchedulerCancelSchedule(username); err != nil {
if _, err := host.SchedulerCancelSchedule(username); err != nil {
return fmt.Errorf("failed to cancel schedule: %w", err)
}
if _, err := ndhost.WebSocketCloseConnection(username, 1000, "Navidrome disconnect"); err != nil {
if _, err := host.WebSocketCloseConnection(username, 1000, "Navidrome disconnect"); err != nil {
return fmt.Errorf("failed to close WebSocket connection: %w", err)
}
return nil
@ -324,7 +324,7 @@ func handleWebSocketMessage(connectionID, message string) error {
if v := msg["s"]; v != nil {
seq := int64(v.(float64))
pdk.Log(pdk.LogTrace, fmt.Sprintf("Received sequence number for connection '%s': %d", connectionID, seq))
if _, err := ndhost.CacheSetInt(fmt.Sprintf("discord.seq.%s", connectionID), seq, int64(heartbeatInterval*2)); err != nil {
if _, err := host.CacheSetInt(fmt.Sprintf("discord.seq.%s", connectionID), seq, int64(heartbeatInterval*2)); err != nil {
return fmt.Errorf("failed to store sequence number for user %s: %w", connectionID, err)
}
}

View File

@ -11,6 +11,6 @@ crate-type = ["cdylib"]
[dependencies]
extism-pdk = "1.2"
nd-host = { path = "../../host/rust" }
nd-host = { path = "../../pdk/rust/host" }
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"

View File

@ -1,4 +1,4 @@
// Code generated by hostgen. DO NOT EDIT.
// Code generated by ndpgen. DO NOT EDIT.
package host

View File

@ -1,4 +1,4 @@
// Code generated by hostgen. DO NOT EDIT.
// Code generated by ndpgen. DO NOT EDIT.
package host

View File

@ -27,7 +27,7 @@
//
// # Generated Code
//
// The hostgen tool reads annotated interfaces and generates Extism host function wrappers
// The ndpgen tool reads annotated interfaces and generates Extism host function wrappers
// that handle:
// - JSON serialization/deserialization of request/response types
// - Memory operations (ReadBytes, WriteBytes, Alloc)
@ -37,5 +37,5 @@
// Generated files follow the pattern <servicename>_gen.go and include a header comment
// indicating they should not be edited manually.
//
//go:generate go run ../cmd/hostgen -input=. -output=. -python -go -rust
//go:generate go run ../cmd/ndpgen -input=. -output=../pdk -go -python -rust
package host

View File

@ -1,54 +0,0 @@
// Code generated by hostgen. DO NOT EDIT.
/*
Package ndhost provides Navidrome host function wrappers for Go/TinyGo plugins.
This package is auto-generated by the hostgen tool and should not be edited manually.
# Usage
Add this module as a dependency in your plugin's go.mod:
require github.com/navidrome/navidrome/plugins/host/go v0.0.0
Then import the package in your plugin code:
import ndhost "github.com/navidrome/navidrome/plugins/host/go"
func myPluginFunction() error {
// Use the cache service
_, err := ndhost.CacheSetString("my_key", "my_value", 3600)
if err != nil {
return err
}
// Schedule a recurring task
_, err = ndhost.SchedulerScheduleRecurring("@every 5m", "payload", "task_id")
if err != nil {
return err
}
return nil
}
# Available Services
The following host services are available:
- Artwork: provides artwork URL generation capabilities for plugins.
- Cache: provides in-memory TTL-based caching capabilities for plugins.
- KVStore: provides persistent key-value storage for plugins.
- Library: provides access to music library metadata for plugins.
- Scheduler: provides task scheduling capabilities for plugins.
- SubsonicAPI: provides access to Navidrome's Subsonic API from plugins.
- WebSocket: provides WebSocket communication capabilities for plugins.
# Building Plugins
Go plugins must be compiled to WebAssembly using TinyGo:
tinygo build -o plugin.wasm -target=wasip1 -buildmode=c-shared .
See the examples directory for complete plugin implementations.
*/
package ndhost

View File

@ -1,5 +0,0 @@
module github.com/navidrome/navidrome/plugins/host/go
go 1.24
require github.com/extism/go-pdk v1.1.3

View File

@ -1,3 +0,0 @@
github.com/extism/go-pdk v1.1.0 h1:K2On6XOERxrYdsgu0uLzCxeu/FYRHE8jId/hdEVSYoY=
github.com/extism/go-pdk v1.1.0/go.mod h1:Gz+LIU/YCKnKXhgge8yo5Yu1F/lbv7KtKFkiCSzW/P4=
github.com/extism/go-pdk v1.1.3/go.mod h1:Gz+LIU/YCKnKXhgge8yo5Yu1F/lbv7KtKFkiCSzW/P4=

View File

@ -1,251 +0,0 @@
// Code generated by hostgen. DO NOT EDIT.
//
// This file contains client wrappers for the Artwork host service.
// It is intended for use in Navidrome plugins built with TinyGo.
//
//go:build wasip1
package ndhost
import (
"encoding/json"
"errors"
"github.com/extism/go-pdk"
)
// artwork_getartisturl is the host function provided by Navidrome.
//
//go:wasmimport extism:host/user artwork_getartisturl
func artwork_getartisturl(uint64) uint64
// artwork_getalbumurl is the host function provided by Navidrome.
//
//go:wasmimport extism:host/user artwork_getalbumurl
func artwork_getalbumurl(uint64) uint64
// artwork_gettrackurl is the host function provided by Navidrome.
//
//go:wasmimport extism:host/user artwork_gettrackurl
func artwork_gettrackurl(uint64) uint64
// artwork_getplaylisturl is the host function provided by Navidrome.
//
//go:wasmimport extism:host/user artwork_getplaylisturl
func artwork_getplaylisturl(uint64) uint64
// ArtworkGetArtistUrlRequest is the request type for Artwork.GetArtistUrl.
type ArtworkGetArtistUrlRequest struct {
Id string `json:"id"`
Size int32 `json:"size"`
}
// ArtworkGetArtistUrlResponse is the response type for Artwork.GetArtistUrl.
type ArtworkGetArtistUrlResponse struct {
Url string `json:"url,omitempty"`
Error string `json:"error,omitempty"`
}
// ArtworkGetAlbumUrlRequest is the request type for Artwork.GetAlbumUrl.
type ArtworkGetAlbumUrlRequest struct {
Id string `json:"id"`
Size int32 `json:"size"`
}
// ArtworkGetAlbumUrlResponse is the response type for Artwork.GetAlbumUrl.
type ArtworkGetAlbumUrlResponse struct {
Url string `json:"url,omitempty"`
Error string `json:"error,omitempty"`
}
// ArtworkGetTrackUrlRequest is the request type for Artwork.GetTrackUrl.
type ArtworkGetTrackUrlRequest struct {
Id string `json:"id"`
Size int32 `json:"size"`
}
// ArtworkGetTrackUrlResponse is the response type for Artwork.GetTrackUrl.
type ArtworkGetTrackUrlResponse struct {
Url string `json:"url,omitempty"`
Error string `json:"error,omitempty"`
}
// ArtworkGetPlaylistUrlRequest is the request type for Artwork.GetPlaylistUrl.
type ArtworkGetPlaylistUrlRequest struct {
Id string `json:"id"`
Size int32 `json:"size"`
}
// ArtworkGetPlaylistUrlResponse is the response type for Artwork.GetPlaylistUrl.
type ArtworkGetPlaylistUrlResponse struct {
Url string `json:"url,omitempty"`
Error string `json:"error,omitempty"`
}
// ArtworkGetArtistUrl calls the artwork_getartisturl host function.
// GetArtistUrl generates a public URL for an artist's artwork.
//
// Parameters:
// - id: The artist's unique identifier
// - size: Desired image size in pixels (0 for original size)
//
// Returns the public URL for the artwork, or an error if generation fails.
func ArtworkGetArtistUrl(id string, size int32) (*ArtworkGetArtistUrlResponse, error) {
// Marshal request to JSON
req := ArtworkGetArtistUrlRequest{
Id: id,
Size: size,
}
reqBytes, err := json.Marshal(req)
if err != nil {
return nil, err
}
reqMem := pdk.AllocateBytes(reqBytes)
defer reqMem.Free()
// Call the host function
responsePtr := artwork_getartisturl(reqMem.Offset())
// Read the response from memory
responseMem := pdk.FindMemory(responsePtr)
responseBytes := responseMem.ReadBytes()
// Parse the response
var response ArtworkGetArtistUrlResponse
if err := json.Unmarshal(responseBytes, &response); err != nil {
return nil, err
}
// Convert Error field to Go error
if response.Error != "" {
return nil, errors.New(response.Error)
}
return &response, nil
}
// ArtworkGetAlbumUrl calls the artwork_getalbumurl host function.
// GetAlbumUrl generates a public URL for an album's artwork.
//
// Parameters:
// - id: The album's unique identifier
// - size: Desired image size in pixels (0 for original size)
//
// Returns the public URL for the artwork, or an error if generation fails.
func ArtworkGetAlbumUrl(id string, size int32) (*ArtworkGetAlbumUrlResponse, error) {
// Marshal request to JSON
req := ArtworkGetAlbumUrlRequest{
Id: id,
Size: size,
}
reqBytes, err := json.Marshal(req)
if err != nil {
return nil, err
}
reqMem := pdk.AllocateBytes(reqBytes)
defer reqMem.Free()
// Call the host function
responsePtr := artwork_getalbumurl(reqMem.Offset())
// Read the response from memory
responseMem := pdk.FindMemory(responsePtr)
responseBytes := responseMem.ReadBytes()
// Parse the response
var response ArtworkGetAlbumUrlResponse
if err := json.Unmarshal(responseBytes, &response); err != nil {
return nil, err
}
// Convert Error field to Go error
if response.Error != "" {
return nil, errors.New(response.Error)
}
return &response, nil
}
// ArtworkGetTrackUrl calls the artwork_gettrackurl host function.
// GetTrackUrl generates a public URL for a track's artwork.
//
// Parameters:
// - id: The track's (media file) unique identifier
// - size: Desired image size in pixels (0 for original size)
//
// Returns the public URL for the artwork, or an error if generation fails.
func ArtworkGetTrackUrl(id string, size int32) (*ArtworkGetTrackUrlResponse, error) {
// Marshal request to JSON
req := ArtworkGetTrackUrlRequest{
Id: id,
Size: size,
}
reqBytes, err := json.Marshal(req)
if err != nil {
return nil, err
}
reqMem := pdk.AllocateBytes(reqBytes)
defer reqMem.Free()
// Call the host function
responsePtr := artwork_gettrackurl(reqMem.Offset())
// Read the response from memory
responseMem := pdk.FindMemory(responsePtr)
responseBytes := responseMem.ReadBytes()
// Parse the response
var response ArtworkGetTrackUrlResponse
if err := json.Unmarshal(responseBytes, &response); err != nil {
return nil, err
}
// Convert Error field to Go error
if response.Error != "" {
return nil, errors.New(response.Error)
}
return &response, nil
}
// ArtworkGetPlaylistUrl calls the artwork_getplaylisturl host function.
// GetPlaylistUrl generates a public URL for a playlist's artwork.
//
// Parameters:
// - id: The playlist's unique identifier
// - size: Desired image size in pixels (0 for original size)
//
// Returns the public URL for the artwork, or an error if generation fails.
func ArtworkGetPlaylistUrl(id string, size int32) (*ArtworkGetPlaylistUrlResponse, error) {
// Marshal request to JSON
req := ArtworkGetPlaylistUrlRequest{
Id: id,
Size: size,
}
reqBytes, err := json.Marshal(req)
if err != nil {
return nil, err
}
reqMem := pdk.AllocateBytes(reqBytes)
defer reqMem.Free()
// Call the host function
responsePtr := artwork_getplaylisturl(reqMem.Offset())
// Read the response from memory
responseMem := pdk.FindMemory(responsePtr)
responseBytes := responseMem.ReadBytes()
// Parse the response
var response ArtworkGetPlaylistUrlResponse
if err := json.Unmarshal(responseBytes, &response); err != nil {
return nil, err
}
// Convert Error field to Go error
if response.Error != "" {
return nil, errors.New(response.Error)
}
return &response, nil
}

View File

@ -1,105 +0,0 @@
// Code generated by hostgen. DO NOT EDIT.
//
// This file contains stub implementations for non-WASM builds.
// These stubs allow IDE support and compilation on non-WASM platforms.
// They panic at runtime since host functions are only available in WASM plugins.
//
//go:build !wasip1
package ndhost
// ArtworkGetArtistUrlRequest is the request type for Artwork.GetArtistUrl.
type ArtworkGetArtistUrlRequest struct {
Id string `json:"id"`
Size int32 `json:"size"`
}
// ArtworkGetArtistUrlResponse is the response type for Artwork.GetArtistUrl.
type ArtworkGetArtistUrlResponse struct {
Url string `json:"url,omitempty"`
Error string `json:"error,omitempty"`
}
// ArtworkGetAlbumUrlRequest is the request type for Artwork.GetAlbumUrl.
type ArtworkGetAlbumUrlRequest struct {
Id string `json:"id"`
Size int32 `json:"size"`
}
// ArtworkGetAlbumUrlResponse is the response type for Artwork.GetAlbumUrl.
type ArtworkGetAlbumUrlResponse struct {
Url string `json:"url,omitempty"`
Error string `json:"error,omitempty"`
}
// ArtworkGetTrackUrlRequest is the request type for Artwork.GetTrackUrl.
type ArtworkGetTrackUrlRequest struct {
Id string `json:"id"`
Size int32 `json:"size"`
}
// ArtworkGetTrackUrlResponse is the response type for Artwork.GetTrackUrl.
type ArtworkGetTrackUrlResponse struct {
Url string `json:"url,omitempty"`
Error string `json:"error,omitempty"`
}
// ArtworkGetPlaylistUrlRequest is the request type for Artwork.GetPlaylistUrl.
type ArtworkGetPlaylistUrlRequest struct {
Id string `json:"id"`
Size int32 `json:"size"`
}
// ArtworkGetPlaylistUrlResponse is the response type for Artwork.GetPlaylistUrl.
type ArtworkGetPlaylistUrlResponse struct {
Url string `json:"url,omitempty"`
Error string `json:"error,omitempty"`
}
// ArtworkGetArtistUrl is a stub that panics on non-WASM platforms.
// GetArtistUrl generates a public URL for an artist's artwork.
//
// Parameters:
// - id: The artist's unique identifier
// - size: Desired image size in pixels (0 for original size)
//
// Returns the public URL for the artwork, or an error if generation fails.
func ArtworkGetArtistUrl(id string, size int32) (*ArtworkGetArtistUrlResponse, error) {
panic("ndhost: ArtworkGetArtistUrl is only available in WASM plugins")
}
// ArtworkGetAlbumUrl is a stub that panics on non-WASM platforms.
// GetAlbumUrl generates a public URL for an album's artwork.
//
// Parameters:
// - id: The album's unique identifier
// - size: Desired image size in pixels (0 for original size)
//
// Returns the public URL for the artwork, or an error if generation fails.
func ArtworkGetAlbumUrl(id string, size int32) (*ArtworkGetAlbumUrlResponse, error) {
panic("ndhost: ArtworkGetAlbumUrl is only available in WASM plugins")
}
// ArtworkGetTrackUrl is a stub that panics on non-WASM platforms.
// GetTrackUrl generates a public URL for a track's artwork.
//
// Parameters:
// - id: The track's (media file) unique identifier
// - size: Desired image size in pixels (0 for original size)
//
// Returns the public URL for the artwork, or an error if generation fails.
func ArtworkGetTrackUrl(id string, size int32) (*ArtworkGetTrackUrlResponse, error) {
panic("ndhost: ArtworkGetTrackUrl is only available in WASM plugins")
}
// ArtworkGetPlaylistUrl is a stub that panics on non-WASM platforms.
// GetPlaylistUrl generates a public URL for a playlist's artwork.
//
// Parameters:
// - id: The playlist's unique identifier
// - size: Desired image size in pixels (0 for original size)
//
// Returns the public URL for the artwork, or an error if generation fails.
func ArtworkGetPlaylistUrl(id string, size int32) (*ArtworkGetPlaylistUrlResponse, error) {
panic("ndhost: ArtworkGetPlaylistUrl is only available in WASM plugins")
}

View File

@ -1,602 +0,0 @@
// Code generated by hostgen. DO NOT EDIT.
//
// This file contains client wrappers for the Cache host service.
// It is intended for use in Navidrome plugins built with TinyGo.
//
//go:build wasip1
package ndhost
import (
"encoding/json"
"errors"
"github.com/extism/go-pdk"
)
// cache_setstring is the host function provided by Navidrome.
//
//go:wasmimport extism:host/user cache_setstring
func cache_setstring(uint64) uint64
// cache_getstring is the host function provided by Navidrome.
//
//go:wasmimport extism:host/user cache_getstring
func cache_getstring(uint64) uint64
// cache_setint is the host function provided by Navidrome.
//
//go:wasmimport extism:host/user cache_setint
func cache_setint(uint64) uint64
// cache_getint is the host function provided by Navidrome.
//
//go:wasmimport extism:host/user cache_getint
func cache_getint(uint64) uint64
// cache_setfloat is the host function provided by Navidrome.
//
//go:wasmimport extism:host/user cache_setfloat
func cache_setfloat(uint64) uint64
// cache_getfloat is the host function provided by Navidrome.
//
//go:wasmimport extism:host/user cache_getfloat
func cache_getfloat(uint64) uint64
// cache_setbytes is the host function provided by Navidrome.
//
//go:wasmimport extism:host/user cache_setbytes
func cache_setbytes(uint64) uint64
// cache_getbytes is the host function provided by Navidrome.
//
//go:wasmimport extism:host/user cache_getbytes
func cache_getbytes(uint64) uint64
// cache_has is the host function provided by Navidrome.
//
//go:wasmimport extism:host/user cache_has
func cache_has(uint64) uint64
// cache_remove is the host function provided by Navidrome.
//
//go:wasmimport extism:host/user cache_remove
func cache_remove(uint64) uint64
// CacheSetStringRequest is the request type for Cache.SetString.
type CacheSetStringRequest struct {
Key string `json:"key"`
Value string `json:"value"`
TtlSeconds int64 `json:"ttlSeconds"`
}
// CacheSetStringResponse is the response type for Cache.SetString.
type CacheSetStringResponse struct {
Error string `json:"error,omitempty"`
}
// CacheGetStringRequest is the request type for Cache.GetString.
type CacheGetStringRequest struct {
Key string `json:"key"`
}
// CacheGetStringResponse is the response type for Cache.GetString.
type CacheGetStringResponse struct {
Value string `json:"value,omitempty"`
Exists bool `json:"exists,omitempty"`
Error string `json:"error,omitempty"`
}
// CacheSetIntRequest is the request type for Cache.SetInt.
type CacheSetIntRequest struct {
Key string `json:"key"`
Value int64 `json:"value"`
TtlSeconds int64 `json:"ttlSeconds"`
}
// CacheSetIntResponse is the response type for Cache.SetInt.
type CacheSetIntResponse struct {
Error string `json:"error,omitempty"`
}
// CacheGetIntRequest is the request type for Cache.GetInt.
type CacheGetIntRequest struct {
Key string `json:"key"`
}
// CacheGetIntResponse is the response type for Cache.GetInt.
type CacheGetIntResponse struct {
Value int64 `json:"value,omitempty"`
Exists bool `json:"exists,omitempty"`
Error string `json:"error,omitempty"`
}
// CacheSetFloatRequest is the request type for Cache.SetFloat.
type CacheSetFloatRequest struct {
Key string `json:"key"`
Value float64 `json:"value"`
TtlSeconds int64 `json:"ttlSeconds"`
}
// CacheSetFloatResponse is the response type for Cache.SetFloat.
type CacheSetFloatResponse struct {
Error string `json:"error,omitempty"`
}
// CacheGetFloatRequest is the request type for Cache.GetFloat.
type CacheGetFloatRequest struct {
Key string `json:"key"`
}
// CacheGetFloatResponse is the response type for Cache.GetFloat.
type CacheGetFloatResponse struct {
Value float64 `json:"value,omitempty"`
Exists bool `json:"exists,omitempty"`
Error string `json:"error,omitempty"`
}
// CacheSetBytesRequest is the request type for Cache.SetBytes.
type CacheSetBytesRequest struct {
Key string `json:"key"`
Value []byte `json:"value"`
TtlSeconds int64 `json:"ttlSeconds"`
}
// CacheSetBytesResponse is the response type for Cache.SetBytes.
type CacheSetBytesResponse struct {
Error string `json:"error,omitempty"`
}
// CacheGetBytesRequest is the request type for Cache.GetBytes.
type CacheGetBytesRequest struct {
Key string `json:"key"`
}
// CacheGetBytesResponse is the response type for Cache.GetBytes.
type CacheGetBytesResponse struct {
Value []byte `json:"value,omitempty"`
Exists bool `json:"exists,omitempty"`
Error string `json:"error,omitempty"`
}
// CacheHasRequest is the request type for Cache.Has.
type CacheHasRequest struct {
Key string `json:"key"`
}
// CacheHasResponse is the response type for Cache.Has.
type CacheHasResponse struct {
Exists bool `json:"exists,omitempty"`
Error string `json:"error,omitempty"`
}
// CacheRemoveRequest is the request type for Cache.Remove.
type CacheRemoveRequest struct {
Key string `json:"key"`
}
// CacheRemoveResponse is the response type for Cache.Remove.
type CacheRemoveResponse struct {
Error string `json:"error,omitempty"`
}
// CacheSetString calls the cache_setstring host function.
// SetString stores a string value in the cache.
//
// Parameters:
// - key: The cache key (will be namespaced with plugin ID)
// - value: The string value to store
// - ttlSeconds: Time-to-live in seconds (0 uses default of 24 hours)
//
// Returns an error if the operation fails.
func CacheSetString(key string, value string, ttlSeconds int64) (*CacheSetStringResponse, error) {
// Marshal request to JSON
req := CacheSetStringRequest{
Key: key,
Value: value,
TtlSeconds: ttlSeconds,
}
reqBytes, err := json.Marshal(req)
if err != nil {
return nil, err
}
reqMem := pdk.AllocateBytes(reqBytes)
defer reqMem.Free()
// Call the host function
responsePtr := cache_setstring(reqMem.Offset())
// Read the response from memory
responseMem := pdk.FindMemory(responsePtr)
responseBytes := responseMem.ReadBytes()
// Parse the response
var response CacheSetStringResponse
if err := json.Unmarshal(responseBytes, &response); err != nil {
return nil, err
}
// Convert Error field to Go error
if response.Error != "" {
return nil, errors.New(response.Error)
}
return &response, nil
}
// CacheGetString calls the cache_getstring host function.
// GetString retrieves a string value from the cache.
//
// Parameters:
// - key: The cache key (will be namespaced with plugin ID)
//
// Returns the value and whether the key exists. If the key doesn't exist
// or the stored value is not a string, exists will be false.
func CacheGetString(key string) (*CacheGetStringResponse, error) {
// Marshal request to JSON
req := CacheGetStringRequest{
Key: key,
}
reqBytes, err := json.Marshal(req)
if err != nil {
return nil, err
}
reqMem := pdk.AllocateBytes(reqBytes)
defer reqMem.Free()
// Call the host function
responsePtr := cache_getstring(reqMem.Offset())
// Read the response from memory
responseMem := pdk.FindMemory(responsePtr)
responseBytes := responseMem.ReadBytes()
// Parse the response
var response CacheGetStringResponse
if err := json.Unmarshal(responseBytes, &response); err != nil {
return nil, err
}
// Convert Error field to Go error
if response.Error != "" {
return nil, errors.New(response.Error)
}
return &response, nil
}
// CacheSetInt calls the cache_setint host function.
// SetInt stores an integer value in the cache.
//
// Parameters:
// - key: The cache key (will be namespaced with plugin ID)
// - value: The integer value to store
// - ttlSeconds: Time-to-live in seconds (0 uses default of 24 hours)
//
// Returns an error if the operation fails.
func CacheSetInt(key string, value int64, ttlSeconds int64) (*CacheSetIntResponse, error) {
// Marshal request to JSON
req := CacheSetIntRequest{
Key: key,
Value: value,
TtlSeconds: ttlSeconds,
}
reqBytes, err := json.Marshal(req)
if err != nil {
return nil, err
}
reqMem := pdk.AllocateBytes(reqBytes)
defer reqMem.Free()
// Call the host function
responsePtr := cache_setint(reqMem.Offset())
// Read the response from memory
responseMem := pdk.FindMemory(responsePtr)
responseBytes := responseMem.ReadBytes()
// Parse the response
var response CacheSetIntResponse
if err := json.Unmarshal(responseBytes, &response); err != nil {
return nil, err
}
// Convert Error field to Go error
if response.Error != "" {
return nil, errors.New(response.Error)
}
return &response, nil
}
// CacheGetInt calls the cache_getint host function.
// GetInt retrieves an integer value from the cache.
//
// Parameters:
// - key: The cache key (will be namespaced with plugin ID)
//
// Returns the value and whether the key exists. If the key doesn't exist
// or the stored value is not an integer, exists will be false.
func CacheGetInt(key string) (*CacheGetIntResponse, error) {
// Marshal request to JSON
req := CacheGetIntRequest{
Key: key,
}
reqBytes, err := json.Marshal(req)
if err != nil {
return nil, err
}
reqMem := pdk.AllocateBytes(reqBytes)
defer reqMem.Free()
// Call the host function
responsePtr := cache_getint(reqMem.Offset())
// Read the response from memory
responseMem := pdk.FindMemory(responsePtr)
responseBytes := responseMem.ReadBytes()
// Parse the response
var response CacheGetIntResponse
if err := json.Unmarshal(responseBytes, &response); err != nil {
return nil, err
}
// Convert Error field to Go error
if response.Error != "" {
return nil, errors.New(response.Error)
}
return &response, nil
}
// CacheSetFloat calls the cache_setfloat host function.
// SetFloat stores a float value in the cache.
//
// Parameters:
// - key: The cache key (will be namespaced with plugin ID)
// - value: The float value to store
// - ttlSeconds: Time-to-live in seconds (0 uses default of 24 hours)
//
// Returns an error if the operation fails.
func CacheSetFloat(key string, value float64, ttlSeconds int64) (*CacheSetFloatResponse, error) {
// Marshal request to JSON
req := CacheSetFloatRequest{
Key: key,
Value: value,
TtlSeconds: ttlSeconds,
}
reqBytes, err := json.Marshal(req)
if err != nil {
return nil, err
}
reqMem := pdk.AllocateBytes(reqBytes)
defer reqMem.Free()
// Call the host function
responsePtr := cache_setfloat(reqMem.Offset())
// Read the response from memory
responseMem := pdk.FindMemory(responsePtr)
responseBytes := responseMem.ReadBytes()
// Parse the response
var response CacheSetFloatResponse
if err := json.Unmarshal(responseBytes, &response); err != nil {
return nil, err
}
// Convert Error field to Go error
if response.Error != "" {
return nil, errors.New(response.Error)
}
return &response, nil
}
// CacheGetFloat calls the cache_getfloat host function.
// GetFloat retrieves a float value from the cache.
//
// Parameters:
// - key: The cache key (will be namespaced with plugin ID)
//
// Returns the value and whether the key exists. If the key doesn't exist
// or the stored value is not a float, exists will be false.
func CacheGetFloat(key string) (*CacheGetFloatResponse, error) {
// Marshal request to JSON
req := CacheGetFloatRequest{
Key: key,
}
reqBytes, err := json.Marshal(req)
if err != nil {
return nil, err
}
reqMem := pdk.AllocateBytes(reqBytes)
defer reqMem.Free()
// Call the host function
responsePtr := cache_getfloat(reqMem.Offset())
// Read the response from memory
responseMem := pdk.FindMemory(responsePtr)
responseBytes := responseMem.ReadBytes()
// Parse the response
var response CacheGetFloatResponse
if err := json.Unmarshal(responseBytes, &response); err != nil {
return nil, err
}
// Convert Error field to Go error
if response.Error != "" {
return nil, errors.New(response.Error)
}
return &response, nil
}
// CacheSetBytes calls the cache_setbytes host function.
// SetBytes stores a byte slice in the cache.
//
// Parameters:
// - key: The cache key (will be namespaced with plugin ID)
// - value: The byte slice to store
// - ttlSeconds: Time-to-live in seconds (0 uses default of 24 hours)
//
// Returns an error if the operation fails.
func CacheSetBytes(key string, value []byte, ttlSeconds int64) (*CacheSetBytesResponse, error) {
// Marshal request to JSON
req := CacheSetBytesRequest{
Key: key,
Value: value,
TtlSeconds: ttlSeconds,
}
reqBytes, err := json.Marshal(req)
if err != nil {
return nil, err
}
reqMem := pdk.AllocateBytes(reqBytes)
defer reqMem.Free()
// Call the host function
responsePtr := cache_setbytes(reqMem.Offset())
// Read the response from memory
responseMem := pdk.FindMemory(responsePtr)
responseBytes := responseMem.ReadBytes()
// Parse the response
var response CacheSetBytesResponse
if err := json.Unmarshal(responseBytes, &response); err != nil {
return nil, err
}
// Convert Error field to Go error
if response.Error != "" {
return nil, errors.New(response.Error)
}
return &response, nil
}
// CacheGetBytes calls the cache_getbytes host function.
// GetBytes retrieves a byte slice from the cache.
//
// Parameters:
// - key: The cache key (will be namespaced with plugin ID)
//
// Returns the value and whether the key exists. If the key doesn't exist
// or the stored value is not a byte slice, exists will be false.
func CacheGetBytes(key string) (*CacheGetBytesResponse, error) {
// Marshal request to JSON
req := CacheGetBytesRequest{
Key: key,
}
reqBytes, err := json.Marshal(req)
if err != nil {
return nil, err
}
reqMem := pdk.AllocateBytes(reqBytes)
defer reqMem.Free()
// Call the host function
responsePtr := cache_getbytes(reqMem.Offset())
// Read the response from memory
responseMem := pdk.FindMemory(responsePtr)
responseBytes := responseMem.ReadBytes()
// Parse the response
var response CacheGetBytesResponse
if err := json.Unmarshal(responseBytes, &response); err != nil {
return nil, err
}
// Convert Error field to Go error
if response.Error != "" {
return nil, errors.New(response.Error)
}
return &response, nil
}
// CacheHas calls the cache_has host function.
// Has checks if a key exists in the cache.
//
// Parameters:
// - key: The cache key (will be namespaced with plugin ID)
//
// Returns true if the key exists and has not expired.
func CacheHas(key string) (*CacheHasResponse, error) {
// Marshal request to JSON
req := CacheHasRequest{
Key: key,
}
reqBytes, err := json.Marshal(req)
if err != nil {
return nil, err
}
reqMem := pdk.AllocateBytes(reqBytes)
defer reqMem.Free()
// Call the host function
responsePtr := cache_has(reqMem.Offset())
// Read the response from memory
responseMem := pdk.FindMemory(responsePtr)
responseBytes := responseMem.ReadBytes()
// Parse the response
var response CacheHasResponse
if err := json.Unmarshal(responseBytes, &response); err != nil {
return nil, err
}
// Convert Error field to Go error
if response.Error != "" {
return nil, errors.New(response.Error)
}
return &response, nil
}
// CacheRemove calls the cache_remove host function.
// Remove deletes a value from the cache.
//
// Parameters:
// - key: The cache key (will be namespaced with plugin ID)
//
// Returns an error if the operation fails. Does not return an error if the key doesn't exist.
func CacheRemove(key string) (*CacheRemoveResponse, error) {
// Marshal request to JSON
req := CacheRemoveRequest{
Key: key,
}
reqBytes, err := json.Marshal(req)
if err != nil {
return nil, err
}
reqMem := pdk.AllocateBytes(reqBytes)
defer reqMem.Free()
// Call the host function
responsePtr := cache_remove(reqMem.Offset())
// Read the response from memory
responseMem := pdk.FindMemory(responsePtr)
responseBytes := responseMem.ReadBytes()
// Parse the response
var response CacheRemoveResponse
if err := json.Unmarshal(responseBytes, &response); err != nil {
return nil, err
}
// Convert Error field to Go error
if response.Error != "" {
return nil, errors.New(response.Error)
}
return &response, nil
}

View File

@ -1,248 +0,0 @@
// Code generated by hostgen. DO NOT EDIT.
//
// This file contains stub implementations for non-WASM builds.
// These stubs allow IDE support and compilation on non-WASM platforms.
// They panic at runtime since host functions are only available in WASM plugins.
//
//go:build !wasip1
package ndhost
// CacheSetStringRequest is the request type for Cache.SetString.
type CacheSetStringRequest struct {
Key string `json:"key"`
Value string `json:"value"`
TtlSeconds int64 `json:"ttlSeconds"`
}
// CacheSetStringResponse is the response type for Cache.SetString.
type CacheSetStringResponse struct {
Error string `json:"error,omitempty"`
}
// CacheGetStringRequest is the request type for Cache.GetString.
type CacheGetStringRequest struct {
Key string `json:"key"`
}
// CacheGetStringResponse is the response type for Cache.GetString.
type CacheGetStringResponse struct {
Value string `json:"value,omitempty"`
Exists bool `json:"exists,omitempty"`
Error string `json:"error,omitempty"`
}
// CacheSetIntRequest is the request type for Cache.SetInt.
type CacheSetIntRequest struct {
Key string `json:"key"`
Value int64 `json:"value"`
TtlSeconds int64 `json:"ttlSeconds"`
}
// CacheSetIntResponse is the response type for Cache.SetInt.
type CacheSetIntResponse struct {
Error string `json:"error,omitempty"`
}
// CacheGetIntRequest is the request type for Cache.GetInt.
type CacheGetIntRequest struct {
Key string `json:"key"`
}
// CacheGetIntResponse is the response type for Cache.GetInt.
type CacheGetIntResponse struct {
Value int64 `json:"value,omitempty"`
Exists bool `json:"exists,omitempty"`
Error string `json:"error,omitempty"`
}
// CacheSetFloatRequest is the request type for Cache.SetFloat.
type CacheSetFloatRequest struct {
Key string `json:"key"`
Value float64 `json:"value"`
TtlSeconds int64 `json:"ttlSeconds"`
}
// CacheSetFloatResponse is the response type for Cache.SetFloat.
type CacheSetFloatResponse struct {
Error string `json:"error,omitempty"`
}
// CacheGetFloatRequest is the request type for Cache.GetFloat.
type CacheGetFloatRequest struct {
Key string `json:"key"`
}
// CacheGetFloatResponse is the response type for Cache.GetFloat.
type CacheGetFloatResponse struct {
Value float64 `json:"value,omitempty"`
Exists bool `json:"exists,omitempty"`
Error string `json:"error,omitempty"`
}
// CacheSetBytesRequest is the request type for Cache.SetBytes.
type CacheSetBytesRequest struct {
Key string `json:"key"`
Value []byte `json:"value"`
TtlSeconds int64 `json:"ttlSeconds"`
}
// CacheSetBytesResponse is the response type for Cache.SetBytes.
type CacheSetBytesResponse struct {
Error string `json:"error,omitempty"`
}
// CacheGetBytesRequest is the request type for Cache.GetBytes.
type CacheGetBytesRequest struct {
Key string `json:"key"`
}
// CacheGetBytesResponse is the response type for Cache.GetBytes.
type CacheGetBytesResponse struct {
Value []byte `json:"value,omitempty"`
Exists bool `json:"exists,omitempty"`
Error string `json:"error,omitempty"`
}
// CacheHasRequest is the request type for Cache.Has.
type CacheHasRequest struct {
Key string `json:"key"`
}
// CacheHasResponse is the response type for Cache.Has.
type CacheHasResponse struct {
Exists bool `json:"exists,omitempty"`
Error string `json:"error,omitempty"`
}
// CacheRemoveRequest is the request type for Cache.Remove.
type CacheRemoveRequest struct {
Key string `json:"key"`
}
// CacheRemoveResponse is the response type for Cache.Remove.
type CacheRemoveResponse struct {
Error string `json:"error,omitempty"`
}
// CacheSetString is a stub that panics on non-WASM platforms.
// SetString stores a string value in the cache.
//
// Parameters:
// - key: The cache key (will be namespaced with plugin ID)
// - value: The string value to store
// - ttlSeconds: Time-to-live in seconds (0 uses default of 24 hours)
//
// Returns an error if the operation fails.
func CacheSetString(key string, value string, ttlSeconds int64) (*CacheSetStringResponse, error) {
panic("ndhost: CacheSetString is only available in WASM plugins")
}
// CacheGetString is a stub that panics on non-WASM platforms.
// GetString retrieves a string value from the cache.
//
// Parameters:
// - key: The cache key (will be namespaced with plugin ID)
//
// Returns the value and whether the key exists. If the key doesn't exist
// or the stored value is not a string, exists will be false.
func CacheGetString(key string) (*CacheGetStringResponse, error) {
panic("ndhost: CacheGetString is only available in WASM plugins")
}
// CacheSetInt is a stub that panics on non-WASM platforms.
// SetInt stores an integer value in the cache.
//
// Parameters:
// - key: The cache key (will be namespaced with plugin ID)
// - value: The integer value to store
// - ttlSeconds: Time-to-live in seconds (0 uses default of 24 hours)
//
// Returns an error if the operation fails.
func CacheSetInt(key string, value int64, ttlSeconds int64) (*CacheSetIntResponse, error) {
panic("ndhost: CacheSetInt is only available in WASM plugins")
}
// CacheGetInt is a stub that panics on non-WASM platforms.
// GetInt retrieves an integer value from the cache.
//
// Parameters:
// - key: The cache key (will be namespaced with plugin ID)
//
// Returns the value and whether the key exists. If the key doesn't exist
// or the stored value is not an integer, exists will be false.
func CacheGetInt(key string) (*CacheGetIntResponse, error) {
panic("ndhost: CacheGetInt is only available in WASM plugins")
}
// CacheSetFloat is a stub that panics on non-WASM platforms.
// SetFloat stores a float value in the cache.
//
// Parameters:
// - key: The cache key (will be namespaced with plugin ID)
// - value: The float value to store
// - ttlSeconds: Time-to-live in seconds (0 uses default of 24 hours)
//
// Returns an error if the operation fails.
func CacheSetFloat(key string, value float64, ttlSeconds int64) (*CacheSetFloatResponse, error) {
panic("ndhost: CacheSetFloat is only available in WASM plugins")
}
// CacheGetFloat is a stub that panics on non-WASM platforms.
// GetFloat retrieves a float value from the cache.
//
// Parameters:
// - key: The cache key (will be namespaced with plugin ID)
//
// Returns the value and whether the key exists. If the key doesn't exist
// or the stored value is not a float, exists will be false.
func CacheGetFloat(key string) (*CacheGetFloatResponse, error) {
panic("ndhost: CacheGetFloat is only available in WASM plugins")
}
// CacheSetBytes is a stub that panics on non-WASM platforms.
// SetBytes stores a byte slice in the cache.
//
// Parameters:
// - key: The cache key (will be namespaced with plugin ID)
// - value: The byte slice to store
// - ttlSeconds: Time-to-live in seconds (0 uses default of 24 hours)
//
// Returns an error if the operation fails.
func CacheSetBytes(key string, value []byte, ttlSeconds int64) (*CacheSetBytesResponse, error) {
panic("ndhost: CacheSetBytes is only available in WASM plugins")
}
// CacheGetBytes is a stub that panics on non-WASM platforms.
// GetBytes retrieves a byte slice from the cache.
//
// Parameters:
// - key: The cache key (will be namespaced with plugin ID)
//
// Returns the value and whether the key exists. If the key doesn't exist
// or the stored value is not a byte slice, exists will be false.
func CacheGetBytes(key string) (*CacheGetBytesResponse, error) {
panic("ndhost: CacheGetBytes is only available in WASM plugins")
}
// CacheHas is a stub that panics on non-WASM platforms.
// Has checks if a key exists in the cache.
//
// Parameters:
// - key: The cache key (will be namespaced with plugin ID)
//
// Returns true if the key exists and has not expired.
func CacheHas(key string) (*CacheHasResponse, error) {
panic("ndhost: CacheHas is only available in WASM plugins")
}
// CacheRemove is a stub that panics on non-WASM platforms.
// Remove deletes a value from the cache.
//
// Parameters:
// - key: The cache key (will be namespaced with plugin ID)
//
// Returns an error if the operation fails. Does not return an error if the key doesn't exist.
func CacheRemove(key string) (*CacheRemoveResponse, error) {
panic("ndhost: CacheRemove is only available in WASM plugins")
}

View File

@ -1,336 +0,0 @@
// Code generated by hostgen. DO NOT EDIT.
//
// This file contains client wrappers for the KVStore host service.
// It is intended for use in Navidrome plugins built with TinyGo.
//
//go:build wasip1
package ndhost
import (
"encoding/json"
"errors"
"github.com/extism/go-pdk"
)
// kvstore_set is the host function provided by Navidrome.
//
//go:wasmimport extism:host/user kvstore_set
func kvstore_set(uint64) uint64
// kvstore_get is the host function provided by Navidrome.
//
//go:wasmimport extism:host/user kvstore_get
func kvstore_get(uint64) uint64
// kvstore_delete is the host function provided by Navidrome.
//
//go:wasmimport extism:host/user kvstore_delete
func kvstore_delete(uint64) uint64
// kvstore_has is the host function provided by Navidrome.
//
//go:wasmimport extism:host/user kvstore_has
func kvstore_has(uint64) uint64
// kvstore_list is the host function provided by Navidrome.
//
//go:wasmimport extism:host/user kvstore_list
func kvstore_list(uint64) uint64
// kvstore_getstorageused is the host function provided by Navidrome.
//
//go:wasmimport extism:host/user kvstore_getstorageused
func kvstore_getstorageused(uint64) uint64
// KVStoreSetRequest is the request type for KVStore.Set.
type KVStoreSetRequest struct {
Key string `json:"key"`
Value []byte `json:"value"`
}
// KVStoreSetResponse is the response type for KVStore.Set.
type KVStoreSetResponse struct {
Error string `json:"error,omitempty"`
}
// KVStoreGetRequest is the request type for KVStore.Get.
type KVStoreGetRequest struct {
Key string `json:"key"`
}
// KVStoreGetResponse is the response type for KVStore.Get.
type KVStoreGetResponse struct {
Value []byte `json:"value,omitempty"`
Exists bool `json:"exists,omitempty"`
Error string `json:"error,omitempty"`
}
// KVStoreDeleteRequest is the request type for KVStore.Delete.
type KVStoreDeleteRequest struct {
Key string `json:"key"`
}
// KVStoreDeleteResponse is the response type for KVStore.Delete.
type KVStoreDeleteResponse struct {
Error string `json:"error,omitempty"`
}
// KVStoreHasRequest is the request type for KVStore.Has.
type KVStoreHasRequest struct {
Key string `json:"key"`
}
// KVStoreHasResponse is the response type for KVStore.Has.
type KVStoreHasResponse struct {
Exists bool `json:"exists,omitempty"`
Error string `json:"error,omitempty"`
}
// KVStoreListRequest is the request type for KVStore.List.
type KVStoreListRequest struct {
Prefix string `json:"prefix"`
}
// KVStoreListResponse is the response type for KVStore.List.
type KVStoreListResponse struct {
Keys []string `json:"keys,omitempty"`
Error string `json:"error,omitempty"`
}
// KVStoreGetStorageUsedResponse is the response type for KVStore.GetStorageUsed.
type KVStoreGetStorageUsedResponse struct {
Bytes int64 `json:"bytes,omitempty"`
Error string `json:"error,omitempty"`
}
// KVStoreSet calls the kvstore_set host function.
// Set stores a byte value with the given key.
//
// Parameters:
// - key: The storage key (max 256 bytes, UTF-8)
// - value: The byte slice to store
//
// Returns an error if the storage limit would be exceeded or the operation fails.
func KVStoreSet(key string, value []byte) (*KVStoreSetResponse, error) {
// Marshal request to JSON
req := KVStoreSetRequest{
Key: key,
Value: value,
}
reqBytes, err := json.Marshal(req)
if err != nil {
return nil, err
}
reqMem := pdk.AllocateBytes(reqBytes)
defer reqMem.Free()
// Call the host function
responsePtr := kvstore_set(reqMem.Offset())
// Read the response from memory
responseMem := pdk.FindMemory(responsePtr)
responseBytes := responseMem.ReadBytes()
// Parse the response
var response KVStoreSetResponse
if err := json.Unmarshal(responseBytes, &response); err != nil {
return nil, err
}
// Convert Error field to Go error
if response.Error != "" {
return nil, errors.New(response.Error)
}
return &response, nil
}
// KVStoreGet calls the kvstore_get host function.
// Get retrieves a byte value from storage.
//
// Parameters:
// - key: The storage key
//
// Returns the value and whether the key exists.
func KVStoreGet(key string) (*KVStoreGetResponse, error) {
// Marshal request to JSON
req := KVStoreGetRequest{
Key: key,
}
reqBytes, err := json.Marshal(req)
if err != nil {
return nil, err
}
reqMem := pdk.AllocateBytes(reqBytes)
defer reqMem.Free()
// Call the host function
responsePtr := kvstore_get(reqMem.Offset())
// Read the response from memory
responseMem := pdk.FindMemory(responsePtr)
responseBytes := responseMem.ReadBytes()
// Parse the response
var response KVStoreGetResponse
if err := json.Unmarshal(responseBytes, &response); err != nil {
return nil, err
}
// Convert Error field to Go error
if response.Error != "" {
return nil, errors.New(response.Error)
}
return &response, nil
}
// KVStoreDelete calls the kvstore_delete host function.
// Delete removes a value from storage.
//
// Parameters:
// - key: The storage key
//
// Returns an error if the operation fails. Does not return an error if the key doesn't exist.
func KVStoreDelete(key string) (*KVStoreDeleteResponse, error) {
// Marshal request to JSON
req := KVStoreDeleteRequest{
Key: key,
}
reqBytes, err := json.Marshal(req)
if err != nil {
return nil, err
}
reqMem := pdk.AllocateBytes(reqBytes)
defer reqMem.Free()
// Call the host function
responsePtr := kvstore_delete(reqMem.Offset())
// Read the response from memory
responseMem := pdk.FindMemory(responsePtr)
responseBytes := responseMem.ReadBytes()
// Parse the response
var response KVStoreDeleteResponse
if err := json.Unmarshal(responseBytes, &response); err != nil {
return nil, err
}
// Convert Error field to Go error
if response.Error != "" {
return nil, errors.New(response.Error)
}
return &response, nil
}
// KVStoreHas calls the kvstore_has host function.
// Has checks if a key exists in storage.
//
// Parameters:
// - key: The storage key
//
// Returns true if the key exists.
func KVStoreHas(key string) (*KVStoreHasResponse, error) {
// Marshal request to JSON
req := KVStoreHasRequest{
Key: key,
}
reqBytes, err := json.Marshal(req)
if err != nil {
return nil, err
}
reqMem := pdk.AllocateBytes(reqBytes)
defer reqMem.Free()
// Call the host function
responsePtr := kvstore_has(reqMem.Offset())
// Read the response from memory
responseMem := pdk.FindMemory(responsePtr)
responseBytes := responseMem.ReadBytes()
// Parse the response
var response KVStoreHasResponse
if err := json.Unmarshal(responseBytes, &response); err != nil {
return nil, err
}
// Convert Error field to Go error
if response.Error != "" {
return nil, errors.New(response.Error)
}
return &response, nil
}
// KVStoreList calls the kvstore_list host function.
// List returns all keys matching the given prefix.
//
// Parameters:
// - prefix: Key prefix to filter by (empty string returns all keys)
//
// Returns a slice of matching keys.
func KVStoreList(prefix string) (*KVStoreListResponse, error) {
// Marshal request to JSON
req := KVStoreListRequest{
Prefix: prefix,
}
reqBytes, err := json.Marshal(req)
if err != nil {
return nil, err
}
reqMem := pdk.AllocateBytes(reqBytes)
defer reqMem.Free()
// Call the host function
responsePtr := kvstore_list(reqMem.Offset())
// Read the response from memory
responseMem := pdk.FindMemory(responsePtr)
responseBytes := responseMem.ReadBytes()
// Parse the response
var response KVStoreListResponse
if err := json.Unmarshal(responseBytes, &response); err != nil {
return nil, err
}
// Convert Error field to Go error
if response.Error != "" {
return nil, errors.New(response.Error)
}
return &response, nil
}
// KVStoreGetStorageUsed calls the kvstore_getstorageused host function.
// GetStorageUsed returns the total storage used by this plugin in bytes.
func KVStoreGetStorageUsed() (*KVStoreGetStorageUsedResponse, error) {
// No parameters - allocate empty JSON object
reqMem := pdk.AllocateBytes([]byte("{}"))
defer reqMem.Free()
// Call the host function
responsePtr := kvstore_getstorageused(reqMem.Offset())
// Read the response from memory
responseMem := pdk.FindMemory(responsePtr)
responseBytes := responseMem.ReadBytes()
// Parse the response
var response KVStoreGetStorageUsedResponse
if err := json.Unmarshal(responseBytes, &response); err != nil {
return nil, err
}
// Convert Error field to Go error
if response.Error != "" {
return nil, errors.New(response.Error)
}
return &response, nil
}

View File

@ -1,132 +0,0 @@
// Code generated by hostgen. DO NOT EDIT.
//
// This file contains stub implementations for non-WASM builds.
// These stubs allow IDE support and compilation on non-WASM platforms.
// They panic at runtime since host functions are only available in WASM plugins.
//
//go:build !wasip1
package ndhost
// KVStoreSetRequest is the request type for KVStore.Set.
type KVStoreSetRequest struct {
Key string `json:"key"`
Value []byte `json:"value"`
}
// KVStoreSetResponse is the response type for KVStore.Set.
type KVStoreSetResponse struct {
Error string `json:"error,omitempty"`
}
// KVStoreGetRequest is the request type for KVStore.Get.
type KVStoreGetRequest struct {
Key string `json:"key"`
}
// KVStoreGetResponse is the response type for KVStore.Get.
type KVStoreGetResponse struct {
Value []byte `json:"value,omitempty"`
Exists bool `json:"exists,omitempty"`
Error string `json:"error,omitempty"`
}
// KVStoreDeleteRequest is the request type for KVStore.Delete.
type KVStoreDeleteRequest struct {
Key string `json:"key"`
}
// KVStoreDeleteResponse is the response type for KVStore.Delete.
type KVStoreDeleteResponse struct {
Error string `json:"error,omitempty"`
}
// KVStoreHasRequest is the request type for KVStore.Has.
type KVStoreHasRequest struct {
Key string `json:"key"`
}
// KVStoreHasResponse is the response type for KVStore.Has.
type KVStoreHasResponse struct {
Exists bool `json:"exists,omitempty"`
Error string `json:"error,omitempty"`
}
// KVStoreListRequest is the request type for KVStore.List.
type KVStoreListRequest struct {
Prefix string `json:"prefix"`
}
// KVStoreListResponse is the response type for KVStore.List.
type KVStoreListResponse struct {
Keys []string `json:"keys,omitempty"`
Error string `json:"error,omitempty"`
}
// KVStoreGetStorageUsedResponse is the response type for KVStore.GetStorageUsed.
type KVStoreGetStorageUsedResponse struct {
Bytes int64 `json:"bytes,omitempty"`
Error string `json:"error,omitempty"`
}
// KVStoreSet is a stub that panics on non-WASM platforms.
// Set stores a byte value with the given key.
//
// Parameters:
// - key: The storage key (max 256 bytes, UTF-8)
// - value: The byte slice to store
//
// Returns an error if the storage limit would be exceeded or the operation fails.
func KVStoreSet(key string, value []byte) (*KVStoreSetResponse, error) {
panic("ndhost: KVStoreSet is only available in WASM plugins")
}
// KVStoreGet is a stub that panics on non-WASM platforms.
// Get retrieves a byte value from storage.
//
// Parameters:
// - key: The storage key
//
// Returns the value and whether the key exists.
func KVStoreGet(key string) (*KVStoreGetResponse, error) {
panic("ndhost: KVStoreGet is only available in WASM plugins")
}
// KVStoreDelete is a stub that panics on non-WASM platforms.
// Delete removes a value from storage.
//
// Parameters:
// - key: The storage key
//
// Returns an error if the operation fails. Does not return an error if the key doesn't exist.
func KVStoreDelete(key string) (*KVStoreDeleteResponse, error) {
panic("ndhost: KVStoreDelete is only available in WASM plugins")
}
// KVStoreHas is a stub that panics on non-WASM platforms.
// Has checks if a key exists in storage.
//
// Parameters:
// - key: The storage key
//
// Returns true if the key exists.
func KVStoreHas(key string) (*KVStoreHasResponse, error) {
panic("ndhost: KVStoreHas is only available in WASM plugins")
}
// KVStoreList is a stub that panics on non-WASM platforms.
// List returns all keys matching the given prefix.
//
// Parameters:
// - prefix: Key prefix to filter by (empty string returns all keys)
//
// Returns a slice of matching keys.
func KVStoreList(prefix string) (*KVStoreListResponse, error) {
panic("ndhost: KVStoreList is only available in WASM plugins")
}
// KVStoreGetStorageUsed is a stub that panics on non-WASM platforms.
// GetStorageUsed returns the total storage used by this plugin in bytes.
func KVStoreGetStorageUsed() (*KVStoreGetStorageUsedResponse, error) {
panic("ndhost: KVStoreGetStorageUsed is only available in WASM plugins")
}

View File

@ -1,127 +0,0 @@
// Code generated by hostgen. DO NOT EDIT.
//
// This file contains client wrappers for the Library host service.
// It is intended for use in Navidrome plugins built with TinyGo.
//
//go:build wasip1
package ndhost
import (
"encoding/json"
"errors"
"github.com/extism/go-pdk"
)
// Library represents the Library data structure.
// Library represents a music library with metadata.
type Library struct {
ID int32 `json:"id"`
Name string `json:"name"`
Path string `json:"path"`
MountPoint string `json:"mountPoint"`
LastScanAt int64 `json:"lastScanAt"`
TotalSongs int32 `json:"totalSongs"`
TotalAlbums int32 `json:"totalAlbums"`
TotalArtists int32 `json:"totalArtists"`
TotalSize int64 `json:"totalSize"`
TotalDuration float64 `json:"totalDuration"`
}
// library_getlibrary is the host function provided by Navidrome.
//
//go:wasmimport extism:host/user library_getlibrary
func library_getlibrary(uint64) uint64
// library_getalllibraries is the host function provided by Navidrome.
//
//go:wasmimport extism:host/user library_getalllibraries
func library_getalllibraries(uint64) uint64
// LibraryGetLibraryRequest is the request type for Library.GetLibrary.
type LibraryGetLibraryRequest struct {
Id int32 `json:"id"`
}
// LibraryGetLibraryResponse is the response type for Library.GetLibrary.
type LibraryGetLibraryResponse struct {
Result *Library `json:"result,omitempty"`
Error string `json:"error,omitempty"`
}
// LibraryGetAllLibrariesResponse is the response type for Library.GetAllLibraries.
type LibraryGetAllLibrariesResponse struct {
Result []Library `json:"result,omitempty"`
Error string `json:"error,omitempty"`
}
// LibraryGetLibrary calls the library_getlibrary host function.
// GetLibrary retrieves metadata for a specific library by ID.
//
// Parameters:
// - id: The library's unique identifier
//
// Returns the library metadata, or an error if the library is not found.
func LibraryGetLibrary(id int32) (*LibraryGetLibraryResponse, error) {
// Marshal request to JSON
req := LibraryGetLibraryRequest{
Id: id,
}
reqBytes, err := json.Marshal(req)
if err != nil {
return nil, err
}
reqMem := pdk.AllocateBytes(reqBytes)
defer reqMem.Free()
// Call the host function
responsePtr := library_getlibrary(reqMem.Offset())
// Read the response from memory
responseMem := pdk.FindMemory(responsePtr)
responseBytes := responseMem.ReadBytes()
// Parse the response
var response LibraryGetLibraryResponse
if err := json.Unmarshal(responseBytes, &response); err != nil {
return nil, err
}
// Convert Error field to Go error
if response.Error != "" {
return nil, errors.New(response.Error)
}
return &response, nil
}
// LibraryGetAllLibraries calls the library_getalllibraries host function.
// GetAllLibraries retrieves metadata for all configured libraries.
//
// Returns a slice of all libraries with their metadata.
func LibraryGetAllLibraries() (*LibraryGetAllLibrariesResponse, error) {
// No parameters - allocate empty JSON object
reqMem := pdk.AllocateBytes([]byte("{}"))
defer reqMem.Free()
// Call the host function
responsePtr := library_getalllibraries(reqMem.Offset())
// Read the response from memory
responseMem := pdk.FindMemory(responsePtr)
responseBytes := responseMem.ReadBytes()
// Parse the response
var response LibraryGetAllLibrariesResponse
if err := json.Unmarshal(responseBytes, &response); err != nil {
return nil, err
}
// Convert Error field to Go error
if response.Error != "" {
return nil, errors.New(response.Error)
}
return &response, nil
}

View File

@ -1,60 +0,0 @@
// Code generated by hostgen. DO NOT EDIT.
//
// This file contains stub implementations for non-WASM builds.
// These stubs allow IDE support and compilation on non-WASM platforms.
// They panic at runtime since host functions are only available in WASM plugins.
//
//go:build !wasip1
package ndhost
// Library represents the Library data structure.
// Library represents a music library with metadata.
type Library struct {
ID int32 `json:"id"`
Name string `json:"name"`
Path string `json:"path"`
MountPoint string `json:"mountPoint"`
LastScanAt int64 `json:"lastScanAt"`
TotalSongs int32 `json:"totalSongs"`
TotalAlbums int32 `json:"totalAlbums"`
TotalArtists int32 `json:"totalArtists"`
TotalSize int64 `json:"totalSize"`
TotalDuration float64 `json:"totalDuration"`
}
// LibraryGetLibraryRequest is the request type for Library.GetLibrary.
type LibraryGetLibraryRequest struct {
Id int32 `json:"id"`
}
// LibraryGetLibraryResponse is the response type for Library.GetLibrary.
type LibraryGetLibraryResponse struct {
Result *Library `json:"result,omitempty"`
Error string `json:"error,omitempty"`
}
// LibraryGetAllLibrariesResponse is the response type for Library.GetAllLibraries.
type LibraryGetAllLibrariesResponse struct {
Result []Library `json:"result,omitempty"`
Error string `json:"error,omitempty"`
}
// LibraryGetLibrary is a stub that panics on non-WASM platforms.
// GetLibrary retrieves metadata for a specific library by ID.
//
// Parameters:
// - id: The library's unique identifier
//
// Returns the library metadata, or an error if the library is not found.
func LibraryGetLibrary(id int32) (*LibraryGetLibraryResponse, error) {
panic("ndhost: LibraryGetLibrary is only available in WASM plugins")
}
// LibraryGetAllLibraries is a stub that panics on non-WASM platforms.
// GetAllLibraries retrieves metadata for all configured libraries.
//
// Returns a slice of all libraries with their metadata.
func LibraryGetAllLibraries() (*LibraryGetAllLibrariesResponse, error) {
panic("ndhost: LibraryGetAllLibraries is only available in WASM plugins")
}

View File

@ -1,196 +0,0 @@
// Code generated by hostgen. DO NOT EDIT.
//
// This file contains client wrappers for the Scheduler host service.
// It is intended for use in Navidrome plugins built with TinyGo.
//
//go:build wasip1
package ndhost
import (
"encoding/json"
"errors"
"github.com/extism/go-pdk"
)
// scheduler_scheduleonetime is the host function provided by Navidrome.
//
//go:wasmimport extism:host/user scheduler_scheduleonetime
func scheduler_scheduleonetime(uint64) uint64
// scheduler_schedulerecurring is the host function provided by Navidrome.
//
//go:wasmimport extism:host/user scheduler_schedulerecurring
func scheduler_schedulerecurring(uint64) uint64
// scheduler_cancelschedule is the host function provided by Navidrome.
//
//go:wasmimport extism:host/user scheduler_cancelschedule
func scheduler_cancelschedule(uint64) uint64
// SchedulerScheduleOneTimeRequest is the request type for Scheduler.ScheduleOneTime.
type SchedulerScheduleOneTimeRequest struct {
DelaySeconds int32 `json:"delaySeconds"`
Payload string `json:"payload"`
ScheduleID string `json:"scheduleId"`
}
// SchedulerScheduleOneTimeResponse is the response type for Scheduler.ScheduleOneTime.
type SchedulerScheduleOneTimeResponse struct {
NewScheduleID string `json:"newScheduleId,omitempty"`
Error string `json:"error,omitempty"`
}
// SchedulerScheduleRecurringRequest is the request type for Scheduler.ScheduleRecurring.
type SchedulerScheduleRecurringRequest struct {
CronExpression string `json:"cronExpression"`
Payload string `json:"payload"`
ScheduleID string `json:"scheduleId"`
}
// SchedulerScheduleRecurringResponse is the response type for Scheduler.ScheduleRecurring.
type SchedulerScheduleRecurringResponse struct {
NewScheduleID string `json:"newScheduleId,omitempty"`
Error string `json:"error,omitempty"`
}
// SchedulerCancelScheduleRequest is the request type for Scheduler.CancelSchedule.
type SchedulerCancelScheduleRequest struct {
ScheduleID string `json:"scheduleId"`
}
// SchedulerCancelScheduleResponse is the response type for Scheduler.CancelSchedule.
type SchedulerCancelScheduleResponse struct {
Error string `json:"error,omitempty"`
}
// SchedulerScheduleOneTime calls the scheduler_scheduleonetime host function.
// ScheduleOneTime schedules a one-time event to be triggered after the specified delay.
// Plugins that use this function must also implement the SchedulerCallback capability
//
// Parameters:
// - delaySeconds: Number of seconds to wait before triggering the event
// - payload: Data to be passed to the scheduled event handler
// - scheduleID: Optional unique identifier for the scheduled job. If empty, one will be generated
//
// Returns the schedule ID that can be used to cancel the job, or an error if scheduling fails.
func SchedulerScheduleOneTime(delaySeconds int32, payload string, scheduleID string) (*SchedulerScheduleOneTimeResponse, error) {
// Marshal request to JSON
req := SchedulerScheduleOneTimeRequest{
DelaySeconds: delaySeconds,
Payload: payload,
ScheduleID: scheduleID,
}
reqBytes, err := json.Marshal(req)
if err != nil {
return nil, err
}
reqMem := pdk.AllocateBytes(reqBytes)
defer reqMem.Free()
// Call the host function
responsePtr := scheduler_scheduleonetime(reqMem.Offset())
// Read the response from memory
responseMem := pdk.FindMemory(responsePtr)
responseBytes := responseMem.ReadBytes()
// Parse the response
var response SchedulerScheduleOneTimeResponse
if err := json.Unmarshal(responseBytes, &response); err != nil {
return nil, err
}
// Convert Error field to Go error
if response.Error != "" {
return nil, errors.New(response.Error)
}
return &response, nil
}
// SchedulerScheduleRecurring calls the scheduler_schedulerecurring host function.
// ScheduleRecurring schedules a recurring event using a cron expression.
// Plugins that use this function must also implement the SchedulerCallback capability
//
// Parameters:
// - cronExpression: Standard cron format expression (e.g., "0 0 * * *" for daily at midnight)
// - payload: Data to be passed to each scheduled event handler invocation
// - scheduleID: Optional unique identifier for the scheduled job. If empty, one will be generated
//
// Returns the schedule ID that can be used to cancel the job, or an error if scheduling fails.
func SchedulerScheduleRecurring(cronExpression string, payload string, scheduleID string) (*SchedulerScheduleRecurringResponse, error) {
// Marshal request to JSON
req := SchedulerScheduleRecurringRequest{
CronExpression: cronExpression,
Payload: payload,
ScheduleID: scheduleID,
}
reqBytes, err := json.Marshal(req)
if err != nil {
return nil, err
}
reqMem := pdk.AllocateBytes(reqBytes)
defer reqMem.Free()
// Call the host function
responsePtr := scheduler_schedulerecurring(reqMem.Offset())
// Read the response from memory
responseMem := pdk.FindMemory(responsePtr)
responseBytes := responseMem.ReadBytes()
// Parse the response
var response SchedulerScheduleRecurringResponse
if err := json.Unmarshal(responseBytes, &response); err != nil {
return nil, err
}
// Convert Error field to Go error
if response.Error != "" {
return nil, errors.New(response.Error)
}
return &response, nil
}
// SchedulerCancelSchedule calls the scheduler_cancelschedule host function.
// CancelSchedule cancels a scheduled job identified by its schedule ID.
//
// This works for both one-time and recurring schedules. Once cancelled, the job will not trigger
// any future events.
//
// Returns an error if the schedule ID is not found or if cancellation fails.
func SchedulerCancelSchedule(scheduleID string) (*SchedulerCancelScheduleResponse, error) {
// Marshal request to JSON
req := SchedulerCancelScheduleRequest{
ScheduleID: scheduleID,
}
reqBytes, err := json.Marshal(req)
if err != nil {
return nil, err
}
reqMem := pdk.AllocateBytes(reqBytes)
defer reqMem.Free()
// Call the host function
responsePtr := scheduler_cancelschedule(reqMem.Offset())
// Read the response from memory
responseMem := pdk.FindMemory(responsePtr)
responseBytes := responseMem.ReadBytes()
// Parse the response
var response SchedulerCancelScheduleResponse
if err := json.Unmarshal(responseBytes, &response); err != nil {
return nil, err
}
// Convert Error field to Go error
if response.Error != "" {
return nil, errors.New(response.Error)
}
return &response, nil
}

View File

@ -1,84 +0,0 @@
// Code generated by hostgen. DO NOT EDIT.
//
// This file contains stub implementations for non-WASM builds.
// These stubs allow IDE support and compilation on non-WASM platforms.
// They panic at runtime since host functions are only available in WASM plugins.
//
//go:build !wasip1
package ndhost
// SchedulerScheduleOneTimeRequest is the request type for Scheduler.ScheduleOneTime.
type SchedulerScheduleOneTimeRequest struct {
DelaySeconds int32 `json:"delaySeconds"`
Payload string `json:"payload"`
ScheduleID string `json:"scheduleId"`
}
// SchedulerScheduleOneTimeResponse is the response type for Scheduler.ScheduleOneTime.
type SchedulerScheduleOneTimeResponse struct {
NewScheduleID string `json:"newScheduleId,omitempty"`
Error string `json:"error,omitempty"`
}
// SchedulerScheduleRecurringRequest is the request type for Scheduler.ScheduleRecurring.
type SchedulerScheduleRecurringRequest struct {
CronExpression string `json:"cronExpression"`
Payload string `json:"payload"`
ScheduleID string `json:"scheduleId"`
}
// SchedulerScheduleRecurringResponse is the response type for Scheduler.ScheduleRecurring.
type SchedulerScheduleRecurringResponse struct {
NewScheduleID string `json:"newScheduleId,omitempty"`
Error string `json:"error,omitempty"`
}
// SchedulerCancelScheduleRequest is the request type for Scheduler.CancelSchedule.
type SchedulerCancelScheduleRequest struct {
ScheduleID string `json:"scheduleId"`
}
// SchedulerCancelScheduleResponse is the response type for Scheduler.CancelSchedule.
type SchedulerCancelScheduleResponse struct {
Error string `json:"error,omitempty"`
}
// SchedulerScheduleOneTime is a stub that panics on non-WASM platforms.
// ScheduleOneTime schedules a one-time event to be triggered after the specified delay.
// Plugins that use this function must also implement the SchedulerCallback capability
//
// Parameters:
// - delaySeconds: Number of seconds to wait before triggering the event
// - payload: Data to be passed to the scheduled event handler
// - scheduleID: Optional unique identifier for the scheduled job. If empty, one will be generated
//
// Returns the schedule ID that can be used to cancel the job, or an error if scheduling fails.
func SchedulerScheduleOneTime(delaySeconds int32, payload string, scheduleID string) (*SchedulerScheduleOneTimeResponse, error) {
panic("ndhost: SchedulerScheduleOneTime is only available in WASM plugins")
}
// SchedulerScheduleRecurring is a stub that panics on non-WASM platforms.
// ScheduleRecurring schedules a recurring event using a cron expression.
// Plugins that use this function must also implement the SchedulerCallback capability
//
// Parameters:
// - cronExpression: Standard cron format expression (e.g., "0 0 * * *" for daily at midnight)
// - payload: Data to be passed to each scheduled event handler invocation
// - scheduleID: Optional unique identifier for the scheduled job. If empty, one will be generated
//
// Returns the schedule ID that can be used to cancel the job, or an error if scheduling fails.
func SchedulerScheduleRecurring(cronExpression string, payload string, scheduleID string) (*SchedulerScheduleRecurringResponse, error) {
panic("ndhost: SchedulerScheduleRecurring is only available in WASM plugins")
}
// SchedulerCancelSchedule is a stub that panics on non-WASM platforms.
// CancelSchedule cancels a scheduled job identified by its schedule ID.
//
// This works for both one-time and recurring schedules. Once cancelled, the job will not trigger
// any future events.
//
// Returns an error if the schedule ID is not found or if cancellation fails.
func SchedulerCancelSchedule(scheduleID string) (*SchedulerCancelScheduleResponse, error) {
panic("ndhost: SchedulerCancelSchedule is only available in WASM plugins")
}

View File

@ -1,69 +0,0 @@
// Code generated by hostgen. DO NOT EDIT.
//
// This file contains client wrappers for the SubsonicAPI host service.
// It is intended for use in Navidrome plugins built with TinyGo.
//
//go:build wasip1
package ndhost
import (
"encoding/json"
"errors"
"github.com/extism/go-pdk"
)
// subsonicapi_call is the host function provided by Navidrome.
//
//go:wasmimport extism:host/user subsonicapi_call
func subsonicapi_call(uint64) uint64
// SubsonicAPICallRequest is the request type for SubsonicAPI.Call.
type SubsonicAPICallRequest struct {
Uri string `json:"uri"`
}
// SubsonicAPICallResponse is the response type for SubsonicAPI.Call.
type SubsonicAPICallResponse struct {
ResponseJSON string `json:"responseJson,omitempty"`
Error string `json:"error,omitempty"`
}
// SubsonicAPICall calls the subsonicapi_call host function.
// Call executes a Subsonic API request and returns the JSON response.
//
// The uri parameter should be the Subsonic API path without the server prefix,
// e.g., "getAlbumList2?type=random&size=10". The response is returned as raw JSON.
func SubsonicAPICall(uri string) (*SubsonicAPICallResponse, error) {
// Marshal request to JSON
req := SubsonicAPICallRequest{
Uri: uri,
}
reqBytes, err := json.Marshal(req)
if err != nil {
return nil, err
}
reqMem := pdk.AllocateBytes(reqBytes)
defer reqMem.Free()
// Call the host function
responsePtr := subsonicapi_call(reqMem.Offset())
// Read the response from memory
responseMem := pdk.FindMemory(responsePtr)
responseBytes := responseMem.ReadBytes()
// Parse the response
var response SubsonicAPICallResponse
if err := json.Unmarshal(responseBytes, &response); err != nil {
return nil, err
}
// Convert Error field to Go error
if response.Error != "" {
return nil, errors.New(response.Error)
}
return &response, nil
}

Some files were not shown because too many files have changed in this diff Show More