From 6d3c29912b656a99a7c0401669cd61520bdf24b3 Mon Sep 17 00:00:00 2001 From: Deluan Date: Mon, 29 Dec 2025 23:04:30 -0500 Subject: [PATCH] feat(plugins): implementation of the Navidrome Plugin Development Kit with generated client wrappers and service interfaces - Phase 2 (2) Signed-off-by: Deluan --- plugins/cmd/hostgen/README.md | 433 ----------- plugins/cmd/hostgen/integration_test.go | 597 --------------- plugins/cmd/hostgen/internal/generator.go | 301 -------- .../cmd/hostgen/internal/generator_test.go | 679 ------------------ .../hostgen/internal/internal_suite_test.go | 13 - plugins/cmd/hostgen/internal/parser.go | 474 ------------ plugins/cmd/hostgen/internal/parser_test.go | 292 -------- .../hostgen/internal/templates/client.go.tmpl | 108 --- .../hostgen/internal/templates/client.py.tmpl | 95 --- .../hostgen/internal/templates/client.rs.tmpl | 103 --- .../internal/templates/client_stub.go.tmpl | 56 -- .../hostgen/internal/templates/doc.go.tmpl | 49 -- .../hostgen/internal/templates/go.mod.tmpl | 5 - .../hostgen/internal/templates/host.go.tmpl | 119 --- .../hostgen/internal/templates/lib.rs.tmpl | 43 -- plugins/cmd/hostgen/internal/types.go | 384 ---------- plugins/cmd/hostgen/main.go | 455 ------------ plugins/cmd/ndpgen/integration_test.go | 35 +- plugins/cmd/ndpgen/internal/generator_test.go | 4 +- .../ndpgen/internal/templates/client.py.tmpl | 2 +- .../ndpgen/internal/templates/client.rs.tmpl | 2 +- .../ndpgen/internal/templates/host.go.tmpl | 2 +- .../cmd/ndpgen/internal/templates/lib.rs.tmpl | 4 +- .../testdata/codec_client_expected.go.txt} | 2 +- .../testdata/codec_client_expected.py | 2 +- .../testdata/codec_client_expected.rs | 2 +- .../testdata/codec_expected.go.txt} | 2 +- .../testdata/codec_service.go.txt} | 0 .../testdata/comprehensive_client_expected.py | 2 +- .../testdata/comprehensive_client_expected.rs | 2 +- .../testdata/comprehensive_service.go.txt} | 0 .../testdata/counter_client_expected.go.txt} | 2 +- .../testdata/counter_client_expected.py | 2 +- .../testdata/counter_client_expected.rs | 2 +- .../testdata/counter_expected.go.txt} | 2 +- .../testdata/counter_service.go.txt} | 0 .../testdata/echo_client_expected.go.txt} | 2 +- .../testdata/echo_client_expected.py | 2 +- .../testdata/echo_client_expected.rs | 2 +- .../testdata/echo_expected.go.txt} | 2 +- .../testdata/echo_service.go.txt} | 0 .../testdata/list_client_expected.go.txt} | 2 +- .../testdata/list_client_expected.py | 2 +- .../testdata/list_client_expected.rs | 2 +- .../testdata/list_expected.go.txt} | 2 +- .../testdata/list_service.go.txt} | 0 .../testdata/math_client_expected.go.txt} | 2 +- .../testdata/math_client_expected.py | 2 +- .../testdata/math_client_expected.rs | 2 +- .../testdata/math_expected.go.txt} | 2 +- .../testdata/math_service.go.txt} | 0 .../testdata/meta_client_expected.go.txt} | 2 +- .../testdata/meta_client_expected.py | 2 +- .../testdata/meta_client_expected.rs | 2 +- .../testdata/meta_expected.go.txt} | 2 +- .../testdata/meta_service.go.txt} | 0 .../testdata/ping_client_expected.go.txt} | 2 +- .../testdata/ping_client_expected.py | 2 +- .../testdata/ping_client_expected.rs | 2 +- .../testdata/ping_expected.go.txt} | 2 +- .../testdata/ping_service.go.txt} | 0 .../testdata/search_client_expected.go.txt} | 2 +- .../testdata/search_client_expected.py | 2 +- .../testdata/search_client_expected.rs | 2 +- .../testdata/search_expected.go.txt} | 2 +- .../testdata/search_service.go.txt} | 0 .../testdata/store_client_expected.go.txt} | 2 +- .../testdata/store_client_expected.py | 2 +- .../testdata/store_client_expected.rs | 2 +- .../testdata/store_expected.go.txt} | 2 +- .../testdata/store_service.go.txt} | 0 .../testdata/users_client_expected.go.txt} | 2 +- .../testdata/users_client_expected.py | 2 +- .../testdata/users_client_expected.rs | 2 +- .../testdata/users_expected.go.txt} | 2 +- .../testdata/users_service.go.txt} | 0 plugins/examples/crypto-ticker/go.mod | 4 +- plugins/examples/crypto-ticker/main.go | 10 +- .../discord-rich-presence-rs/Cargo.toml | 2 +- plugins/examples/discord-rich-presence/go.mod | 4 +- .../examples/discord-rich-presence/main.go | 8 +- plugins/examples/discord-rich-presence/rpc.go | 26 +- plugins/examples/library-inspector/Cargo.toml | 2 +- plugins/host/artwork_gen.go | 2 +- plugins/host/cache_gen.go | 2 +- plugins/host/doc.go | 4 +- plugins/host/go/doc.go | 54 -- plugins/host/go/go.mod | 5 - plugins/host/go/go.sum | 3 - plugins/host/go/nd_host_artwork.go | 251 ------- plugins/host/go/nd_host_artwork_stub.go | 105 --- plugins/host/go/nd_host_cache.go | 602 ---------------- plugins/host/go/nd_host_cache_stub.go | 248 ------- plugins/host/go/nd_host_kvstore.go | 336 --------- plugins/host/go/nd_host_kvstore_stub.go | 132 ---- plugins/host/go/nd_host_library.go | 127 ---- plugins/host/go/nd_host_library_stub.go | 60 -- plugins/host/go/nd_host_scheduler.go | 196 ----- plugins/host/go/nd_host_scheduler_stub.go | 84 --- plugins/host/go/nd_host_subsonicapi.go | 69 -- plugins/host/go/nd_host_subsonicapi_stub.go | 29 - plugins/host/go/nd_host_websocket.go | 258 ------- plugins/host/go/nd_host_websocket_stub.go | 110 --- plugins/host/kvstore_gen.go | 2 +- plugins/host/library_gen.go | 2 +- plugins/host/python/nd_host_artwork.py | 183 ----- plugins/host/python/nd_host_cache.py | 447 ------------ plugins/host/python/nd_host_kvstore.py | 241 ------- plugins/host/python/nd_host_library.py | 86 --- plugins/host/python/nd_host_scheduler.py | 143 ---- plugins/host/python/nd_host_subsonicapi.py | 55 -- plugins/host/python/nd_host_websocket.py | 181 ----- plugins/host/rust/lib.rs | 65 -- plugins/host/rust/nd_host_artwork.rs | 207 ------ plugins/host/rust/nd_host_cache.rs | 480 ------------- plugins/host/rust/nd_host_kvstore.rs | 261 ------- plugins/host/rust/nd_host_library.rs | 105 --- plugins/host/rust/nd_host_scheduler.rs | 159 ---- plugins/host/rust/nd_host_subsonicapi.rs | 54 -- plugins/host/rust/nd_host_websocket.rs | 204 ------ plugins/host/scheduler_gen.go | 2 +- plugins/host/subsonicapi_gen.go | 2 +- plugins/host/websocket_gen.go | 2 +- plugins/{host/go => pdk/go/host}/README.md | 21 +- plugins/pdk/go/host/go.sum | 1 + plugins/pdk/python/host/nd_host_artwork.py | 2 +- plugins/pdk/python/host/nd_host_cache.py | 2 +- plugins/pdk/python/host/nd_host_kvstore.py | 2 +- plugins/pdk/python/host/nd_host_library.py | 2 +- plugins/pdk/python/host/nd_host_scheduler.py | 2 +- .../pdk/python/host/nd_host_subsonicapi.py | 2 +- plugins/pdk/python/host/nd_host_websocket.py | 2 +- plugins/pdk/rust/host/README.md | 7 +- plugins/pdk/rust/host/lib.rs | 4 +- plugins/pdk/rust/host/nd_host_artwork.rs | 2 +- plugins/pdk/rust/host/nd_host_cache.rs | 2 +- plugins/pdk/rust/host/nd_host_kvstore.rs | 2 +- plugins/pdk/rust/host/nd_host_library.rs | 2 +- plugins/pdk/rust/host/nd_host_scheduler.rs | 2 +- plugins/pdk/rust/host/nd_host_subsonicapi.rs | 2 +- plugins/pdk/rust/host/nd_host_websocket.rs | 2 +- 141 files changed, 131 insertions(+), 9883 deletions(-) delete mode 100644 plugins/cmd/hostgen/README.md delete mode 100644 plugins/cmd/hostgen/integration_test.go delete mode 100644 plugins/cmd/hostgen/internal/generator.go delete mode 100644 plugins/cmd/hostgen/internal/generator_test.go delete mode 100644 plugins/cmd/hostgen/internal/internal_suite_test.go delete mode 100644 plugins/cmd/hostgen/internal/parser.go delete mode 100644 plugins/cmd/hostgen/internal/parser_test.go delete mode 100644 plugins/cmd/hostgen/internal/templates/client.go.tmpl delete mode 100644 plugins/cmd/hostgen/internal/templates/client.py.tmpl delete mode 100644 plugins/cmd/hostgen/internal/templates/client.rs.tmpl delete mode 100644 plugins/cmd/hostgen/internal/templates/client_stub.go.tmpl delete mode 100644 plugins/cmd/hostgen/internal/templates/doc.go.tmpl delete mode 100644 plugins/cmd/hostgen/internal/templates/go.mod.tmpl delete mode 100644 plugins/cmd/hostgen/internal/templates/host.go.tmpl delete mode 100644 plugins/cmd/hostgen/internal/templates/lib.rs.tmpl delete mode 100644 plugins/cmd/hostgen/internal/types.go delete mode 100644 plugins/cmd/hostgen/main.go rename plugins/cmd/{hostgen/testdata/codec_client_expected.go => ndpgen/testdata/codec_client_expected.go.txt} (97%) rename plugins/cmd/{hostgen => ndpgen}/testdata/codec_client_expected.py (97%) rename plugins/cmd/{hostgen => ndpgen}/testdata/codec_client_expected.rs (96%) rename plugins/cmd/{hostgen/testdata/codec_expected.go => ndpgen/testdata/codec_expected.go.txt} (98%) rename plugins/cmd/{hostgen/testdata/codec_service.go => ndpgen/testdata/codec_service.go.txt} (100%) rename plugins/cmd/{hostgen => ndpgen}/testdata/comprehensive_client_expected.py (99%) rename plugins/cmd/{hostgen => ndpgen}/testdata/comprehensive_client_expected.rs (99%) rename plugins/cmd/{hostgen/testdata/comprehensive_service.go => ndpgen/testdata/comprehensive_service.go.txt} (100%) rename plugins/cmd/{hostgen/testdata/counter_client_expected.go => ndpgen/testdata/counter_client_expected.go.txt} (97%) rename plugins/cmd/{hostgen => ndpgen}/testdata/counter_client_expected.py (97%) rename plugins/cmd/{hostgen => ndpgen}/testdata/counter_client_expected.rs (96%) rename plugins/cmd/{hostgen/testdata/counter_expected.go => ndpgen/testdata/counter_expected.go.txt} (98%) rename plugins/cmd/{hostgen/testdata/counter_service.go => ndpgen/testdata/counter_service.go.txt} (100%) rename plugins/cmd/{hostgen/testdata/echo_client_expected.go => ndpgen/testdata/echo_client_expected.go.txt} (97%) rename plugins/cmd/{hostgen => ndpgen}/testdata/echo_client_expected.py (97%) rename plugins/cmd/{hostgen => ndpgen}/testdata/echo_client_expected.rs (96%) rename plugins/cmd/{hostgen/testdata/echo_expected.go => ndpgen/testdata/echo_expected.go.txt} (98%) rename plugins/cmd/{hostgen/testdata/echo_service.go => ndpgen/testdata/echo_service.go.txt} (100%) rename plugins/cmd/{hostgen/testdata/list_client_expected.go => ndpgen/testdata/list_client_expected.go.txt} (97%) rename plugins/cmd/{hostgen => ndpgen}/testdata/list_client_expected.py (97%) rename plugins/cmd/{hostgen => ndpgen}/testdata/list_client_expected.rs (96%) rename plugins/cmd/{hostgen/testdata/list_expected.go => ndpgen/testdata/list_expected.go.txt} (98%) rename plugins/cmd/{hostgen/testdata/list_service.go => ndpgen/testdata/list_service.go.txt} (100%) rename plugins/cmd/{hostgen/testdata/math_client_expected.go => ndpgen/testdata/math_client_expected.go.txt} (97%) rename plugins/cmd/{hostgen => ndpgen}/testdata/math_client_expected.py (97%) rename plugins/cmd/{hostgen => ndpgen}/testdata/math_client_expected.rs (96%) rename plugins/cmd/{hostgen/testdata/math_expected.go => ndpgen/testdata/math_expected.go.txt} (98%) rename plugins/cmd/{hostgen/testdata/math_service.go => ndpgen/testdata/math_service.go.txt} (100%) rename plugins/cmd/{hostgen/testdata/meta_client_expected.go => ndpgen/testdata/meta_client_expected.go.txt} (98%) rename plugins/cmd/{hostgen => ndpgen}/testdata/meta_client_expected.py (98%) rename plugins/cmd/{hostgen => ndpgen}/testdata/meta_client_expected.rs (97%) rename plugins/cmd/{hostgen/testdata/meta_expected.go => ndpgen/testdata/meta_expected.go.txt} (98%) rename plugins/cmd/{hostgen/testdata/meta_service.go => ndpgen/testdata/meta_service.go.txt} (100%) rename plugins/cmd/{hostgen/testdata/ping_client_expected.go => ndpgen/testdata/ping_client_expected.go.txt} (96%) rename plugins/cmd/{hostgen => ndpgen}/testdata/ping_client_expected.py (96%) rename plugins/cmd/{hostgen => ndpgen}/testdata/ping_client_expected.rs (94%) rename plugins/cmd/{hostgen/testdata/ping_expected.go => ndpgen/testdata/ping_expected.go.txt} (97%) rename plugins/cmd/{hostgen/testdata/ping_service.go => ndpgen/testdata/ping_service.go.txt} (100%) rename plugins/cmd/{hostgen/testdata/search_client_expected.go => ndpgen/testdata/search_client_expected.go.txt} (97%) rename plugins/cmd/{hostgen => ndpgen}/testdata/search_client_expected.py (97%) rename plugins/cmd/{hostgen => ndpgen}/testdata/search_client_expected.rs (96%) rename plugins/cmd/{hostgen/testdata/search_expected.go => ndpgen/testdata/search_expected.go.txt} (98%) rename plugins/cmd/{hostgen/testdata/search_service.go => ndpgen/testdata/search_service.go.txt} (100%) rename plugins/cmd/{hostgen/testdata/store_client_expected.go => ndpgen/testdata/store_client_expected.go.txt} (97%) rename plugins/cmd/{hostgen => ndpgen}/testdata/store_client_expected.py (97%) rename plugins/cmd/{hostgen => ndpgen}/testdata/store_client_expected.rs (96%) rename plugins/cmd/{hostgen/testdata/store_expected.go => ndpgen/testdata/store_expected.go.txt} (98%) rename plugins/cmd/{hostgen/testdata/store_service.go => ndpgen/testdata/store_service.go.txt} (100%) rename plugins/cmd/{hostgen/testdata/users_client_expected.go => ndpgen/testdata/users_client_expected.go.txt} (97%) rename plugins/cmd/{hostgen => ndpgen}/testdata/users_client_expected.py (97%) rename plugins/cmd/{hostgen => ndpgen}/testdata/users_client_expected.rs (96%) rename plugins/cmd/{hostgen/testdata/users_expected.go => ndpgen/testdata/users_expected.go.txt} (98%) rename plugins/cmd/{hostgen/testdata/users_service.go => ndpgen/testdata/users_service.go.txt} (100%) delete mode 100644 plugins/host/go/doc.go delete mode 100644 plugins/host/go/go.mod delete mode 100644 plugins/host/go/go.sum delete mode 100644 plugins/host/go/nd_host_artwork.go delete mode 100644 plugins/host/go/nd_host_artwork_stub.go delete mode 100644 plugins/host/go/nd_host_cache.go delete mode 100644 plugins/host/go/nd_host_cache_stub.go delete mode 100644 plugins/host/go/nd_host_kvstore.go delete mode 100644 plugins/host/go/nd_host_kvstore_stub.go delete mode 100644 plugins/host/go/nd_host_library.go delete mode 100644 plugins/host/go/nd_host_library_stub.go delete mode 100644 plugins/host/go/nd_host_scheduler.go delete mode 100644 plugins/host/go/nd_host_scheduler_stub.go delete mode 100644 plugins/host/go/nd_host_subsonicapi.go delete mode 100644 plugins/host/go/nd_host_subsonicapi_stub.go delete mode 100644 plugins/host/go/nd_host_websocket.go delete mode 100644 plugins/host/go/nd_host_websocket_stub.go delete mode 100644 plugins/host/python/nd_host_artwork.py delete mode 100644 plugins/host/python/nd_host_cache.py delete mode 100644 plugins/host/python/nd_host_kvstore.py delete mode 100644 plugins/host/python/nd_host_library.py delete mode 100644 plugins/host/python/nd_host_scheduler.py delete mode 100644 plugins/host/python/nd_host_subsonicapi.py delete mode 100644 plugins/host/python/nd_host_websocket.py delete mode 100644 plugins/host/rust/lib.rs delete mode 100644 plugins/host/rust/nd_host_artwork.rs delete mode 100644 plugins/host/rust/nd_host_cache.rs delete mode 100644 plugins/host/rust/nd_host_kvstore.rs delete mode 100644 plugins/host/rust/nd_host_library.rs delete mode 100644 plugins/host/rust/nd_host_scheduler.rs delete mode 100644 plugins/host/rust/nd_host_subsonicapi.rs delete mode 100644 plugins/host/rust/nd_host_websocket.rs rename plugins/{host/go => pdk/go/host}/README.md (78%) create mode 100644 plugins/pdk/go/host/go.sum diff --git a/plugins/cmd/hostgen/README.md b/plugins/cmd/hostgen/README.md deleted file mode 100644 index 3b1f798af..000000000 --- a/plugins/cmd/hostgen/README.md +++ /dev/null @@ -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 -output -package [-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= 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=] -MethodName(ctx context.Context, ...) (result Type, err error) -``` - -| Parameter | Description | Required | -|-----------|-------------------------------------------------------------------------|----------| -| `name` | Custom export name (default: `_` 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 `_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_.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_.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_.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` 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` | | -| `[]T` | `Vec` | | -| `map[K]V` | `HashMap` | From `std::collections` | -| `*T` | `Option` | | -| `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 { - // 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 diff --git a/plugins/cmd/hostgen/integration_test.go b/plugins/cmd/hostgen/integration_test.go deleted file mode 100644 index a82e7162b..000000000 --- a/plugins/cmd/hostgen/integration_test.go +++ /dev/null @@ -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 - } -} diff --git a/plugins/cmd/hostgen/internal/generator.go b/plugins/cmd/hostgen/internal/generator.go deleted file mode 100644 index a7ad31ab3..000000000 --- a/plugins/cmd/hostgen/internal/generator.go +++ /dev/null @@ -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 -} diff --git a/plugins/cmd/hostgen/internal/generator_test.go b/plugins/cmd/hostgen/internal/generator_test.go deleted file mode 100644 index c2aebe49c..000000000 --- a/plugins/cmd/hostgen/internal/generator_test.go +++ /dev/null @@ -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) -} diff --git a/plugins/cmd/hostgen/internal/internal_suite_test.go b/plugins/cmd/hostgen/internal/internal_suite_test.go deleted file mode 100644 index 2e25efaef..000000000 --- a/plugins/cmd/hostgen/internal/internal_suite_test.go +++ /dev/null @@ -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") -} diff --git a/plugins/cmd/hostgen/internal/parser.go b/plugins/cmd/hostgen/internal/parser.go deleted file mode 100644 index 48ffdeb0e..000000000 --- a/plugins/cmd/hostgen/internal/parser.go +++ /dev/null @@ -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")) -} diff --git a/plugins/cmd/hostgen/internal/parser_test.go b/plugins/cmd/hostgen/internal/parser_test.go deleted file mode 100644 index 98c2cfb58..000000000 --- a/plugins/cmd/hostgen/internal/parser_test.go +++ /dev/null @@ -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")) - }) - }) -}) diff --git a/plugins/cmd/hostgen/internal/templates/client.go.tmpl b/plugins/cmd/hostgen/internal/templates/client.go.tmpl deleted file mode 100644 index 5cc53640a..000000000 --- a/plugins/cmd/hostgen/internal/templates/client.go.tmpl +++ /dev/null @@ -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}} diff --git a/plugins/cmd/hostgen/internal/templates/client.py.tmpl b/plugins/cmd/hostgen/internal/templates/client.py.tmpl deleted file mode 100644 index 4ad6ae01e..000000000 --- a/plugins/cmd/hostgen/internal/templates/client.py.tmpl +++ /dev/null @@ -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}} diff --git a/plugins/cmd/hostgen/internal/templates/client.rs.tmpl b/plugins/cmd/hostgen/internal/templates/client.rs.tmpl deleted file mode 100644 index 3bd86779d..000000000 --- a/plugins/cmd/hostgen/internal/templates/client.rs.tmpl +++ /dev/null @@ -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, -} -{{- 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}} diff --git a/plugins/cmd/hostgen/internal/templates/client_stub.go.tmpl b/plugins/cmd/hostgen/internal/templates/client_stub.go.tmpl deleted file mode 100644 index 69802e717..000000000 --- a/plugins/cmd/hostgen/internal/templates/client_stub.go.tmpl +++ /dev/null @@ -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}} diff --git a/plugins/cmd/hostgen/internal/templates/doc.go.tmpl b/plugins/cmd/hostgen/internal/templates/doc.go.tmpl deleted file mode 100644 index 2a1ecc4b2..000000000 --- a/plugins/cmd/hostgen/internal/templates/doc.go.tmpl +++ /dev/null @@ -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 diff --git a/plugins/cmd/hostgen/internal/templates/go.mod.tmpl b/plugins/cmd/hostgen/internal/templates/go.mod.tmpl deleted file mode 100644 index 954c98071..000000000 --- a/plugins/cmd/hostgen/internal/templates/go.mod.tmpl +++ /dev/null @@ -1,5 +0,0 @@ -module github.com/navidrome/navidrome/plugins/host/go - -go 1.24 - -require github.com/extism/go-pdk v1.1.3 diff --git a/plugins/cmd/hostgen/internal/templates/host.go.tmpl b/plugins/cmd/hostgen/internal/templates/host.go.tmpl deleted file mode 100644 index 4a961bd56..000000000 --- a/plugins/cmd/hostgen/internal/templates/host.go.tmpl +++ /dev/null @@ -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 -} diff --git a/plugins/cmd/hostgen/internal/templates/lib.rs.tmpl b/plugins/cmd/hostgen/internal/templates/lib.rs.tmpl deleted file mode 100644 index 7edbb0f62..000000000 --- a/plugins/cmd/hostgen/internal/templates/lib.rs.tmpl +++ /dev/null @@ -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; diff --git a/plugins/cmd/hostgen/internal/types.go b/plugins/cmd/hostgen/internal/types.go deleted file mode 100644 index af5ccfb6b..000000000 --- a/plugins/cmd/hostgen/internal/types.go +++ /dev/null @@ -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" - } - 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" - } -} diff --git a/plugins/cmd/hostgen/main.go b/plugins/cmd/hostgen/main.go deleted file mode 100644 index 5fc35569d..000000000 --- a/plugins/cmd/hostgen/main.go +++ /dev/null @@ -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 -} diff --git a/plugins/cmd/ndpgen/integration_test.go b/plugins/cmd/ndpgen/integration_test.go index 8c9d899bd..20093ef81 100644 --- a/plugins/cmd/ndpgen/integration_test.go +++ b/plugins/cmd/ndpgen/integration_test.go @@ -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:")) diff --git a/plugins/cmd/ndpgen/internal/generator_test.go b/plugins/cmd/ndpgen/internal/generator_test.go index 68dcedee9..8cf0a65ca 100644 --- a/plugins/cmd/ndpgen/internal/generator_test.go +++ b/plugins/cmd/ndpgen/internal/generator_test.go @@ -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")) diff --git a/plugins/cmd/ndpgen/internal/templates/client.py.tmpl b/plugins/cmd/ndpgen/internal/templates/client.py.tmpl index 4ad6ae01e..67053e134 100644 --- a/plugins/cmd/ndpgen/internal/templates/client.py.tmpl +++ b/plugins/cmd/ndpgen/internal/templates/client.py.tmpl @@ -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. diff --git a/plugins/cmd/ndpgen/internal/templates/client.rs.tmpl b/plugins/cmd/ndpgen/internal/templates/client.rs.tmpl index 3bd86779d..8fc01c1f3 100644 --- a/plugins/cmd/ndpgen/internal/templates/client.rs.tmpl +++ b/plugins/cmd/ndpgen/internal/templates/client.rs.tmpl @@ -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. diff --git a/plugins/cmd/ndpgen/internal/templates/host.go.tmpl b/plugins/cmd/ndpgen/internal/templates/host.go.tmpl index 4a961bd56..39e6ad0ce 100644 --- a/plugins/cmd/ndpgen/internal/templates/host.go.tmpl +++ b/plugins/cmd/ndpgen/internal/templates/host.go.tmpl @@ -1,4 +1,4 @@ -// Code generated by hostgen. DO NOT EDIT. +// Code generated by ndpgen. DO NOT EDIT. package {{.Package}} diff --git a/plugins/cmd/ndpgen/internal/templates/lib.rs.tmpl b/plugins/cmd/ndpgen/internal/templates/lib.rs.tmpl index 7edbb0f62..50f2603bf 100644 --- a/plugins/cmd/ndpgen/internal/templates/lib.rs.tmpl +++ b/plugins/cmd/ndpgen/internal/templates/lib.rs.tmpl @@ -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 //! diff --git a/plugins/cmd/hostgen/testdata/codec_client_expected.go b/plugins/cmd/ndpgen/testdata/codec_client_expected.go.txt similarity index 97% rename from plugins/cmd/hostgen/testdata/codec_client_expected.go rename to plugins/cmd/ndpgen/testdata/codec_client_expected.go.txt index 4ae8795d8..04a349ad4 100644 --- a/plugins/cmd/hostgen/testdata/codec_client_expected.go +++ b/plugins/cmd/ndpgen/testdata/codec_client_expected.go.txt @@ -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. diff --git a/plugins/cmd/hostgen/testdata/codec_client_expected.py b/plugins/cmd/ndpgen/testdata/codec_client_expected.py similarity index 97% rename from plugins/cmd/hostgen/testdata/codec_client_expected.py rename to plugins/cmd/ndpgen/testdata/codec_client_expected.py index 5521f1bbf..e1eb92501 100644 --- a/plugins/cmd/hostgen/testdata/codec_client_expected.py +++ b/plugins/cmd/ndpgen/testdata/codec_client_expected.py @@ -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. diff --git a/plugins/cmd/hostgen/testdata/codec_client_expected.rs b/plugins/cmd/ndpgen/testdata/codec_client_expected.rs similarity index 96% rename from plugins/cmd/hostgen/testdata/codec_client_expected.rs rename to plugins/cmd/ndpgen/testdata/codec_client_expected.rs index c4b5e7e7c..ff61294d0 100644 --- a/plugins/cmd/hostgen/testdata/codec_client_expected.rs +++ b/plugins/cmd/ndpgen/testdata/codec_client_expected.rs @@ -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. diff --git a/plugins/cmd/hostgen/testdata/codec_expected.go b/plugins/cmd/ndpgen/testdata/codec_expected.go.txt similarity index 98% rename from plugins/cmd/hostgen/testdata/codec_expected.go rename to plugins/cmd/ndpgen/testdata/codec_expected.go.txt index 28eb22139..8655e49c8 100644 --- a/plugins/cmd/hostgen/testdata/codec_expected.go +++ b/plugins/cmd/ndpgen/testdata/codec_expected.go.txt @@ -1,4 +1,4 @@ -// Code generated by hostgen. DO NOT EDIT. +// Code generated by ndpgen. DO NOT EDIT. package testpkg diff --git a/plugins/cmd/hostgen/testdata/codec_service.go b/plugins/cmd/ndpgen/testdata/codec_service.go.txt similarity index 100% rename from plugins/cmd/hostgen/testdata/codec_service.go rename to plugins/cmd/ndpgen/testdata/codec_service.go.txt diff --git a/plugins/cmd/hostgen/testdata/comprehensive_client_expected.py b/plugins/cmd/ndpgen/testdata/comprehensive_client_expected.py similarity index 99% rename from plugins/cmd/hostgen/testdata/comprehensive_client_expected.py rename to plugins/cmd/ndpgen/testdata/comprehensive_client_expected.py index bcb6e2f12..0fdbfb0f2 100644 --- a/plugins/cmd/hostgen/testdata/comprehensive_client_expected.py +++ b/plugins/cmd/ndpgen/testdata/comprehensive_client_expected.py @@ -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. diff --git a/plugins/cmd/hostgen/testdata/comprehensive_client_expected.rs b/plugins/cmd/ndpgen/testdata/comprehensive_client_expected.rs similarity index 99% rename from plugins/cmd/hostgen/testdata/comprehensive_client_expected.rs rename to plugins/cmd/ndpgen/testdata/comprehensive_client_expected.rs index a78d17609..efcc1b8ef 100644 --- a/plugins/cmd/hostgen/testdata/comprehensive_client_expected.rs +++ b/plugins/cmd/ndpgen/testdata/comprehensive_client_expected.rs @@ -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. diff --git a/plugins/cmd/hostgen/testdata/comprehensive_service.go b/plugins/cmd/ndpgen/testdata/comprehensive_service.go.txt similarity index 100% rename from plugins/cmd/hostgen/testdata/comprehensive_service.go rename to plugins/cmd/ndpgen/testdata/comprehensive_service.go.txt diff --git a/plugins/cmd/hostgen/testdata/counter_client_expected.go b/plugins/cmd/ndpgen/testdata/counter_client_expected.go.txt similarity index 97% rename from plugins/cmd/hostgen/testdata/counter_client_expected.go rename to plugins/cmd/ndpgen/testdata/counter_client_expected.go.txt index ee9bea22a..73ca80085 100644 --- a/plugins/cmd/hostgen/testdata/counter_client_expected.go +++ b/plugins/cmd/ndpgen/testdata/counter_client_expected.go.txt @@ -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. diff --git a/plugins/cmd/hostgen/testdata/counter_client_expected.py b/plugins/cmd/ndpgen/testdata/counter_client_expected.py similarity index 97% rename from plugins/cmd/hostgen/testdata/counter_client_expected.py rename to plugins/cmd/ndpgen/testdata/counter_client_expected.py index 6bdbc61dc..2af5b3b38 100644 --- a/plugins/cmd/hostgen/testdata/counter_client_expected.py +++ b/plugins/cmd/ndpgen/testdata/counter_client_expected.py @@ -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. diff --git a/plugins/cmd/hostgen/testdata/counter_client_expected.rs b/plugins/cmd/ndpgen/testdata/counter_client_expected.rs similarity index 96% rename from plugins/cmd/hostgen/testdata/counter_client_expected.rs rename to plugins/cmd/ndpgen/testdata/counter_client_expected.rs index 11842689c..37b8f8605 100644 --- a/plugins/cmd/hostgen/testdata/counter_client_expected.rs +++ b/plugins/cmd/ndpgen/testdata/counter_client_expected.rs @@ -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. diff --git a/plugins/cmd/hostgen/testdata/counter_expected.go b/plugins/cmd/ndpgen/testdata/counter_expected.go.txt similarity index 98% rename from plugins/cmd/hostgen/testdata/counter_expected.go rename to plugins/cmd/ndpgen/testdata/counter_expected.go.txt index 036ff4523..f15f3c2a7 100644 --- a/plugins/cmd/hostgen/testdata/counter_expected.go +++ b/plugins/cmd/ndpgen/testdata/counter_expected.go.txt @@ -1,4 +1,4 @@ -// Code generated by hostgen. DO NOT EDIT. +// Code generated by ndpgen. DO NOT EDIT. package testpkg diff --git a/plugins/cmd/hostgen/testdata/counter_service.go b/plugins/cmd/ndpgen/testdata/counter_service.go.txt similarity index 100% rename from plugins/cmd/hostgen/testdata/counter_service.go rename to plugins/cmd/ndpgen/testdata/counter_service.go.txt diff --git a/plugins/cmd/hostgen/testdata/echo_client_expected.go b/plugins/cmd/ndpgen/testdata/echo_client_expected.go.txt similarity index 97% rename from plugins/cmd/hostgen/testdata/echo_client_expected.go rename to plugins/cmd/ndpgen/testdata/echo_client_expected.go.txt index 6adbe2bb6..4858488bf 100644 --- a/plugins/cmd/hostgen/testdata/echo_client_expected.go +++ b/plugins/cmd/ndpgen/testdata/echo_client_expected.go.txt @@ -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. diff --git a/plugins/cmd/hostgen/testdata/echo_client_expected.py b/plugins/cmd/ndpgen/testdata/echo_client_expected.py similarity index 97% rename from plugins/cmd/hostgen/testdata/echo_client_expected.py rename to plugins/cmd/ndpgen/testdata/echo_client_expected.py index d3144c347..06565b0d6 100644 --- a/plugins/cmd/hostgen/testdata/echo_client_expected.py +++ b/plugins/cmd/ndpgen/testdata/echo_client_expected.py @@ -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. diff --git a/plugins/cmd/hostgen/testdata/echo_client_expected.rs b/plugins/cmd/ndpgen/testdata/echo_client_expected.rs similarity index 96% rename from plugins/cmd/hostgen/testdata/echo_client_expected.rs rename to plugins/cmd/ndpgen/testdata/echo_client_expected.rs index 4c4fd672a..0b97ca1cd 100644 --- a/plugins/cmd/hostgen/testdata/echo_client_expected.rs +++ b/plugins/cmd/ndpgen/testdata/echo_client_expected.rs @@ -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. diff --git a/plugins/cmd/hostgen/testdata/echo_expected.go b/plugins/cmd/ndpgen/testdata/echo_expected.go.txt similarity index 98% rename from plugins/cmd/hostgen/testdata/echo_expected.go rename to plugins/cmd/ndpgen/testdata/echo_expected.go.txt index da0dc931e..d67854abf 100644 --- a/plugins/cmd/hostgen/testdata/echo_expected.go +++ b/plugins/cmd/ndpgen/testdata/echo_expected.go.txt @@ -1,4 +1,4 @@ -// Code generated by hostgen. DO NOT EDIT. +// Code generated by ndpgen. DO NOT EDIT. package testpkg diff --git a/plugins/cmd/hostgen/testdata/echo_service.go b/plugins/cmd/ndpgen/testdata/echo_service.go.txt similarity index 100% rename from plugins/cmd/hostgen/testdata/echo_service.go rename to plugins/cmd/ndpgen/testdata/echo_service.go.txt diff --git a/plugins/cmd/hostgen/testdata/list_client_expected.go b/plugins/cmd/ndpgen/testdata/list_client_expected.go.txt similarity index 97% rename from plugins/cmd/hostgen/testdata/list_client_expected.go rename to plugins/cmd/ndpgen/testdata/list_client_expected.go.txt index 3bf4244a9..b0f76a013 100644 --- a/plugins/cmd/hostgen/testdata/list_client_expected.go +++ b/plugins/cmd/ndpgen/testdata/list_client_expected.go.txt @@ -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. diff --git a/plugins/cmd/hostgen/testdata/list_client_expected.py b/plugins/cmd/ndpgen/testdata/list_client_expected.py similarity index 97% rename from plugins/cmd/hostgen/testdata/list_client_expected.py rename to plugins/cmd/ndpgen/testdata/list_client_expected.py index 3a19ac476..58ccad146 100644 --- a/plugins/cmd/hostgen/testdata/list_client_expected.py +++ b/plugins/cmd/ndpgen/testdata/list_client_expected.py @@ -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. diff --git a/plugins/cmd/hostgen/testdata/list_client_expected.rs b/plugins/cmd/ndpgen/testdata/list_client_expected.rs similarity index 96% rename from plugins/cmd/hostgen/testdata/list_client_expected.rs rename to plugins/cmd/ndpgen/testdata/list_client_expected.rs index ec3ec2d57..9b54f7544 100644 --- a/plugins/cmd/hostgen/testdata/list_client_expected.rs +++ b/plugins/cmd/ndpgen/testdata/list_client_expected.rs @@ -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. diff --git a/plugins/cmd/hostgen/testdata/list_expected.go b/plugins/cmd/ndpgen/testdata/list_expected.go.txt similarity index 98% rename from plugins/cmd/hostgen/testdata/list_expected.go rename to plugins/cmd/ndpgen/testdata/list_expected.go.txt index 8a7dee4e7..778f3a409 100644 --- a/plugins/cmd/hostgen/testdata/list_expected.go +++ b/plugins/cmd/ndpgen/testdata/list_expected.go.txt @@ -1,4 +1,4 @@ -// Code generated by hostgen. DO NOT EDIT. +// Code generated by ndpgen. DO NOT EDIT. package testpkg diff --git a/plugins/cmd/hostgen/testdata/list_service.go b/plugins/cmd/ndpgen/testdata/list_service.go.txt similarity index 100% rename from plugins/cmd/hostgen/testdata/list_service.go rename to plugins/cmd/ndpgen/testdata/list_service.go.txt diff --git a/plugins/cmd/hostgen/testdata/math_client_expected.go b/plugins/cmd/ndpgen/testdata/math_client_expected.go.txt similarity index 97% rename from plugins/cmd/hostgen/testdata/math_client_expected.go rename to plugins/cmd/ndpgen/testdata/math_client_expected.go.txt index f7b30579b..455cc8f41 100644 --- a/plugins/cmd/hostgen/testdata/math_client_expected.go +++ b/plugins/cmd/ndpgen/testdata/math_client_expected.go.txt @@ -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. diff --git a/plugins/cmd/hostgen/testdata/math_client_expected.py b/plugins/cmd/ndpgen/testdata/math_client_expected.py similarity index 97% rename from plugins/cmd/hostgen/testdata/math_client_expected.py rename to plugins/cmd/ndpgen/testdata/math_client_expected.py index e714fbc02..f3ea53335 100644 --- a/plugins/cmd/hostgen/testdata/math_client_expected.py +++ b/plugins/cmd/ndpgen/testdata/math_client_expected.py @@ -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. diff --git a/plugins/cmd/hostgen/testdata/math_client_expected.rs b/plugins/cmd/ndpgen/testdata/math_client_expected.rs similarity index 96% rename from plugins/cmd/hostgen/testdata/math_client_expected.rs rename to plugins/cmd/ndpgen/testdata/math_client_expected.rs index 4bfa3bd12..fde6a7cb9 100644 --- a/plugins/cmd/hostgen/testdata/math_client_expected.rs +++ b/plugins/cmd/ndpgen/testdata/math_client_expected.rs @@ -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. diff --git a/plugins/cmd/hostgen/testdata/math_expected.go b/plugins/cmd/ndpgen/testdata/math_expected.go.txt similarity index 98% rename from plugins/cmd/hostgen/testdata/math_expected.go rename to plugins/cmd/ndpgen/testdata/math_expected.go.txt index 6fc55aeef..48a2bf875 100644 --- a/plugins/cmd/hostgen/testdata/math_expected.go +++ b/plugins/cmd/ndpgen/testdata/math_expected.go.txt @@ -1,4 +1,4 @@ -// Code generated by hostgen. DO NOT EDIT. +// Code generated by ndpgen. DO NOT EDIT. package testpkg diff --git a/plugins/cmd/hostgen/testdata/math_service.go b/plugins/cmd/ndpgen/testdata/math_service.go.txt similarity index 100% rename from plugins/cmd/hostgen/testdata/math_service.go rename to plugins/cmd/ndpgen/testdata/math_service.go.txt diff --git a/plugins/cmd/hostgen/testdata/meta_client_expected.go b/plugins/cmd/ndpgen/testdata/meta_client_expected.go.txt similarity index 98% rename from plugins/cmd/hostgen/testdata/meta_client_expected.go rename to plugins/cmd/ndpgen/testdata/meta_client_expected.go.txt index fb61c327e..40f4d7ecf 100644 --- a/plugins/cmd/hostgen/testdata/meta_client_expected.go +++ b/plugins/cmd/ndpgen/testdata/meta_client_expected.go.txt @@ -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. diff --git a/plugins/cmd/hostgen/testdata/meta_client_expected.py b/plugins/cmd/ndpgen/testdata/meta_client_expected.py similarity index 98% rename from plugins/cmd/hostgen/testdata/meta_client_expected.py rename to plugins/cmd/ndpgen/testdata/meta_client_expected.py index e132fb334..4d20c73ff 100644 --- a/plugins/cmd/hostgen/testdata/meta_client_expected.py +++ b/plugins/cmd/ndpgen/testdata/meta_client_expected.py @@ -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. diff --git a/plugins/cmd/hostgen/testdata/meta_client_expected.rs b/plugins/cmd/ndpgen/testdata/meta_client_expected.rs similarity index 97% rename from plugins/cmd/hostgen/testdata/meta_client_expected.rs rename to plugins/cmd/ndpgen/testdata/meta_client_expected.rs index 807b4895d..79b95ffb3 100644 --- a/plugins/cmd/hostgen/testdata/meta_client_expected.rs +++ b/plugins/cmd/ndpgen/testdata/meta_client_expected.rs @@ -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. diff --git a/plugins/cmd/hostgen/testdata/meta_expected.go b/plugins/cmd/ndpgen/testdata/meta_expected.go.txt similarity index 98% rename from plugins/cmd/hostgen/testdata/meta_expected.go rename to plugins/cmd/ndpgen/testdata/meta_expected.go.txt index cea15a200..6f660fd65 100644 --- a/plugins/cmd/hostgen/testdata/meta_expected.go +++ b/plugins/cmd/ndpgen/testdata/meta_expected.go.txt @@ -1,4 +1,4 @@ -// Code generated by hostgen. DO NOT EDIT. +// Code generated by ndpgen. DO NOT EDIT. package testpkg diff --git a/plugins/cmd/hostgen/testdata/meta_service.go b/plugins/cmd/ndpgen/testdata/meta_service.go.txt similarity index 100% rename from plugins/cmd/hostgen/testdata/meta_service.go rename to plugins/cmd/ndpgen/testdata/meta_service.go.txt diff --git a/plugins/cmd/hostgen/testdata/ping_client_expected.go b/plugins/cmd/ndpgen/testdata/ping_client_expected.go.txt similarity index 96% rename from plugins/cmd/hostgen/testdata/ping_client_expected.go rename to plugins/cmd/ndpgen/testdata/ping_client_expected.go.txt index 6adc4c798..e94e7e735 100644 --- a/plugins/cmd/hostgen/testdata/ping_client_expected.go +++ b/plugins/cmd/ndpgen/testdata/ping_client_expected.go.txt @@ -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. diff --git a/plugins/cmd/hostgen/testdata/ping_client_expected.py b/plugins/cmd/ndpgen/testdata/ping_client_expected.py similarity index 96% rename from plugins/cmd/hostgen/testdata/ping_client_expected.py rename to plugins/cmd/ndpgen/testdata/ping_client_expected.py index 492cc436c..4c7d41d8e 100644 --- a/plugins/cmd/hostgen/testdata/ping_client_expected.py +++ b/plugins/cmd/ndpgen/testdata/ping_client_expected.py @@ -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. diff --git a/plugins/cmd/hostgen/testdata/ping_client_expected.rs b/plugins/cmd/ndpgen/testdata/ping_client_expected.rs similarity index 94% rename from plugins/cmd/hostgen/testdata/ping_client_expected.rs rename to plugins/cmd/ndpgen/testdata/ping_client_expected.rs index 0b5e11eba..a40f10843 100644 --- a/plugins/cmd/hostgen/testdata/ping_client_expected.rs +++ b/plugins/cmd/ndpgen/testdata/ping_client_expected.rs @@ -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. diff --git a/plugins/cmd/hostgen/testdata/ping_expected.go b/plugins/cmd/ndpgen/testdata/ping_expected.go.txt similarity index 97% rename from plugins/cmd/hostgen/testdata/ping_expected.go rename to plugins/cmd/ndpgen/testdata/ping_expected.go.txt index dff52b03e..0b0253817 100644 --- a/plugins/cmd/hostgen/testdata/ping_expected.go +++ b/plugins/cmd/ndpgen/testdata/ping_expected.go.txt @@ -1,4 +1,4 @@ -// Code generated by hostgen. DO NOT EDIT. +// Code generated by ndpgen. DO NOT EDIT. package testpkg diff --git a/plugins/cmd/hostgen/testdata/ping_service.go b/plugins/cmd/ndpgen/testdata/ping_service.go.txt similarity index 100% rename from plugins/cmd/hostgen/testdata/ping_service.go rename to plugins/cmd/ndpgen/testdata/ping_service.go.txt diff --git a/plugins/cmd/hostgen/testdata/search_client_expected.go b/plugins/cmd/ndpgen/testdata/search_client_expected.go.txt similarity index 97% rename from plugins/cmd/hostgen/testdata/search_client_expected.go rename to plugins/cmd/ndpgen/testdata/search_client_expected.go.txt index 7e311856a..c4d707bef 100644 --- a/plugins/cmd/hostgen/testdata/search_client_expected.go +++ b/plugins/cmd/ndpgen/testdata/search_client_expected.go.txt @@ -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. diff --git a/plugins/cmd/hostgen/testdata/search_client_expected.py b/plugins/cmd/ndpgen/testdata/search_client_expected.py similarity index 97% rename from plugins/cmd/hostgen/testdata/search_client_expected.py rename to plugins/cmd/ndpgen/testdata/search_client_expected.py index 87cb3f9cd..aa2e98a36 100644 --- a/plugins/cmd/hostgen/testdata/search_client_expected.py +++ b/plugins/cmd/ndpgen/testdata/search_client_expected.py @@ -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. diff --git a/plugins/cmd/hostgen/testdata/search_client_expected.rs b/plugins/cmd/ndpgen/testdata/search_client_expected.rs similarity index 96% rename from plugins/cmd/hostgen/testdata/search_client_expected.rs rename to plugins/cmd/ndpgen/testdata/search_client_expected.rs index a6fa1bfc1..b0ab2505a 100644 --- a/plugins/cmd/hostgen/testdata/search_client_expected.rs +++ b/plugins/cmd/ndpgen/testdata/search_client_expected.rs @@ -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. diff --git a/plugins/cmd/hostgen/testdata/search_expected.go b/plugins/cmd/ndpgen/testdata/search_expected.go.txt similarity index 98% rename from plugins/cmd/hostgen/testdata/search_expected.go rename to plugins/cmd/ndpgen/testdata/search_expected.go.txt index 17f634085..6c316266f 100644 --- a/plugins/cmd/hostgen/testdata/search_expected.go +++ b/plugins/cmd/ndpgen/testdata/search_expected.go.txt @@ -1,4 +1,4 @@ -// Code generated by hostgen. DO NOT EDIT. +// Code generated by ndpgen. DO NOT EDIT. package testpkg diff --git a/plugins/cmd/hostgen/testdata/search_service.go b/plugins/cmd/ndpgen/testdata/search_service.go.txt similarity index 100% rename from plugins/cmd/hostgen/testdata/search_service.go rename to plugins/cmd/ndpgen/testdata/search_service.go.txt diff --git a/plugins/cmd/hostgen/testdata/store_client_expected.go b/plugins/cmd/ndpgen/testdata/store_client_expected.go.txt similarity index 97% rename from plugins/cmd/hostgen/testdata/store_client_expected.go rename to plugins/cmd/ndpgen/testdata/store_client_expected.go.txt index 153813e95..eec5156d6 100644 --- a/plugins/cmd/hostgen/testdata/store_client_expected.go +++ b/plugins/cmd/ndpgen/testdata/store_client_expected.go.txt @@ -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. diff --git a/plugins/cmd/hostgen/testdata/store_client_expected.py b/plugins/cmd/ndpgen/testdata/store_client_expected.py similarity index 97% rename from plugins/cmd/hostgen/testdata/store_client_expected.py rename to plugins/cmd/ndpgen/testdata/store_client_expected.py index 5f1e6a2b0..4a964a497 100644 --- a/plugins/cmd/hostgen/testdata/store_client_expected.py +++ b/plugins/cmd/ndpgen/testdata/store_client_expected.py @@ -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. diff --git a/plugins/cmd/hostgen/testdata/store_client_expected.rs b/plugins/cmd/ndpgen/testdata/store_client_expected.rs similarity index 96% rename from plugins/cmd/hostgen/testdata/store_client_expected.rs rename to plugins/cmd/ndpgen/testdata/store_client_expected.rs index 5ad8cdafe..25d2af2e0 100644 --- a/plugins/cmd/hostgen/testdata/store_client_expected.rs +++ b/plugins/cmd/ndpgen/testdata/store_client_expected.rs @@ -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. diff --git a/plugins/cmd/hostgen/testdata/store_expected.go b/plugins/cmd/ndpgen/testdata/store_expected.go.txt similarity index 98% rename from plugins/cmd/hostgen/testdata/store_expected.go rename to plugins/cmd/ndpgen/testdata/store_expected.go.txt index 9242c8c4b..07537ca41 100644 --- a/plugins/cmd/hostgen/testdata/store_expected.go +++ b/plugins/cmd/ndpgen/testdata/store_expected.go.txt @@ -1,4 +1,4 @@ -// Code generated by hostgen. DO NOT EDIT. +// Code generated by ndpgen. DO NOT EDIT. package testpkg diff --git a/plugins/cmd/hostgen/testdata/store_service.go b/plugins/cmd/ndpgen/testdata/store_service.go.txt similarity index 100% rename from plugins/cmd/hostgen/testdata/store_service.go rename to plugins/cmd/ndpgen/testdata/store_service.go.txt diff --git a/plugins/cmd/hostgen/testdata/users_client_expected.go b/plugins/cmd/ndpgen/testdata/users_client_expected.go.txt similarity index 97% rename from plugins/cmd/hostgen/testdata/users_client_expected.go rename to plugins/cmd/ndpgen/testdata/users_client_expected.go.txt index 9f61bc447..43769d890 100644 --- a/plugins/cmd/hostgen/testdata/users_client_expected.go +++ b/plugins/cmd/ndpgen/testdata/users_client_expected.go.txt @@ -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. diff --git a/plugins/cmd/hostgen/testdata/users_client_expected.py b/plugins/cmd/ndpgen/testdata/users_client_expected.py similarity index 97% rename from plugins/cmd/hostgen/testdata/users_client_expected.py rename to plugins/cmd/ndpgen/testdata/users_client_expected.py index c89abb134..468b87b98 100644 --- a/plugins/cmd/hostgen/testdata/users_client_expected.py +++ b/plugins/cmd/ndpgen/testdata/users_client_expected.py @@ -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. diff --git a/plugins/cmd/hostgen/testdata/users_client_expected.rs b/plugins/cmd/ndpgen/testdata/users_client_expected.rs similarity index 96% rename from plugins/cmd/hostgen/testdata/users_client_expected.rs rename to plugins/cmd/ndpgen/testdata/users_client_expected.rs index 081db0356..40daa9cfd 100644 --- a/plugins/cmd/hostgen/testdata/users_client_expected.rs +++ b/plugins/cmd/ndpgen/testdata/users_client_expected.rs @@ -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. diff --git a/plugins/cmd/hostgen/testdata/users_expected.go b/plugins/cmd/ndpgen/testdata/users_expected.go.txt similarity index 98% rename from plugins/cmd/hostgen/testdata/users_expected.go rename to plugins/cmd/ndpgen/testdata/users_expected.go.txt index 417252426..1b0fbfa93 100644 --- a/plugins/cmd/hostgen/testdata/users_expected.go +++ b/plugins/cmd/ndpgen/testdata/users_expected.go.txt @@ -1,4 +1,4 @@ -// Code generated by hostgen. DO NOT EDIT. +// Code generated by ndpgen. DO NOT EDIT. package testpkg diff --git a/plugins/cmd/hostgen/testdata/users_service.go b/plugins/cmd/ndpgen/testdata/users_service.go.txt similarity index 100% rename from plugins/cmd/hostgen/testdata/users_service.go rename to plugins/cmd/ndpgen/testdata/users_service.go.txt diff --git a/plugins/examples/crypto-ticker/go.mod b/plugins/examples/crypto-ticker/go.mod index 78f28a7e7..c126c2d0a 100755 --- a/plugins/examples/crypto-ticker/go.mod +++ b/plugins/examples/crypto-ticker/go.mod @@ -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 diff --git a/plugins/examples/crypto-ticker/main.go b/plugins/examples/crypto-ticker/main.go index 400c8ca60..364a9a451 100755 --- a/plugins/examples/crypto-ticker/main.go +++ b/plugins/examples/crypto-ticker/main.go @@ -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)) } diff --git a/plugins/examples/discord-rich-presence-rs/Cargo.toml b/plugins/examples/discord-rich-presence-rs/Cargo.toml index 44ec844e9..ed9f32249 100644 --- a/plugins/examples/discord-rich-presence-rs/Cargo.toml +++ b/plugins/examples/discord-rich-presence-rs/Cargo.toml @@ -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" } diff --git a/plugins/examples/discord-rich-presence/go.mod b/plugins/examples/discord-rich-presence/go.mod index bc6bf30cc..2e18b9784 100644 --- a/plugins/examples/discord-rich-presence/go.mod +++ b/plugins/examples/discord-rich-presence/go.mod @@ -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 diff --git a/plugins/examples/discord-rich-presence/main.go b/plugins/examples/discord-rich-presence/main.go index 8c8be5101..d2817b1ec 100644 --- a/plugins/examples/discord-rich-presence/main.go +++ b/plugins/examples/discord-rich-presence/main.go @@ -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)) } diff --git a/plugins/examples/discord-rich-presence/rpc.go b/plugins/examples/discord-rich-presence/rpc.go index e189a5aab..cbc0da6ff 100644 --- a/plugins/examples/discord-rich-presence/rpc.go +++ b/plugins/examples/discord-rich-presence/rpc.go @@ -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) } } diff --git a/plugins/examples/library-inspector/Cargo.toml b/plugins/examples/library-inspector/Cargo.toml index 2d4b5e70c..f2915e8f6 100644 --- a/plugins/examples/library-inspector/Cargo.toml +++ b/plugins/examples/library-inspector/Cargo.toml @@ -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" diff --git a/plugins/host/artwork_gen.go b/plugins/host/artwork_gen.go index ff76ba7cf..fbf807351 100644 --- a/plugins/host/artwork_gen.go +++ b/plugins/host/artwork_gen.go @@ -1,4 +1,4 @@ -// Code generated by hostgen. DO NOT EDIT. +// Code generated by ndpgen. DO NOT EDIT. package host diff --git a/plugins/host/cache_gen.go b/plugins/host/cache_gen.go index 792f7420c..5645c5495 100644 --- a/plugins/host/cache_gen.go +++ b/plugins/host/cache_gen.go @@ -1,4 +1,4 @@ -// Code generated by hostgen. DO NOT EDIT. +// Code generated by ndpgen. DO NOT EDIT. package host diff --git a/plugins/host/doc.go b/plugins/host/doc.go index 905060405..34917d74e 100644 --- a/plugins/host/doc.go +++ b/plugins/host/doc.go @@ -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 _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 diff --git a/plugins/host/go/doc.go b/plugins/host/go/doc.go deleted file mode 100644 index 211368063..000000000 --- a/plugins/host/go/doc.go +++ /dev/null @@ -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 diff --git a/plugins/host/go/go.mod b/plugins/host/go/go.mod deleted file mode 100644 index 954c98071..000000000 --- a/plugins/host/go/go.mod +++ /dev/null @@ -1,5 +0,0 @@ -module github.com/navidrome/navidrome/plugins/host/go - -go 1.24 - -require github.com/extism/go-pdk v1.1.3 diff --git a/plugins/host/go/go.sum b/plugins/host/go/go.sum deleted file mode 100644 index bf78d8808..000000000 --- a/plugins/host/go/go.sum +++ /dev/null @@ -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= diff --git a/plugins/host/go/nd_host_artwork.go b/plugins/host/go/nd_host_artwork.go deleted file mode 100644 index 51811ea5a..000000000 --- a/plugins/host/go/nd_host_artwork.go +++ /dev/null @@ -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 -} diff --git a/plugins/host/go/nd_host_artwork_stub.go b/plugins/host/go/nd_host_artwork_stub.go deleted file mode 100644 index 9bf0b1352..000000000 --- a/plugins/host/go/nd_host_artwork_stub.go +++ /dev/null @@ -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") -} diff --git a/plugins/host/go/nd_host_cache.go b/plugins/host/go/nd_host_cache.go deleted file mode 100644 index ee4183820..000000000 --- a/plugins/host/go/nd_host_cache.go +++ /dev/null @@ -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 -} diff --git a/plugins/host/go/nd_host_cache_stub.go b/plugins/host/go/nd_host_cache_stub.go deleted file mode 100644 index d5a8932a9..000000000 --- a/plugins/host/go/nd_host_cache_stub.go +++ /dev/null @@ -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") -} diff --git a/plugins/host/go/nd_host_kvstore.go b/plugins/host/go/nd_host_kvstore.go deleted file mode 100644 index 52e9da911..000000000 --- a/plugins/host/go/nd_host_kvstore.go +++ /dev/null @@ -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 -} diff --git a/plugins/host/go/nd_host_kvstore_stub.go b/plugins/host/go/nd_host_kvstore_stub.go deleted file mode 100644 index d90e4071e..000000000 --- a/plugins/host/go/nd_host_kvstore_stub.go +++ /dev/null @@ -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") -} diff --git a/plugins/host/go/nd_host_library.go b/plugins/host/go/nd_host_library.go deleted file mode 100644 index 2d7da44c7..000000000 --- a/plugins/host/go/nd_host_library.go +++ /dev/null @@ -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 -} diff --git a/plugins/host/go/nd_host_library_stub.go b/plugins/host/go/nd_host_library_stub.go deleted file mode 100644 index c71a30cd1..000000000 --- a/plugins/host/go/nd_host_library_stub.go +++ /dev/null @@ -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") -} diff --git a/plugins/host/go/nd_host_scheduler.go b/plugins/host/go/nd_host_scheduler.go deleted file mode 100644 index 89411f2d1..000000000 --- a/plugins/host/go/nd_host_scheduler.go +++ /dev/null @@ -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 -} diff --git a/plugins/host/go/nd_host_scheduler_stub.go b/plugins/host/go/nd_host_scheduler_stub.go deleted file mode 100644 index 4a6029429..000000000 --- a/plugins/host/go/nd_host_scheduler_stub.go +++ /dev/null @@ -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") -} diff --git a/plugins/host/go/nd_host_subsonicapi.go b/plugins/host/go/nd_host_subsonicapi.go deleted file mode 100644 index e845e617a..000000000 --- a/plugins/host/go/nd_host_subsonicapi.go +++ /dev/null @@ -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 -} diff --git a/plugins/host/go/nd_host_subsonicapi_stub.go b/plugins/host/go/nd_host_subsonicapi_stub.go deleted file mode 100644 index e99f52ebb..000000000 --- a/plugins/host/go/nd_host_subsonicapi_stub.go +++ /dev/null @@ -1,29 +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 - -// 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 is a stub that panics on non-WASM platforms. -// 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) { - panic("ndhost: SubsonicAPICall is only available in WASM plugins") -} diff --git a/plugins/host/go/nd_host_websocket.go b/plugins/host/go/nd_host_websocket.go deleted file mode 100644 index d4ce707d7..000000000 --- a/plugins/host/go/nd_host_websocket.go +++ /dev/null @@ -1,258 +0,0 @@ -// Code generated by hostgen. DO NOT EDIT. -// -// This file contains client wrappers for the WebSocket 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" -) - -// websocket_connect is the host function provided by Navidrome. -// -//go:wasmimport extism:host/user websocket_connect -func websocket_connect(uint64) uint64 - -// websocket_sendtext is the host function provided by Navidrome. -// -//go:wasmimport extism:host/user websocket_sendtext -func websocket_sendtext(uint64) uint64 - -// websocket_sendbinary is the host function provided by Navidrome. -// -//go:wasmimport extism:host/user websocket_sendbinary -func websocket_sendbinary(uint64) uint64 - -// websocket_closeconnection is the host function provided by Navidrome. -// -//go:wasmimport extism:host/user websocket_closeconnection -func websocket_closeconnection(uint64) uint64 - -// WebSocketConnectRequest is the request type for WebSocket.Connect. -type WebSocketConnectRequest struct { - Url string `json:"url"` - Headers map[string]string `json:"headers"` - ConnectionID string `json:"connectionId"` -} - -// WebSocketConnectResponse is the response type for WebSocket.Connect. -type WebSocketConnectResponse struct { - NewConnectionID string `json:"newConnectionId,omitempty"` - Error string `json:"error,omitempty"` -} - -// WebSocketSendTextRequest is the request type for WebSocket.SendText. -type WebSocketSendTextRequest struct { - ConnectionID string `json:"connectionId"` - Message string `json:"message"` -} - -// WebSocketSendTextResponse is the response type for WebSocket.SendText. -type WebSocketSendTextResponse struct { - Error string `json:"error,omitempty"` -} - -// WebSocketSendBinaryRequest is the request type for WebSocket.SendBinary. -type WebSocketSendBinaryRequest struct { - ConnectionID string `json:"connectionId"` - Data []byte `json:"data"` -} - -// WebSocketSendBinaryResponse is the response type for WebSocket.SendBinary. -type WebSocketSendBinaryResponse struct { - Error string `json:"error,omitempty"` -} - -// WebSocketCloseConnectionRequest is the request type for WebSocket.CloseConnection. -type WebSocketCloseConnectionRequest struct { - ConnectionID string `json:"connectionId"` - Code int32 `json:"code"` - Reason string `json:"reason"` -} - -// WebSocketCloseConnectionResponse is the response type for WebSocket.CloseConnection. -type WebSocketCloseConnectionResponse struct { - Error string `json:"error,omitempty"` -} - -// WebSocketConnect calls the websocket_connect host function. -// Connect establishes a WebSocket connection to the specified URL. -// -// Plugins that use this function must also implement the WebSocketCallback capability -// to receive incoming messages and connection events. -// -// Parameters: -// - url: The WebSocket URL to connect to (ws:// or wss://) -// - headers: Optional HTTP headers to include in the handshake request -// - connectionID: Optional unique identifier for the connection. If empty, one will be generated -// -// Returns the connection ID that can be used to send messages or close the connection, -// or an error if the connection fails. -func WebSocketConnect(url string, headers map[string]string, connectionID string) (*WebSocketConnectResponse, error) { - // Marshal request to JSON - req := WebSocketConnectRequest{ - Url: url, - Headers: headers, - ConnectionID: connectionID, - } - reqBytes, err := json.Marshal(req) - if err != nil { - return nil, err - } - reqMem := pdk.AllocateBytes(reqBytes) - defer reqMem.Free() - - // Call the host function - responsePtr := websocket_connect(reqMem.Offset()) - - // Read the response from memory - responseMem := pdk.FindMemory(responsePtr) - responseBytes := responseMem.ReadBytes() - - // Parse the response - var response WebSocketConnectResponse - 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 -} - -// WebSocketSendText calls the websocket_sendtext host function. -// SendText sends a text message over an established WebSocket connection. -// -// Parameters: -// - connectionID: The connection identifier returned by Connect -// - message: The text message to send -// -// Returns an error if the connection is not found or if sending fails. -func WebSocketSendText(connectionID string, message string) (*WebSocketSendTextResponse, error) { - // Marshal request to JSON - req := WebSocketSendTextRequest{ - ConnectionID: connectionID, - Message: message, - } - reqBytes, err := json.Marshal(req) - if err != nil { - return nil, err - } - reqMem := pdk.AllocateBytes(reqBytes) - defer reqMem.Free() - - // Call the host function - responsePtr := websocket_sendtext(reqMem.Offset()) - - // Read the response from memory - responseMem := pdk.FindMemory(responsePtr) - responseBytes := responseMem.ReadBytes() - - // Parse the response - var response WebSocketSendTextResponse - 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 -} - -// WebSocketSendBinary calls the websocket_sendbinary host function. -// SendBinary sends binary data over an established WebSocket connection. -// -// Parameters: -// - connectionID: The connection identifier returned by Connect -// - data: The binary data to send -// -// Returns an error if the connection is not found or if sending fails. -func WebSocketSendBinary(connectionID string, data []byte) (*WebSocketSendBinaryResponse, error) { - // Marshal request to JSON - req := WebSocketSendBinaryRequest{ - ConnectionID: connectionID, - Data: data, - } - reqBytes, err := json.Marshal(req) - if err != nil { - return nil, err - } - reqMem := pdk.AllocateBytes(reqBytes) - defer reqMem.Free() - - // Call the host function - responsePtr := websocket_sendbinary(reqMem.Offset()) - - // Read the response from memory - responseMem := pdk.FindMemory(responsePtr) - responseBytes := responseMem.ReadBytes() - - // Parse the response - var response WebSocketSendBinaryResponse - 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 -} - -// WebSocketCloseConnection calls the websocket_closeconnection host function. -// CloseConnection gracefully closes a WebSocket connection. -// -// Parameters: -// - connectionID: The connection identifier returned by Connect -// - code: WebSocket close status code (e.g., 1000 for normal closure) -// - reason: Optional human-readable reason for closing -// -// Returns an error if the connection is not found or if closing fails. -func WebSocketCloseConnection(connectionID string, code int32, reason string) (*WebSocketCloseConnectionResponse, error) { - // Marshal request to JSON - req := WebSocketCloseConnectionRequest{ - ConnectionID: connectionID, - Code: code, - Reason: reason, - } - reqBytes, err := json.Marshal(req) - if err != nil { - return nil, err - } - reqMem := pdk.AllocateBytes(reqBytes) - defer reqMem.Free() - - // Call the host function - responsePtr := websocket_closeconnection(reqMem.Offset()) - - // Read the response from memory - responseMem := pdk.FindMemory(responsePtr) - responseBytes := responseMem.ReadBytes() - - // Parse the response - var response WebSocketCloseConnectionResponse - 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 -} diff --git a/plugins/host/go/nd_host_websocket_stub.go b/plugins/host/go/nd_host_websocket_stub.go deleted file mode 100644 index 75b4ae0f9..000000000 --- a/plugins/host/go/nd_host_websocket_stub.go +++ /dev/null @@ -1,110 +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 - -// WebSocketConnectRequest is the request type for WebSocket.Connect. -type WebSocketConnectRequest struct { - Url string `json:"url"` - Headers map[string]string `json:"headers"` - ConnectionID string `json:"connectionId"` -} - -// WebSocketConnectResponse is the response type for WebSocket.Connect. -type WebSocketConnectResponse struct { - NewConnectionID string `json:"newConnectionId,omitempty"` - Error string `json:"error,omitempty"` -} - -// WebSocketSendTextRequest is the request type for WebSocket.SendText. -type WebSocketSendTextRequest struct { - ConnectionID string `json:"connectionId"` - Message string `json:"message"` -} - -// WebSocketSendTextResponse is the response type for WebSocket.SendText. -type WebSocketSendTextResponse struct { - Error string `json:"error,omitempty"` -} - -// WebSocketSendBinaryRequest is the request type for WebSocket.SendBinary. -type WebSocketSendBinaryRequest struct { - ConnectionID string `json:"connectionId"` - Data []byte `json:"data"` -} - -// WebSocketSendBinaryResponse is the response type for WebSocket.SendBinary. -type WebSocketSendBinaryResponse struct { - Error string `json:"error,omitempty"` -} - -// WebSocketCloseConnectionRequest is the request type for WebSocket.CloseConnection. -type WebSocketCloseConnectionRequest struct { - ConnectionID string `json:"connectionId"` - Code int32 `json:"code"` - Reason string `json:"reason"` -} - -// WebSocketCloseConnectionResponse is the response type for WebSocket.CloseConnection. -type WebSocketCloseConnectionResponse struct { - Error string `json:"error,omitempty"` -} - -// WebSocketConnect is a stub that panics on non-WASM platforms. -// Connect establishes a WebSocket connection to the specified URL. -// -// Plugins that use this function must also implement the WebSocketCallback capability -// to receive incoming messages and connection events. -// -// Parameters: -// - url: The WebSocket URL to connect to (ws:// or wss://) -// - headers: Optional HTTP headers to include in the handshake request -// - connectionID: Optional unique identifier for the connection. If empty, one will be generated -// -// Returns the connection ID that can be used to send messages or close the connection, -// or an error if the connection fails. -func WebSocketConnect(url string, headers map[string]string, connectionID string) (*WebSocketConnectResponse, error) { - panic("ndhost: WebSocketConnect is only available in WASM plugins") -} - -// WebSocketSendText is a stub that panics on non-WASM platforms. -// SendText sends a text message over an established WebSocket connection. -// -// Parameters: -// - connectionID: The connection identifier returned by Connect -// - message: The text message to send -// -// Returns an error if the connection is not found or if sending fails. -func WebSocketSendText(connectionID string, message string) (*WebSocketSendTextResponse, error) { - panic("ndhost: WebSocketSendText is only available in WASM plugins") -} - -// WebSocketSendBinary is a stub that panics on non-WASM platforms. -// SendBinary sends binary data over an established WebSocket connection. -// -// Parameters: -// - connectionID: The connection identifier returned by Connect -// - data: The binary data to send -// -// Returns an error if the connection is not found or if sending fails. -func WebSocketSendBinary(connectionID string, data []byte) (*WebSocketSendBinaryResponse, error) { - panic("ndhost: WebSocketSendBinary is only available in WASM plugins") -} - -// WebSocketCloseConnection is a stub that panics on non-WASM platforms. -// CloseConnection gracefully closes a WebSocket connection. -// -// Parameters: -// - connectionID: The connection identifier returned by Connect -// - code: WebSocket close status code (e.g., 1000 for normal closure) -// - reason: Optional human-readable reason for closing -// -// Returns an error if the connection is not found or if closing fails. -func WebSocketCloseConnection(connectionID string, code int32, reason string) (*WebSocketCloseConnectionResponse, error) { - panic("ndhost: WebSocketCloseConnection is only available in WASM plugins") -} diff --git a/plugins/host/kvstore_gen.go b/plugins/host/kvstore_gen.go index 422199f23..2ad24959d 100644 --- a/plugins/host/kvstore_gen.go +++ b/plugins/host/kvstore_gen.go @@ -1,4 +1,4 @@ -// Code generated by hostgen. DO NOT EDIT. +// Code generated by ndpgen. DO NOT EDIT. package host diff --git a/plugins/host/library_gen.go b/plugins/host/library_gen.go index 95409ab36..27195e85c 100644 --- a/plugins/host/library_gen.go +++ b/plugins/host/library_gen.go @@ -1,4 +1,4 @@ -// Code generated by hostgen. DO NOT EDIT. +// Code generated by ndpgen. DO NOT EDIT. package host diff --git a/plugins/host/python/nd_host_artwork.py b/plugins/host/python/nd_host_artwork.py deleted file mode 100644 index 3919dbbe9..000000000 --- a/plugins/host/python/nd_host_artwork.py +++ /dev/null @@ -1,183 +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 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 - - -@extism.import_fn("extism:host/user", "artwork_getartisturl") -def _artwork_getartisturl(offset: int) -> int: - """Raw host function - do not call directly.""" - ... - - -@extism.import_fn("extism:host/user", "artwork_getalbumurl") -def _artwork_getalbumurl(offset: int) -> int: - """Raw host function - do not call directly.""" - ... - - -@extism.import_fn("extism:host/user", "artwork_gettrackurl") -def _artwork_gettrackurl(offset: int) -> int: - """Raw host function - do not call directly.""" - ... - - -@extism.import_fn("extism:host/user", "artwork_getplaylisturl") -def _artwork_getplaylisturl(offset: int) -> int: - """Raw host function - do not call directly.""" - ... - - -def artwork_get_artist_url(id: str, size: int) -> str: - """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. - - Args: - id: str parameter. - size: int parameter. - - Returns: - str: The result value. - - Raises: - HostFunctionError: If the host function returns an error. - """ - request = { - "id": id, - "size": size, - } - request_bytes = json.dumps(request).encode("utf-8") - request_mem = extism.memory.alloc(request_bytes) - response_offset = _artwork_getartisturl(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"]) - - return response.get("url", "") - - -def artwork_get_album_url(id: str, size: int) -> str: - """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. - - Args: - id: str parameter. - size: int parameter. - - Returns: - str: The result value. - - Raises: - HostFunctionError: If the host function returns an error. - """ - request = { - "id": id, - "size": size, - } - request_bytes = json.dumps(request).encode("utf-8") - request_mem = extism.memory.alloc(request_bytes) - response_offset = _artwork_getalbumurl(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"]) - - return response.get("url", "") - - -def artwork_get_track_url(id: str, size: int) -> str: - """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. - - Args: - id: str parameter. - size: int parameter. - - Returns: - str: The result value. - - Raises: - HostFunctionError: If the host function returns an error. - """ - request = { - "id": id, - "size": size, - } - request_bytes = json.dumps(request).encode("utf-8") - request_mem = extism.memory.alloc(request_bytes) - response_offset = _artwork_gettrackurl(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"]) - - return response.get("url", "") - - -def artwork_get_playlist_url(id: str, size: int) -> str: - """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. - - Args: - id: str parameter. - size: int parameter. - - Returns: - str: The result value. - - Raises: - HostFunctionError: If the host function returns an error. - """ - request = { - "id": id, - "size": size, - } - request_bytes = json.dumps(request).encode("utf-8") - request_mem = extism.memory.alloc(request_bytes) - response_offset = _artwork_getplaylisturl(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"]) - - return response.get("url", "") diff --git a/plugins/host/python/nd_host_cache.py b/plugins/host/python/nd_host_cache.py deleted file mode 100644 index f06b2ad0c..000000000 --- a/plugins/host/python/nd_host_cache.py +++ /dev/null @@ -1,447 +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 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 - - -@extism.import_fn("extism:host/user", "cache_setstring") -def _cache_setstring(offset: int) -> int: - """Raw host function - do not call directly.""" - ... - - -@extism.import_fn("extism:host/user", "cache_getstring") -def _cache_getstring(offset: int) -> int: - """Raw host function - do not call directly.""" - ... - - -@extism.import_fn("extism:host/user", "cache_setint") -def _cache_setint(offset: int) -> int: - """Raw host function - do not call directly.""" - ... - - -@extism.import_fn("extism:host/user", "cache_getint") -def _cache_getint(offset: int) -> int: - """Raw host function - do not call directly.""" - ... - - -@extism.import_fn("extism:host/user", "cache_setfloat") -def _cache_setfloat(offset: int) -> int: - """Raw host function - do not call directly.""" - ... - - -@extism.import_fn("extism:host/user", "cache_getfloat") -def _cache_getfloat(offset: int) -> int: - """Raw host function - do not call directly.""" - ... - - -@extism.import_fn("extism:host/user", "cache_setbytes") -def _cache_setbytes(offset: int) -> int: - """Raw host function - do not call directly.""" - ... - - -@extism.import_fn("extism:host/user", "cache_getbytes") -def _cache_getbytes(offset: int) -> int: - """Raw host function - do not call directly.""" - ... - - -@extism.import_fn("extism:host/user", "cache_has") -def _cache_has(offset: int) -> int: - """Raw host function - do not call directly.""" - ... - - -@extism.import_fn("extism:host/user", "cache_remove") -def _cache_remove(offset: int) -> int: - """Raw host function - do not call directly.""" - ... - - -@dataclass -class CacheGetStringResult: - """Result type for cache_get_string.""" - value: str - exists: bool - - -@dataclass -class CacheGetIntResult: - """Result type for cache_get_int.""" - value: int - exists: bool - - -@dataclass -class CacheGetFloatResult: - """Result type for cache_get_float.""" - value: float - exists: bool - - -@dataclass -class CacheGetBytesResult: - """Result type for cache_get_bytes.""" - value: bytes - exists: bool - - -def cache_set_string(key: str, value: str, ttl_seconds: int) -> None: - """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. - - Args: - key: str parameter. - value: str parameter. - ttl_seconds: int parameter. - - Raises: - HostFunctionError: If the host function returns an error. - """ - request = { - "key": key, - "value": value, - "ttlSeconds": ttl_seconds, - } - request_bytes = json.dumps(request).encode("utf-8") - request_mem = extism.memory.alloc(request_bytes) - response_offset = _cache_setstring(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"]) - - - -def cache_get_string(key: str) -> CacheGetStringResult: - """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. - - Args: - key: str parameter. - - Returns: - CacheGetStringResult containing value, exists,. - - Raises: - HostFunctionError: If the host function returns an error. - """ - request = { - "key": key, - } - request_bytes = json.dumps(request).encode("utf-8") - request_mem = extism.memory.alloc(request_bytes) - response_offset = _cache_getstring(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"]) - - return CacheGetStringResult( - value=response.get("value", ""), - exists=response.get("exists", False), - ) - - -def cache_set_int(key: str, value: int, ttl_seconds: int) -> None: - """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. - - Args: - key: str parameter. - value: int parameter. - ttl_seconds: int parameter. - - Raises: - HostFunctionError: If the host function returns an error. - """ - request = { - "key": key, - "value": value, - "ttlSeconds": ttl_seconds, - } - request_bytes = json.dumps(request).encode("utf-8") - request_mem = extism.memory.alloc(request_bytes) - response_offset = _cache_setint(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"]) - - - -def cache_get_int(key: str) -> CacheGetIntResult: - """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. - - Args: - key: str parameter. - - Returns: - CacheGetIntResult containing value, exists,. - - Raises: - HostFunctionError: If the host function returns an error. - """ - request = { - "key": key, - } - request_bytes = json.dumps(request).encode("utf-8") - request_mem = extism.memory.alloc(request_bytes) - response_offset = _cache_getint(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"]) - - return CacheGetIntResult( - value=response.get("value", 0), - exists=response.get("exists", False), - ) - - -def cache_set_float(key: str, value: float, ttl_seconds: int) -> None: - """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. - - Args: - key: str parameter. - value: float parameter. - ttl_seconds: int parameter. - - Raises: - HostFunctionError: If the host function returns an error. - """ - request = { - "key": key, - "value": value, - "ttlSeconds": ttl_seconds, - } - request_bytes = json.dumps(request).encode("utf-8") - request_mem = extism.memory.alloc(request_bytes) - response_offset = _cache_setfloat(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"]) - - - -def cache_get_float(key: str) -> CacheGetFloatResult: - """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. - - Args: - key: str parameter. - - Returns: - CacheGetFloatResult containing value, exists,. - - Raises: - HostFunctionError: If the host function returns an error. - """ - request = { - "key": key, - } - request_bytes = json.dumps(request).encode("utf-8") - request_mem = extism.memory.alloc(request_bytes) - response_offset = _cache_getfloat(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"]) - - return CacheGetFloatResult( - value=response.get("value", 0.0), - exists=response.get("exists", False), - ) - - -def cache_set_bytes(key: str, value: bytes, ttl_seconds: int) -> None: - """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. - - Args: - key: str parameter. - value: bytes parameter. - ttl_seconds: int parameter. - - Raises: - HostFunctionError: If the host function returns an error. - """ - request = { - "key": key, - "value": value, - "ttlSeconds": ttl_seconds, - } - request_bytes = json.dumps(request).encode("utf-8") - request_mem = extism.memory.alloc(request_bytes) - response_offset = _cache_setbytes(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"]) - - - -def cache_get_bytes(key: str) -> CacheGetBytesResult: - """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. - - Args: - key: str parameter. - - Returns: - CacheGetBytesResult containing value, exists,. - - Raises: - HostFunctionError: If the host function returns an error. - """ - request = { - "key": key, - } - request_bytes = json.dumps(request).encode("utf-8") - request_mem = extism.memory.alloc(request_bytes) - response_offset = _cache_getbytes(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"]) - - return CacheGetBytesResult( - value=response.get("value", b""), - exists=response.get("exists", False), - ) - - -def cache_has(key: str) -> bool: - """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. - - Args: - key: str parameter. - - Returns: - bool: The result value. - - Raises: - HostFunctionError: If the host function returns an error. - """ - request = { - "key": key, - } - request_bytes = json.dumps(request).encode("utf-8") - request_mem = extism.memory.alloc(request_bytes) - response_offset = _cache_has(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"]) - - return response.get("exists", False) - - -def cache_remove(key: str) -> None: - """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. - - Args: - key: str parameter. - - Raises: - HostFunctionError: If the host function returns an error. - """ - request = { - "key": key, - } - request_bytes = json.dumps(request).encode("utf-8") - request_mem = extism.memory.alloc(request_bytes) - response_offset = _cache_remove(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"]) - diff --git a/plugins/host/python/nd_host_kvstore.py b/plugins/host/python/nd_host_kvstore.py deleted file mode 100644 index f1a6e5724..000000000 --- a/plugins/host/python/nd_host_kvstore.py +++ /dev/null @@ -1,241 +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 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 - - -@extism.import_fn("extism:host/user", "kvstore_set") -def _kvstore_set(offset: int) -> int: - """Raw host function - do not call directly.""" - ... - - -@extism.import_fn("extism:host/user", "kvstore_get") -def _kvstore_get(offset: int) -> int: - """Raw host function - do not call directly.""" - ... - - -@extism.import_fn("extism:host/user", "kvstore_delete") -def _kvstore_delete(offset: int) -> int: - """Raw host function - do not call directly.""" - ... - - -@extism.import_fn("extism:host/user", "kvstore_has") -def _kvstore_has(offset: int) -> int: - """Raw host function - do not call directly.""" - ... - - -@extism.import_fn("extism:host/user", "kvstore_list") -def _kvstore_list(offset: int) -> int: - """Raw host function - do not call directly.""" - ... - - -@extism.import_fn("extism:host/user", "kvstore_getstorageused") -def _kvstore_getstorageused(offset: int) -> int: - """Raw host function - do not call directly.""" - ... - - -@dataclass -class KVStoreGetResult: - """Result type for kvstore_get.""" - value: bytes - exists: bool - - -def kvstore_set(key: str, value: bytes) -> None: - """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. - - Args: - key: str parameter. - value: bytes parameter. - - Raises: - HostFunctionError: If the host function returns an error. - """ - request = { - "key": key, - "value": value, - } - request_bytes = json.dumps(request).encode("utf-8") - request_mem = extism.memory.alloc(request_bytes) - response_offset = _kvstore_set(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"]) - - - -def kvstore_get(key: str) -> KVStoreGetResult: - """Get retrieves a byte value from storage. - -Parameters: - - key: The storage key - -Returns the value and whether the key exists. - - Args: - key: str parameter. - - Returns: - KVStoreGetResult containing value, exists,. - - Raises: - HostFunctionError: If the host function returns an error. - """ - request = { - "key": key, - } - request_bytes = json.dumps(request).encode("utf-8") - request_mem = extism.memory.alloc(request_bytes) - response_offset = _kvstore_get(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"]) - - return KVStoreGetResult( - value=response.get("value", b""), - exists=response.get("exists", False), - ) - - -def kvstore_delete(key: str) -> None: - """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. - - Args: - key: str parameter. - - Raises: - HostFunctionError: If the host function returns an error. - """ - request = { - "key": key, - } - request_bytes = json.dumps(request).encode("utf-8") - request_mem = extism.memory.alloc(request_bytes) - response_offset = _kvstore_delete(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"]) - - - -def kvstore_has(key: str) -> bool: - """Has checks if a key exists in storage. - -Parameters: - - key: The storage key - -Returns true if the key exists. - - Args: - key: str parameter. - - Returns: - bool: The result value. - - Raises: - HostFunctionError: If the host function returns an error. - """ - request = { - "key": key, - } - request_bytes = json.dumps(request).encode("utf-8") - request_mem = extism.memory.alloc(request_bytes) - response_offset = _kvstore_has(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"]) - - return response.get("exists", False) - - -def kvstore_list(prefix: str) -> Any: - """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. - - Args: - prefix: str parameter. - - Returns: - Any: The result value. - - Raises: - HostFunctionError: If the host function returns an error. - """ - request = { - "prefix": prefix, - } - request_bytes = json.dumps(request).encode("utf-8") - request_mem = extism.memory.alloc(request_bytes) - response_offset = _kvstore_list(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"]) - - return response.get("keys", None) - - -def kvstore_get_storage_used() -> int: - """GetStorageUsed returns the total storage used by this plugin in bytes. - - Returns: - int: The result value. - - Raises: - HostFunctionError: If the host function returns an error. - """ - request_bytes = b"{}" - request_mem = extism.memory.alloc(request_bytes) - response_offset = _kvstore_getstorageused(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"]) - - return response.get("bytes", 0) diff --git a/plugins/host/python/nd_host_library.py b/plugins/host/python/nd_host_library.py deleted file mode 100644 index 4111df2b9..000000000 --- a/plugins/host/python/nd_host_library.py +++ /dev/null @@ -1,86 +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 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 - - -@extism.import_fn("extism:host/user", "library_getlibrary") -def _library_getlibrary(offset: int) -> int: - """Raw host function - do not call directly.""" - ... - - -@extism.import_fn("extism:host/user", "library_getalllibraries") -def _library_getalllibraries(offset: int) -> int: - """Raw host function - do not call directly.""" - ... - - -def library_get_library(id: int) -> Any: - """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. - - Args: - id: int parameter. - - Returns: - Any: The result value. - - Raises: - HostFunctionError: If the host function returns an error. - """ - request = { - "id": id, - } - request_bytes = json.dumps(request).encode("utf-8") - request_mem = extism.memory.alloc(request_bytes) - response_offset = _library_getlibrary(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"]) - - return response.get("result", None) - - -def library_get_all_libraries() -> Any: - """GetAllLibraries retrieves metadata for all configured libraries. - -Returns a slice of all libraries with their metadata. - - Returns: - Any: The result value. - - Raises: - HostFunctionError: If the host function returns an error. - """ - request_bytes = b"{}" - request_mem = extism.memory.alloc(request_bytes) - response_offset = _library_getalllibraries(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"]) - - return response.get("result", None) diff --git a/plugins/host/python/nd_host_scheduler.py b/plugins/host/python/nd_host_scheduler.py deleted file mode 100644 index 3a94da94d..000000000 --- a/plugins/host/python/nd_host_scheduler.py +++ /dev/null @@ -1,143 +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 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 - - -@extism.import_fn("extism:host/user", "scheduler_scheduleonetime") -def _scheduler_scheduleonetime(offset: int) -> int: - """Raw host function - do not call directly.""" - ... - - -@extism.import_fn("extism:host/user", "scheduler_schedulerecurring") -def _scheduler_schedulerecurring(offset: int) -> int: - """Raw host function - do not call directly.""" - ... - - -@extism.import_fn("extism:host/user", "scheduler_cancelschedule") -def _scheduler_cancelschedule(offset: int) -> int: - """Raw host function - do not call directly.""" - ... - - -def scheduler_schedule_one_time(delay_seconds: int, payload: str, schedule_id: str) -> str: - """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. - - Args: - delay_seconds: int parameter. - payload: str parameter. - schedule_id: str parameter. - - Returns: - str: The result value. - - Raises: - HostFunctionError: If the host function returns an error. - """ - request = { - "delaySeconds": delay_seconds, - "payload": payload, - "scheduleId": schedule_id, - } - request_bytes = json.dumps(request).encode("utf-8") - request_mem = extism.memory.alloc(request_bytes) - response_offset = _scheduler_scheduleonetime(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"]) - - return response.get("newScheduleId", "") - - -def scheduler_schedule_recurring(cron_expression: str, payload: str, schedule_id: str) -> str: - """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. - - Args: - cron_expression: str parameter. - payload: str parameter. - schedule_id: str parameter. - - Returns: - str: The result value. - - Raises: - HostFunctionError: If the host function returns an error. - """ - request = { - "cronExpression": cron_expression, - "payload": payload, - "scheduleId": schedule_id, - } - request_bytes = json.dumps(request).encode("utf-8") - request_mem = extism.memory.alloc(request_bytes) - response_offset = _scheduler_schedulerecurring(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"]) - - return response.get("newScheduleId", "") - - -def scheduler_cancel_schedule(schedule_id: str) -> None: - """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. - - Args: - schedule_id: str parameter. - - Raises: - HostFunctionError: If the host function returns an error. - """ - request = { - "scheduleId": schedule_id, - } - request_bytes = json.dumps(request).encode("utf-8") - request_mem = extism.memory.alloc(request_bytes) - response_offset = _scheduler_cancelschedule(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"]) - diff --git a/plugins/host/python/nd_host_subsonicapi.py b/plugins/host/python/nd_host_subsonicapi.py deleted file mode 100644 index 4eaf06d4c..000000000 --- a/plugins/host/python/nd_host_subsonicapi.py +++ /dev/null @@ -1,55 +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 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 - - -@extism.import_fn("extism:host/user", "subsonicapi_call") -def _subsonicapi_call(offset: int) -> int: - """Raw host function - do not call directly.""" - ... - - -def subsonicapi_call(uri: str) -> str: - """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. - - Args: - uri: str parameter. - - Returns: - str: The result value. - - Raises: - HostFunctionError: If the host function returns an error. - """ - request = { - "uri": uri, - } - request_bytes = json.dumps(request).encode("utf-8") - request_mem = extism.memory.alloc(request_bytes) - response_offset = _subsonicapi_call(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"]) - - return response.get("responseJson", "") diff --git a/plugins/host/python/nd_host_websocket.py b/plugins/host/python/nd_host_websocket.py deleted file mode 100644 index 67938f41f..000000000 --- a/plugins/host/python/nd_host_websocket.py +++ /dev/null @@ -1,181 +0,0 @@ -# Code generated by hostgen. DO NOT EDIT. -# -# This file contains client wrappers for the WebSocket 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 - - -@extism.import_fn("extism:host/user", "websocket_connect") -def _websocket_connect(offset: int) -> int: - """Raw host function - do not call directly.""" - ... - - -@extism.import_fn("extism:host/user", "websocket_sendtext") -def _websocket_sendtext(offset: int) -> int: - """Raw host function - do not call directly.""" - ... - - -@extism.import_fn("extism:host/user", "websocket_sendbinary") -def _websocket_sendbinary(offset: int) -> int: - """Raw host function - do not call directly.""" - ... - - -@extism.import_fn("extism:host/user", "websocket_closeconnection") -def _websocket_closeconnection(offset: int) -> int: - """Raw host function - do not call directly.""" - ... - - -def websocket_connect(url: str, headers: Any, connection_id: str) -> str: - """Connect establishes a WebSocket connection to the specified URL. - -Plugins that use this function must also implement the WebSocketCallback capability -to receive incoming messages and connection events. - -Parameters: - - url: The WebSocket URL to connect to (ws:// or wss://) - - headers: Optional HTTP headers to include in the handshake request - - connectionID: Optional unique identifier for the connection. If empty, one will be generated - -Returns the connection ID that can be used to send messages or close the connection, -or an error if the connection fails. - - Args: - url: str parameter. - headers: Any parameter. - connection_id: str parameter. - - Returns: - str: The result value. - - Raises: - HostFunctionError: If the host function returns an error. - """ - request = { - "url": url, - "headers": headers, - "connectionId": connection_id, - } - request_bytes = json.dumps(request).encode("utf-8") - request_mem = extism.memory.alloc(request_bytes) - response_offset = _websocket_connect(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"]) - - return response.get("newConnectionId", "") - - -def websocket_send_text(connection_id: str, message: str) -> None: - """SendText sends a text message over an established WebSocket connection. - -Parameters: - - connectionID: The connection identifier returned by Connect - - message: The text message to send - -Returns an error if the connection is not found or if sending fails. - - Args: - connection_id: str parameter. - message: str parameter. - - Raises: - HostFunctionError: If the host function returns an error. - """ - request = { - "connectionId": connection_id, - "message": message, - } - request_bytes = json.dumps(request).encode("utf-8") - request_mem = extism.memory.alloc(request_bytes) - response_offset = _websocket_sendtext(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"]) - - - -def websocket_send_binary(connection_id: str, data: bytes) -> None: - """SendBinary sends binary data over an established WebSocket connection. - -Parameters: - - connectionID: The connection identifier returned by Connect - - data: The binary data to send - -Returns an error if the connection is not found or if sending fails. - - Args: - connection_id: str parameter. - data: bytes parameter. - - Raises: - HostFunctionError: If the host function returns an error. - """ - request = { - "connectionId": connection_id, - "data": data, - } - request_bytes = json.dumps(request).encode("utf-8") - request_mem = extism.memory.alloc(request_bytes) - response_offset = _websocket_sendbinary(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"]) - - - -def websocket_close_connection(connection_id: str, code: int, reason: str) -> None: - """CloseConnection gracefully closes a WebSocket connection. - -Parameters: - - connectionID: The connection identifier returned by Connect - - code: WebSocket close status code (e.g., 1000 for normal closure) - - reason: Optional human-readable reason for closing - -Returns an error if the connection is not found or if closing fails. - - Args: - connection_id: str parameter. - code: int parameter. - reason: str parameter. - - Raises: - HostFunctionError: If the host function returns an error. - """ - request = { - "connectionId": connection_id, - "code": code, - "reason": reason, - } - request_bytes = json.dumps(request).encode("utf-8") - request_mem = extism.memory.alloc(request_bytes) - response_offset = _websocket_closeconnection(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"]) - diff --git a/plugins/host/rust/lib.rs b/plugins/host/rust/lib.rs deleted file mode 100644 index b71861398..000000000 --- a/plugins/host/rust/lib.rs +++ /dev/null @@ -1,65 +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 -//! -//! - [`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. - -#[path = "nd_host_artwork.rs"] -pub mod artwork; - -#[path = "nd_host_cache.rs"] -pub mod cache; - -#[path = "nd_host_kvstore.rs"] -pub mod kvstore; - -#[path = "nd_host_library.rs"] -pub mod library; - -#[path = "nd_host_scheduler.rs"] -pub mod scheduler; - -#[path = "nd_host_subsonicapi.rs"] -pub mod subsonicapi; - -#[path = "nd_host_websocket.rs"] -pub mod websocket; - -// Re-export commonly used types from extism-pdk for convenience -pub use extism_pdk::Error; diff --git a/plugins/host/rust/nd_host_artwork.rs b/plugins/host/rust/nd_host_artwork.rs deleted file mode 100644 index 31ccb4370..000000000 --- a/plugins/host/rust/nd_host_artwork.rs +++ /dev/null @@ -1,207 +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 extism-pdk. - -use extism_pdk::*; -use serde::{Deserialize, Serialize}; - -#[derive(Debug, Clone, Serialize)] -#[serde(rename_all = "camelCase")] -struct ArtworkGetArtistUrlRequest { - id: String, - size: i32, -} - -#[derive(Debug, Clone, Deserialize)] -#[serde(rename_all = "camelCase")] -struct ArtworkGetArtistUrlResponse { - #[serde(default)] - url: String, - #[serde(default)] - error: Option, -} - -#[derive(Debug, Clone, Serialize)] -#[serde(rename_all = "camelCase")] -struct ArtworkGetAlbumUrlRequest { - id: String, - size: i32, -} - -#[derive(Debug, Clone, Deserialize)] -#[serde(rename_all = "camelCase")] -struct ArtworkGetAlbumUrlResponse { - #[serde(default)] - url: String, - #[serde(default)] - error: Option, -} - -#[derive(Debug, Clone, Serialize)] -#[serde(rename_all = "camelCase")] -struct ArtworkGetTrackUrlRequest { - id: String, - size: i32, -} - -#[derive(Debug, Clone, Deserialize)] -#[serde(rename_all = "camelCase")] -struct ArtworkGetTrackUrlResponse { - #[serde(default)] - url: String, - #[serde(default)] - error: Option, -} - -#[derive(Debug, Clone, Serialize)] -#[serde(rename_all = "camelCase")] -struct ArtworkGetPlaylistUrlRequest { - id: String, - size: i32, -} - -#[derive(Debug, Clone, Deserialize)] -#[serde(rename_all = "camelCase")] -struct ArtworkGetPlaylistUrlResponse { - #[serde(default)] - url: String, - #[serde(default)] - error: Option, -} - -#[host_fn] -extern "ExtismHost" { - fn artwork_getartisturl(input: Json) -> Json; - fn artwork_getalbumurl(input: Json) -> Json; - fn artwork_gettrackurl(input: Json) -> Json; - fn artwork_getplaylisturl(input: Json) -> Json; -} - -/// 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. -/// -/// # Arguments -/// * `id` - String parameter. -/// * `size` - i32 parameter. -/// -/// # Returns -/// The url value. -/// -/// # Errors -/// Returns an error if the host function call fails. -pub fn get_artist_url(id: &str, size: i32) -> Result { - let response = unsafe { - artwork_getartisturl(Json(ArtworkGetArtistUrlRequest { - id: id.to_owned(), - size: size, - }))? - }; - - if let Some(err) = response.0.error { - return Err(Error::msg(err)); - } - - Ok(response.0.url) -} - -/// 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. -/// -/// # Arguments -/// * `id` - String parameter. -/// * `size` - i32 parameter. -/// -/// # Returns -/// The url value. -/// -/// # Errors -/// Returns an error if the host function call fails. -pub fn get_album_url(id: &str, size: i32) -> Result { - let response = unsafe { - artwork_getalbumurl(Json(ArtworkGetAlbumUrlRequest { - id: id.to_owned(), - size: size, - }))? - }; - - if let Some(err) = response.0.error { - return Err(Error::msg(err)); - } - - Ok(response.0.url) -} - -/// 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. -/// -/// # Arguments -/// * `id` - String parameter. -/// * `size` - i32 parameter. -/// -/// # Returns -/// The url value. -/// -/// # Errors -/// Returns an error if the host function call fails. -pub fn get_track_url(id: &str, size: i32) -> Result { - let response = unsafe { - artwork_gettrackurl(Json(ArtworkGetTrackUrlRequest { - id: id.to_owned(), - size: size, - }))? - }; - - if let Some(err) = response.0.error { - return Err(Error::msg(err)); - } - - Ok(response.0.url) -} - -/// 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. -/// -/// # Arguments -/// * `id` - String parameter. -/// * `size` - i32 parameter. -/// -/// # Returns -/// The url value. -/// -/// # Errors -/// Returns an error if the host function call fails. -pub fn get_playlist_url(id: &str, size: i32) -> Result { - let response = unsafe { - artwork_getplaylisturl(Json(ArtworkGetPlaylistUrlRequest { - id: id.to_owned(), - size: size, - }))? - }; - - if let Some(err) = response.0.error { - return Err(Error::msg(err)); - } - - Ok(response.0.url) -} diff --git a/plugins/host/rust/nd_host_cache.rs b/plugins/host/rust/nd_host_cache.rs deleted file mode 100644 index f223c8de8..000000000 --- a/plugins/host/rust/nd_host_cache.rs +++ /dev/null @@ -1,480 +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 extism-pdk. - -use extism_pdk::*; -use serde::{Deserialize, Serialize}; - -#[derive(Debug, Clone, Serialize)] -#[serde(rename_all = "camelCase")] -struct CacheSetStringRequest { - key: String, - value: String, - ttl_seconds: i64, -} - -#[derive(Debug, Clone, Deserialize)] -#[serde(rename_all = "camelCase")] -struct CacheSetStringResponse { - #[serde(default)] - error: Option, -} - -#[derive(Debug, Clone, Serialize)] -#[serde(rename_all = "camelCase")] -struct CacheGetStringRequest { - key: String, -} - -#[derive(Debug, Clone, Deserialize)] -#[serde(rename_all = "camelCase")] -struct CacheGetStringResponse { - #[serde(default)] - value: String, - #[serde(default)] - exists: bool, - #[serde(default)] - error: Option, -} - -#[derive(Debug, Clone, Serialize)] -#[serde(rename_all = "camelCase")] -struct CacheSetIntRequest { - key: String, - value: i64, - ttl_seconds: i64, -} - -#[derive(Debug, Clone, Deserialize)] -#[serde(rename_all = "camelCase")] -struct CacheSetIntResponse { - #[serde(default)] - error: Option, -} - -#[derive(Debug, Clone, Serialize)] -#[serde(rename_all = "camelCase")] -struct CacheGetIntRequest { - key: String, -} - -#[derive(Debug, Clone, Deserialize)] -#[serde(rename_all = "camelCase")] -struct CacheGetIntResponse { - #[serde(default)] - value: i64, - #[serde(default)] - exists: bool, - #[serde(default)] - error: Option, -} - -#[derive(Debug, Clone, Serialize)] -#[serde(rename_all = "camelCase")] -struct CacheSetFloatRequest { - key: String, - value: f64, - ttl_seconds: i64, -} - -#[derive(Debug, Clone, Deserialize)] -#[serde(rename_all = "camelCase")] -struct CacheSetFloatResponse { - #[serde(default)] - error: Option, -} - -#[derive(Debug, Clone, Serialize)] -#[serde(rename_all = "camelCase")] -struct CacheGetFloatRequest { - key: String, -} - -#[derive(Debug, Clone, Deserialize)] -#[serde(rename_all = "camelCase")] -struct CacheGetFloatResponse { - #[serde(default)] - value: f64, - #[serde(default)] - exists: bool, - #[serde(default)] - error: Option, -} - -#[derive(Debug, Clone, Serialize)] -#[serde(rename_all = "camelCase")] -struct CacheSetBytesRequest { - key: String, - value: Vec, - ttl_seconds: i64, -} - -#[derive(Debug, Clone, Deserialize)] -#[serde(rename_all = "camelCase")] -struct CacheSetBytesResponse { - #[serde(default)] - error: Option, -} - -#[derive(Debug, Clone, Serialize)] -#[serde(rename_all = "camelCase")] -struct CacheGetBytesRequest { - key: String, -} - -#[derive(Debug, Clone, Deserialize)] -#[serde(rename_all = "camelCase")] -struct CacheGetBytesResponse { - #[serde(default)] - value: Vec, - #[serde(default)] - exists: bool, - #[serde(default)] - error: Option, -} - -#[derive(Debug, Clone, Serialize)] -#[serde(rename_all = "camelCase")] -struct CacheHasRequest { - key: String, -} - -#[derive(Debug, Clone, Deserialize)] -#[serde(rename_all = "camelCase")] -struct CacheHasResponse { - #[serde(default)] - exists: bool, - #[serde(default)] - error: Option, -} - -#[derive(Debug, Clone, Serialize)] -#[serde(rename_all = "camelCase")] -struct CacheRemoveRequest { - key: String, -} - -#[derive(Debug, Clone, Deserialize)] -#[serde(rename_all = "camelCase")] -struct CacheRemoveResponse { - #[serde(default)] - error: Option, -} - -#[host_fn] -extern "ExtismHost" { - fn cache_setstring(input: Json) -> Json; - fn cache_getstring(input: Json) -> Json; - fn cache_setint(input: Json) -> Json; - fn cache_getint(input: Json) -> Json; - fn cache_setfloat(input: Json) -> Json; - fn cache_getfloat(input: Json) -> Json; - fn cache_setbytes(input: Json) -> Json; - fn cache_getbytes(input: Json) -> Json; - fn cache_has(input: Json) -> Json; - fn cache_remove(input: Json) -> Json; -} - -/// 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. -/// -/// # Arguments -/// * `key` - String parameter. -/// * `value` - String parameter. -/// * `ttl_seconds` - i64 parameter. -/// -/// # Errors -/// Returns an error if the host function call fails. -pub fn set_string(key: &str, value: &str, ttl_seconds: i64) -> Result<(), Error> { - let response = unsafe { - cache_setstring(Json(CacheSetStringRequest { - key: key.to_owned(), - value: value.to_owned(), - ttl_seconds: ttl_seconds, - }))? - }; - - if let Some(err) = response.0.error { - return Err(Error::msg(err)); - } - - Ok(()) -} - -/// 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. -/// -/// # Arguments -/// * `key` - String parameter. -/// -/// # Returns -/// A tuple of (value, exists). -/// -/// # Errors -/// Returns an error if the host function call fails. -pub fn get_string(key: &str) -> Result<(String, bool), Error> { - let response = unsafe { - cache_getstring(Json(CacheGetStringRequest { - key: key.to_owned(), - }))? - }; - - if let Some(err) = response.0.error { - return Err(Error::msg(err)); - } - - Ok((response.0.value, response.0.exists)) -} - -/// 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. -/// -/// # Arguments -/// * `key` - String parameter. -/// * `value` - i64 parameter. -/// * `ttl_seconds` - i64 parameter. -/// -/// # Errors -/// Returns an error if the host function call fails. -pub fn set_int(key: &str, value: i64, ttl_seconds: i64) -> Result<(), Error> { - let response = unsafe { - cache_setint(Json(CacheSetIntRequest { - key: key.to_owned(), - value: value, - ttl_seconds: ttl_seconds, - }))? - }; - - if let Some(err) = response.0.error { - return Err(Error::msg(err)); - } - - Ok(()) -} - -/// 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. -/// -/// # Arguments -/// * `key` - String parameter. -/// -/// # Returns -/// A tuple of (value, exists). -/// -/// # Errors -/// Returns an error if the host function call fails. -pub fn get_int(key: &str) -> Result<(i64, bool), Error> { - let response = unsafe { - cache_getint(Json(CacheGetIntRequest { - key: key.to_owned(), - }))? - }; - - if let Some(err) = response.0.error { - return Err(Error::msg(err)); - } - - Ok((response.0.value, response.0.exists)) -} - -/// 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. -/// -/// # Arguments -/// * `key` - String parameter. -/// * `value` - f64 parameter. -/// * `ttl_seconds` - i64 parameter. -/// -/// # Errors -/// Returns an error if the host function call fails. -pub fn set_float(key: &str, value: f64, ttl_seconds: i64) -> Result<(), Error> { - let response = unsafe { - cache_setfloat(Json(CacheSetFloatRequest { - key: key.to_owned(), - value: value, - ttl_seconds: ttl_seconds, - }))? - }; - - if let Some(err) = response.0.error { - return Err(Error::msg(err)); - } - - Ok(()) -} - -/// 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. -/// -/// # Arguments -/// * `key` - String parameter. -/// -/// # Returns -/// A tuple of (value, exists). -/// -/// # Errors -/// Returns an error if the host function call fails. -pub fn get_float(key: &str) -> Result<(f64, bool), Error> { - let response = unsafe { - cache_getfloat(Json(CacheGetFloatRequest { - key: key.to_owned(), - }))? - }; - - if let Some(err) = response.0.error { - return Err(Error::msg(err)); - } - - Ok((response.0.value, response.0.exists)) -} - -/// 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. -/// -/// # Arguments -/// * `key` - String parameter. -/// * `value` - Vec parameter. -/// * `ttl_seconds` - i64 parameter. -/// -/// # Errors -/// Returns an error if the host function call fails. -pub fn set_bytes(key: &str, value: Vec, ttl_seconds: i64) -> Result<(), Error> { - let response = unsafe { - cache_setbytes(Json(CacheSetBytesRequest { - key: key.to_owned(), - value: value, - ttl_seconds: ttl_seconds, - }))? - }; - - if let Some(err) = response.0.error { - return Err(Error::msg(err)); - } - - Ok(()) -} - -/// 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. -/// -/// # Arguments -/// * `key` - String parameter. -/// -/// # Returns -/// A tuple of (value, exists). -/// -/// # Errors -/// Returns an error if the host function call fails. -pub fn get_bytes(key: &str) -> Result<(Vec, bool), Error> { - let response = unsafe { - cache_getbytes(Json(CacheGetBytesRequest { - key: key.to_owned(), - }))? - }; - - if let Some(err) = response.0.error { - return Err(Error::msg(err)); - } - - Ok((response.0.value, response.0.exists)) -} - -/// 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. -/// -/// # Arguments -/// * `key` - String parameter. -/// -/// # Returns -/// The exists value. -/// -/// # Errors -/// Returns an error if the host function call fails. -pub fn has(key: &str) -> Result { - let response = unsafe { - cache_has(Json(CacheHasRequest { - key: key.to_owned(), - }))? - }; - - if let Some(err) = response.0.error { - return Err(Error::msg(err)); - } - - Ok(response.0.exists) -} - -/// 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. -/// -/// # Arguments -/// * `key` - String parameter. -/// -/// # Errors -/// Returns an error if the host function call fails. -pub fn remove(key: &str) -> Result<(), Error> { - let response = unsafe { - cache_remove(Json(CacheRemoveRequest { - key: key.to_owned(), - }))? - }; - - if let Some(err) = response.0.error { - return Err(Error::msg(err)); - } - - Ok(()) -} diff --git a/plugins/host/rust/nd_host_kvstore.rs b/plugins/host/rust/nd_host_kvstore.rs deleted file mode 100644 index 5c25a7e9b..000000000 --- a/plugins/host/rust/nd_host_kvstore.rs +++ /dev/null @@ -1,261 +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 extism-pdk. - -use extism_pdk::*; -use serde::{Deserialize, Serialize}; - -#[derive(Debug, Clone, Serialize)] -#[serde(rename_all = "camelCase")] -struct KVStoreSetRequest { - key: String, - value: Vec, -} - -#[derive(Debug, Clone, Deserialize)] -#[serde(rename_all = "camelCase")] -struct KVStoreSetResponse { - #[serde(default)] - error: Option, -} - -#[derive(Debug, Clone, Serialize)] -#[serde(rename_all = "camelCase")] -struct KVStoreGetRequest { - key: String, -} - -#[derive(Debug, Clone, Deserialize)] -#[serde(rename_all = "camelCase")] -struct KVStoreGetResponse { - #[serde(default)] - value: Vec, - #[serde(default)] - exists: bool, - #[serde(default)] - error: Option, -} - -#[derive(Debug, Clone, Serialize)] -#[serde(rename_all = "camelCase")] -struct KVStoreDeleteRequest { - key: String, -} - -#[derive(Debug, Clone, Deserialize)] -#[serde(rename_all = "camelCase")] -struct KVStoreDeleteResponse { - #[serde(default)] - error: Option, -} - -#[derive(Debug, Clone, Serialize)] -#[serde(rename_all = "camelCase")] -struct KVStoreHasRequest { - key: String, -} - -#[derive(Debug, Clone, Deserialize)] -#[serde(rename_all = "camelCase")] -struct KVStoreHasResponse { - #[serde(default)] - exists: bool, - #[serde(default)] - error: Option, -} - -#[derive(Debug, Clone, Serialize)] -#[serde(rename_all = "camelCase")] -struct KVStoreListRequest { - prefix: String, -} - -#[derive(Debug, Clone, Deserialize)] -#[serde(rename_all = "camelCase")] -struct KVStoreListResponse { - #[serde(default)] - keys: Vec, - #[serde(default)] - error: Option, -} - -#[derive(Debug, Clone, Deserialize)] -#[serde(rename_all = "camelCase")] -struct KVStoreGetStorageUsedResponse { - #[serde(default)] - bytes: i64, - #[serde(default)] - error: Option, -} - -#[host_fn] -extern "ExtismHost" { - fn kvstore_set(input: Json) -> Json; - fn kvstore_get(input: Json) -> Json; - fn kvstore_delete(input: Json) -> Json; - fn kvstore_has(input: Json) -> Json; - fn kvstore_list(input: Json) -> Json; - fn kvstore_getstorageused(input: Json) -> Json; -} - -/// 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. -/// -/// # Arguments -/// * `key` - String parameter. -/// * `value` - Vec parameter. -/// -/// # Errors -/// Returns an error if the host function call fails. -pub fn set(key: &str, value: Vec) -> Result<(), Error> { - let response = unsafe { - kvstore_set(Json(KVStoreSetRequest { - key: key.to_owned(), - value: value, - }))? - }; - - if let Some(err) = response.0.error { - return Err(Error::msg(err)); - } - - Ok(()) -} - -/// Get retrieves a byte value from storage. -/// -/// Parameters: -/// - key: The storage key -/// -/// Returns the value and whether the key exists. -/// -/// # Arguments -/// * `key` - String parameter. -/// -/// # Returns -/// A tuple of (value, exists). -/// -/// # Errors -/// Returns an error if the host function call fails. -pub fn get(key: &str) -> Result<(Vec, bool), Error> { - let response = unsafe { - kvstore_get(Json(KVStoreGetRequest { - key: key.to_owned(), - }))? - }; - - if let Some(err) = response.0.error { - return Err(Error::msg(err)); - } - - Ok((response.0.value, response.0.exists)) -} - -/// 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. -/// -/// # Arguments -/// * `key` - String parameter. -/// -/// # Errors -/// Returns an error if the host function call fails. -pub fn delete(key: &str) -> Result<(), Error> { - let response = unsafe { - kvstore_delete(Json(KVStoreDeleteRequest { - key: key.to_owned(), - }))? - }; - - if let Some(err) = response.0.error { - return Err(Error::msg(err)); - } - - Ok(()) -} - -/// Has checks if a key exists in storage. -/// -/// Parameters: -/// - key: The storage key -/// -/// Returns true if the key exists. -/// -/// # Arguments -/// * `key` - String parameter. -/// -/// # Returns -/// The exists value. -/// -/// # Errors -/// Returns an error if the host function call fails. -pub fn has(key: &str) -> Result { - let response = unsafe { - kvstore_has(Json(KVStoreHasRequest { - key: key.to_owned(), - }))? - }; - - if let Some(err) = response.0.error { - return Err(Error::msg(err)); - } - - Ok(response.0.exists) -} - -/// 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. -/// -/// # Arguments -/// * `prefix` - String parameter. -/// -/// # Returns -/// The keys value. -/// -/// # Errors -/// Returns an error if the host function call fails. -pub fn list(prefix: &str) -> Result, Error> { - let response = unsafe { - kvstore_list(Json(KVStoreListRequest { - prefix: prefix.to_owned(), - }))? - }; - - if let Some(err) = response.0.error { - return Err(Error::msg(err)); - } - - Ok(response.0.keys) -} - -/// GetStorageUsed returns the total storage used by this plugin in bytes. -/// -/// # Returns -/// The bytes value. -/// -/// # Errors -/// Returns an error if the host function call fails. -pub fn get_storage_used() -> Result { - let response = unsafe { - kvstore_getstorageused(Json(serde_json::json!({})))? - }; - - if let Some(err) = response.0.error { - return Err(Error::msg(err)); - } - - Ok(response.0.bytes) -} diff --git a/plugins/host/rust/nd_host_library.rs b/plugins/host/rust/nd_host_library.rs deleted file mode 100644 index ca6bf958d..000000000 --- a/plugins/host/rust/nd_host_library.rs +++ /dev/null @@ -1,105 +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 extism-pdk. - -use extism_pdk::*; -use serde::{Deserialize, Serialize}; - -/// Library represents a music library with metadata. -#[derive(Debug, Clone, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct Library { - pub id: i32, - pub name: String, - #[serde(default)] - pub path: String, - #[serde(default)] - pub mount_point: String, - pub last_scan_at: i64, - pub total_songs: i32, - pub total_albums: i32, - pub total_artists: i32, - pub total_size: i64, - pub total_duration: f64, -} - -#[derive(Debug, Clone, Serialize)] -#[serde(rename_all = "camelCase")] -struct LibraryGetLibraryRequest { - id: i32, -} - -#[derive(Debug, Clone, Deserialize)] -#[serde(rename_all = "camelCase")] -struct LibraryGetLibraryResponse { - #[serde(default)] - result: Option, - #[serde(default)] - error: Option, -} - -#[derive(Debug, Clone, Deserialize)] -#[serde(rename_all = "camelCase")] -struct LibraryGetAllLibrariesResponse { - #[serde(default)] - result: Vec, - #[serde(default)] - error: Option, -} - -#[host_fn] -extern "ExtismHost" { - fn library_getlibrary(input: Json) -> Json; - fn library_getalllibraries(input: Json) -> Json; -} - -/// 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. -/// -/// # Arguments -/// * `id` - i32 parameter. -/// -/// # Returns -/// The result value. -/// -/// # Errors -/// Returns an error if the host function call fails. -pub fn get_library(id: i32) -> Result, Error> { - let response = unsafe { - library_getlibrary(Json(LibraryGetLibraryRequest { - id: id, - }))? - }; - - if let Some(err) = response.0.error { - return Err(Error::msg(err)); - } - - Ok(response.0.result) -} - -/// GetAllLibraries retrieves metadata for all configured libraries. -/// -/// Returns a slice of all libraries with their metadata. -/// -/// # Returns -/// The result value. -/// -/// # Errors -/// Returns an error if the host function call fails. -pub fn get_all_libraries() -> Result, Error> { - let response = unsafe { - library_getalllibraries(Json(serde_json::json!({})))? - }; - - if let Some(err) = response.0.error { - return Err(Error::msg(err)); - } - - Ok(response.0.result) -} diff --git a/plugins/host/rust/nd_host_scheduler.rs b/plugins/host/rust/nd_host_scheduler.rs deleted file mode 100644 index 9578463a9..000000000 --- a/plugins/host/rust/nd_host_scheduler.rs +++ /dev/null @@ -1,159 +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 extism-pdk. - -use extism_pdk::*; -use serde::{Deserialize, Serialize}; - -#[derive(Debug, Clone, Serialize)] -#[serde(rename_all = "camelCase")] -struct SchedulerScheduleOneTimeRequest { - delay_seconds: i32, - payload: String, - schedule_id: String, -} - -#[derive(Debug, Clone, Deserialize)] -#[serde(rename_all = "camelCase")] -struct SchedulerScheduleOneTimeResponse { - #[serde(default)] - new_schedule_id: String, - #[serde(default)] - error: Option, -} - -#[derive(Debug, Clone, Serialize)] -#[serde(rename_all = "camelCase")] -struct SchedulerScheduleRecurringRequest { - cron_expression: String, - payload: String, - schedule_id: String, -} - -#[derive(Debug, Clone, Deserialize)] -#[serde(rename_all = "camelCase")] -struct SchedulerScheduleRecurringResponse { - #[serde(default)] - new_schedule_id: String, - #[serde(default)] - error: Option, -} - -#[derive(Debug, Clone, Serialize)] -#[serde(rename_all = "camelCase")] -struct SchedulerCancelScheduleRequest { - schedule_id: String, -} - -#[derive(Debug, Clone, Deserialize)] -#[serde(rename_all = "camelCase")] -struct SchedulerCancelScheduleResponse { - #[serde(default)] - error: Option, -} - -#[host_fn] -extern "ExtismHost" { - fn scheduler_scheduleonetime(input: Json) -> Json; - fn scheduler_schedulerecurring(input: Json) -> Json; - fn scheduler_cancelschedule(input: Json) -> Json; -} - -/// 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. -/// -/// # Arguments -/// * `delay_seconds` - i32 parameter. -/// * `payload` - String parameter. -/// * `schedule_id` - String parameter. -/// -/// # Returns -/// The new_schedule_id value. -/// -/// # Errors -/// Returns an error if the host function call fails. -pub fn schedule_one_time(delay_seconds: i32, payload: &str, schedule_id: &str) -> Result { - let response = unsafe { - scheduler_scheduleonetime(Json(SchedulerScheduleOneTimeRequest { - delay_seconds: delay_seconds, - payload: payload.to_owned(), - schedule_id: schedule_id.to_owned(), - }))? - }; - - if let Some(err) = response.0.error { - return Err(Error::msg(err)); - } - - Ok(response.0.new_schedule_id) -} - -/// 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. -/// -/// # Arguments -/// * `cron_expression` - String parameter. -/// * `payload` - String parameter. -/// * `schedule_id` - String parameter. -/// -/// # Returns -/// The new_schedule_id value. -/// -/// # Errors -/// Returns an error if the host function call fails. -pub fn schedule_recurring(cron_expression: &str, payload: &str, schedule_id: &str) -> Result { - let response = unsafe { - scheduler_schedulerecurring(Json(SchedulerScheduleRecurringRequest { - cron_expression: cron_expression.to_owned(), - payload: payload.to_owned(), - schedule_id: schedule_id.to_owned(), - }))? - }; - - if let Some(err) = response.0.error { - return Err(Error::msg(err)); - } - - Ok(response.0.new_schedule_id) -} - -/// 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. -/// -/// # Arguments -/// * `schedule_id` - String parameter. -/// -/// # Errors -/// Returns an error if the host function call fails. -pub fn cancel_schedule(schedule_id: &str) -> Result<(), Error> { - let response = unsafe { - scheduler_cancelschedule(Json(SchedulerCancelScheduleRequest { - schedule_id: schedule_id.to_owned(), - }))? - }; - - if let Some(err) = response.0.error { - return Err(Error::msg(err)); - } - - Ok(()) -} diff --git a/plugins/host/rust/nd_host_subsonicapi.rs b/plugins/host/rust/nd_host_subsonicapi.rs deleted file mode 100644 index d96995189..000000000 --- a/plugins/host/rust/nd_host_subsonicapi.rs +++ /dev/null @@ -1,54 +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 extism-pdk. - -use extism_pdk::*; -use serde::{Deserialize, Serialize}; - -#[derive(Debug, Clone, Serialize)] -#[serde(rename_all = "camelCase")] -struct SubsonicAPICallRequest { - uri: String, -} - -#[derive(Debug, Clone, Deserialize)] -#[serde(rename_all = "camelCase")] -struct SubsonicAPICallResponse { - #[serde(default)] - response_json: String, - #[serde(default)] - error: Option, -} - -#[host_fn] -extern "ExtismHost" { - fn subsonicapi_call(input: Json) -> Json; -} - -/// 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. -/// -/// # Arguments -/// * `uri` - String parameter. -/// -/// # Returns -/// The response_json value. -/// -/// # Errors -/// Returns an error if the host function call fails. -pub fn call(uri: &str) -> Result { - let response = unsafe { - subsonicapi_call(Json(SubsonicAPICallRequest { - uri: uri.to_owned(), - }))? - }; - - if let Some(err) = response.0.error { - return Err(Error::msg(err)); - } - - Ok(response.0.response_json) -} diff --git a/plugins/host/rust/nd_host_websocket.rs b/plugins/host/rust/nd_host_websocket.rs deleted file mode 100644 index 61392072a..000000000 --- a/plugins/host/rust/nd_host_websocket.rs +++ /dev/null @@ -1,204 +0,0 @@ -// Code generated by hostgen. DO NOT EDIT. -// -// This file contains client wrappers for the WebSocket host service. -// It is intended for use in Navidrome plugins built with extism-pdk. - -use extism_pdk::*; -use serde::{Deserialize, Serialize}; - -#[derive(Debug, Clone, Serialize)] -#[serde(rename_all = "camelCase")] -struct WebSocketConnectRequest { - url: String, - headers: std::collections::HashMap, - connection_id: String, -} - -#[derive(Debug, Clone, Deserialize)] -#[serde(rename_all = "camelCase")] -struct WebSocketConnectResponse { - #[serde(default)] - new_connection_id: String, - #[serde(default)] - error: Option, -} - -#[derive(Debug, Clone, Serialize)] -#[serde(rename_all = "camelCase")] -struct WebSocketSendTextRequest { - connection_id: String, - message: String, -} - -#[derive(Debug, Clone, Deserialize)] -#[serde(rename_all = "camelCase")] -struct WebSocketSendTextResponse { - #[serde(default)] - error: Option, -} - -#[derive(Debug, Clone, Serialize)] -#[serde(rename_all = "camelCase")] -struct WebSocketSendBinaryRequest { - connection_id: String, - data: Vec, -} - -#[derive(Debug, Clone, Deserialize)] -#[serde(rename_all = "camelCase")] -struct WebSocketSendBinaryResponse { - #[serde(default)] - error: Option, -} - -#[derive(Debug, Clone, Serialize)] -#[serde(rename_all = "camelCase")] -struct WebSocketCloseConnectionRequest { - connection_id: String, - code: i32, - reason: String, -} - -#[derive(Debug, Clone, Deserialize)] -#[serde(rename_all = "camelCase")] -struct WebSocketCloseConnectionResponse { - #[serde(default)] - error: Option, -} - -#[host_fn] -extern "ExtismHost" { - fn websocket_connect(input: Json) -> Json; - fn websocket_sendtext(input: Json) -> Json; - fn websocket_sendbinary(input: Json) -> Json; - fn websocket_closeconnection(input: Json) -> Json; -} - -/// Connect establishes a WebSocket connection to the specified URL. -/// -/// Plugins that use this function must also implement the WebSocketCallback capability -/// to receive incoming messages and connection events. -/// -/// Parameters: -/// - url: The WebSocket URL to connect to (ws:// or wss://) -/// - headers: Optional HTTP headers to include in the handshake request -/// - connectionID: Optional unique identifier for the connection. If empty, one will be generated -/// -/// Returns the connection ID that can be used to send messages or close the connection, -/// or an error if the connection fails. -/// -/// # Arguments -/// * `url` - String parameter. -/// * `headers` - std::collections::HashMap parameter. -/// * `connection_id` - String parameter. -/// -/// # Returns -/// The new_connection_id value. -/// -/// # Errors -/// Returns an error if the host function call fails. -pub fn connect(url: &str, headers: std::collections::HashMap, connection_id: &str) -> Result { - let response = unsafe { - websocket_connect(Json(WebSocketConnectRequest { - url: url.to_owned(), - headers: headers, - connection_id: connection_id.to_owned(), - }))? - }; - - if let Some(err) = response.0.error { - return Err(Error::msg(err)); - } - - Ok(response.0.new_connection_id) -} - -/// SendText sends a text message over an established WebSocket connection. -/// -/// Parameters: -/// - connectionID: The connection identifier returned by Connect -/// - message: The text message to send -/// -/// Returns an error if the connection is not found or if sending fails. -/// -/// # Arguments -/// * `connection_id` - String parameter. -/// * `message` - String parameter. -/// -/// # Errors -/// Returns an error if the host function call fails. -pub fn send_text(connection_id: &str, message: &str) -> Result<(), Error> { - let response = unsafe { - websocket_sendtext(Json(WebSocketSendTextRequest { - connection_id: connection_id.to_owned(), - message: message.to_owned(), - }))? - }; - - if let Some(err) = response.0.error { - return Err(Error::msg(err)); - } - - Ok(()) -} - -/// SendBinary sends binary data over an established WebSocket connection. -/// -/// Parameters: -/// - connectionID: The connection identifier returned by Connect -/// - data: The binary data to send -/// -/// Returns an error if the connection is not found or if sending fails. -/// -/// # Arguments -/// * `connection_id` - String parameter. -/// * `data` - Vec parameter. -/// -/// # Errors -/// Returns an error if the host function call fails. -pub fn send_binary(connection_id: &str, data: Vec) -> Result<(), Error> { - let response = unsafe { - websocket_sendbinary(Json(WebSocketSendBinaryRequest { - connection_id: connection_id.to_owned(), - data: data, - }))? - }; - - if let Some(err) = response.0.error { - return Err(Error::msg(err)); - } - - Ok(()) -} - -/// CloseConnection gracefully closes a WebSocket connection. -/// -/// Parameters: -/// - connectionID: The connection identifier returned by Connect -/// - code: WebSocket close status code (e.g., 1000 for normal closure) -/// - reason: Optional human-readable reason for closing -/// -/// Returns an error if the connection is not found or if closing fails. -/// -/// # Arguments -/// * `connection_id` - String parameter. -/// * `code` - i32 parameter. -/// * `reason` - String parameter. -/// -/// # Errors -/// Returns an error if the host function call fails. -pub fn close_connection(connection_id: &str, code: i32, reason: &str) -> Result<(), Error> { - let response = unsafe { - websocket_closeconnection(Json(WebSocketCloseConnectionRequest { - connection_id: connection_id.to_owned(), - code: code, - reason: reason.to_owned(), - }))? - }; - - if let Some(err) = response.0.error { - return Err(Error::msg(err)); - } - - Ok(()) -} diff --git a/plugins/host/scheduler_gen.go b/plugins/host/scheduler_gen.go index bf24a30d4..d3845419c 100644 --- a/plugins/host/scheduler_gen.go +++ b/plugins/host/scheduler_gen.go @@ -1,4 +1,4 @@ -// Code generated by hostgen. DO NOT EDIT. +// Code generated by ndpgen. DO NOT EDIT. package host diff --git a/plugins/host/subsonicapi_gen.go b/plugins/host/subsonicapi_gen.go index 32d5ee709..e3c2af7bd 100644 --- a/plugins/host/subsonicapi_gen.go +++ b/plugins/host/subsonicapi_gen.go @@ -1,4 +1,4 @@ -// Code generated by hostgen. DO NOT EDIT. +// Code generated by ndpgen. DO NOT EDIT. package host diff --git a/plugins/host/websocket_gen.go b/plugins/host/websocket_gen.go index abba57066..b7b630674 100644 --- a/plugins/host/websocket_gen.go +++ b/plugins/host/websocket_gen.go @@ -1,4 +1,4 @@ -// Code generated by hostgen. DO NOT EDIT. +// Code generated by ndpgen. DO NOT EDIT. package host diff --git a/plugins/host/go/README.md b/plugins/pdk/go/host/README.md similarity index 78% rename from plugins/host/go/README.md rename to plugins/pdk/go/host/README.md index 97a30ae56..f7efebdd0 100644 --- a/plugins/host/go/README.md +++ b/plugins/pdk/go/host/README.md @@ -5,13 +5,12 @@ These wrappers provide idiomatic Go APIs for interacting with Navidrome from WAS ## ⚠️ Auto-Generated Code -**Do not edit these files manually.** They are generated by the `hostgen` tool. +**Do not edit these files manually.** They are generated by the `ndpgen` tool. To regenerate: ```bash -cd plugins/host -go generate +make generate-pdk ``` ## Usage @@ -19,9 +18,9 @@ go generate Add this module as a dependency in your plugin's `go.mod`: ```go -require github.com/navidrome/navidrome/plugins/host/go v0.0.0 +require 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 ``` Then import the package in your plugin code: @@ -30,25 +29,25 @@ Then import the package in your plugin code: package main import ( - ndhost "github.com/navidrome/navidrome/plugins/host/go" + host "github.com/navidrome/navidrome/plugins/pdk/go/host" "github.com/extism/go-pdk" ) func myPluginFunction() error { // Use the cache service - _, err := ndhost.CacheSetString("my_key", "my_value", 3600) + _, err := host.CacheSetString("my_key", "my_value", 3600) if err != nil { return err } // Schedule a recurring task - _, err = ndhost.SchedulerScheduleRecurring("@every 5m", "payload", "task_id") + _, err = host.SchedulerScheduleRecurring("@every 5m", "payload", "task_id") if err != nil { return err } // Access library data with typed structs - resp, err := ndhost.LibraryGetAllLibraries() + resp, err := host.LibraryGetAllLibraries() if err != nil { return err } @@ -69,7 +68,7 @@ autocompletion. For example, the library service provides a `Library` struct: ```go -resp, err := ndhost.LibraryGetAllLibraries() +resp, err := host.LibraryGetAllLibraries() if err != nil { return err } @@ -97,5 +96,3 @@ Go plugins must be compiled to WebAssembly using TinyGo: ```bash tinygo build -o plugin.wasm -target=wasip1 -buildmode=c-shared . ``` - -See the [discord-rich-presence](../../examples/discord-rich-presence/) example for a complete plugin implementation. diff --git a/plugins/pdk/go/host/go.sum b/plugins/pdk/go/host/go.sum new file mode 100644 index 000000000..280f3c901 --- /dev/null +++ b/plugins/pdk/go/host/go.sum @@ -0,0 +1 @@ +github.com/extism/go-pdk v1.1.3/go.mod h1:Gz+LIU/YCKnKXhgge8yo5Yu1F/lbv7KtKFkiCSzW/P4= diff --git a/plugins/pdk/python/host/nd_host_artwork.py b/plugins/pdk/python/host/nd_host_artwork.py index 3919dbbe9..9bcb529ae 100644 --- a/plugins/pdk/python/host/nd_host_artwork.py +++ b/plugins/pdk/python/host/nd_host_artwork.py @@ -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 Artwork host service. # It is intended for use in Navidrome plugins built with extism-py. diff --git a/plugins/pdk/python/host/nd_host_cache.py b/plugins/pdk/python/host/nd_host_cache.py index f06b2ad0c..c22f95f6f 100644 --- a/plugins/pdk/python/host/nd_host_cache.py +++ b/plugins/pdk/python/host/nd_host_cache.py @@ -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 Cache host service. # It is intended for use in Navidrome plugins built with extism-py. diff --git a/plugins/pdk/python/host/nd_host_kvstore.py b/plugins/pdk/python/host/nd_host_kvstore.py index f1a6e5724..5485d2fb5 100644 --- a/plugins/pdk/python/host/nd_host_kvstore.py +++ b/plugins/pdk/python/host/nd_host_kvstore.py @@ -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 KVStore host service. # It is intended for use in Navidrome plugins built with extism-py. diff --git a/plugins/pdk/python/host/nd_host_library.py b/plugins/pdk/python/host/nd_host_library.py index 4111df2b9..12e1bc4eb 100644 --- a/plugins/pdk/python/host/nd_host_library.py +++ b/plugins/pdk/python/host/nd_host_library.py @@ -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 Library host service. # It is intended for use in Navidrome plugins built with extism-py. diff --git a/plugins/pdk/python/host/nd_host_scheduler.py b/plugins/pdk/python/host/nd_host_scheduler.py index 3a94da94d..7f0d19241 100644 --- a/plugins/pdk/python/host/nd_host_scheduler.py +++ b/plugins/pdk/python/host/nd_host_scheduler.py @@ -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 Scheduler host service. # It is intended for use in Navidrome plugins built with extism-py. diff --git a/plugins/pdk/python/host/nd_host_subsonicapi.py b/plugins/pdk/python/host/nd_host_subsonicapi.py index 4eaf06d4c..ee6b543fa 100644 --- a/plugins/pdk/python/host/nd_host_subsonicapi.py +++ b/plugins/pdk/python/host/nd_host_subsonicapi.py @@ -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 SubsonicAPI host service. # It is intended for use in Navidrome plugins built with extism-py. diff --git a/plugins/pdk/python/host/nd_host_websocket.py b/plugins/pdk/python/host/nd_host_websocket.py index 67938f41f..b62ee792c 100644 --- a/plugins/pdk/python/host/nd_host_websocket.py +++ b/plugins/pdk/python/host/nd_host_websocket.py @@ -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 WebSocket host service. # It is intended for use in Navidrome plugins built with extism-py. diff --git a/plugins/pdk/rust/host/README.md b/plugins/pdk/rust/host/README.md index 8aa625e1c..45463575d 100644 --- a/plugins/pdk/rust/host/README.md +++ b/plugins/pdk/rust/host/README.md @@ -5,13 +5,12 @@ These wrappers provide idiomatic Rust APIs for interacting with Navidrome from W ## ⚠️ Auto-Generated Code -**Do not edit these files manually.** They are generated by the `hostgen` tool. +**Do not edit these files manually.** They are generated by the `ndpgen` tool. To regenerate: ```bash -cd plugins/host -go generate +make generate-pdk ``` ## Usage @@ -20,7 +19,7 @@ Add this crate as a dependency in your plugin's `Cargo.toml`: ```toml [dependencies] -nd-host = { path = "../../host/rust" } +nd-host = { path = "../../pdk/rust/host" } ``` Then import the services you need: diff --git a/plugins/pdk/rust/host/lib.rs b/plugins/pdk/rust/host/lib.rs index b71861398..7fabce3d5 100644 --- a/plugins/pdk/rust/host/lib.rs +++ b/plugins/pdk/rust/host/lib.rs @@ -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 //! diff --git a/plugins/pdk/rust/host/nd_host_artwork.rs b/plugins/pdk/rust/host/nd_host_artwork.rs index 31ccb4370..e565e0956 100644 --- a/plugins/pdk/rust/host/nd_host_artwork.rs +++ b/plugins/pdk/rust/host/nd_host_artwork.rs @@ -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 Artwork host service. // It is intended for use in Navidrome plugins built with extism-pdk. diff --git a/plugins/pdk/rust/host/nd_host_cache.rs b/plugins/pdk/rust/host/nd_host_cache.rs index f223c8de8..d9baa7c56 100644 --- a/plugins/pdk/rust/host/nd_host_cache.rs +++ b/plugins/pdk/rust/host/nd_host_cache.rs @@ -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 Cache host service. // It is intended for use in Navidrome plugins built with extism-pdk. diff --git a/plugins/pdk/rust/host/nd_host_kvstore.rs b/plugins/pdk/rust/host/nd_host_kvstore.rs index 5c25a7e9b..6e5b79f2b 100644 --- a/plugins/pdk/rust/host/nd_host_kvstore.rs +++ b/plugins/pdk/rust/host/nd_host_kvstore.rs @@ -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 KVStore host service. // It is intended for use in Navidrome plugins built with extism-pdk. diff --git a/plugins/pdk/rust/host/nd_host_library.rs b/plugins/pdk/rust/host/nd_host_library.rs index ca6bf958d..b4b9b3fb0 100644 --- a/plugins/pdk/rust/host/nd_host_library.rs +++ b/plugins/pdk/rust/host/nd_host_library.rs @@ -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 Library host service. // It is intended for use in Navidrome plugins built with extism-pdk. diff --git a/plugins/pdk/rust/host/nd_host_scheduler.rs b/plugins/pdk/rust/host/nd_host_scheduler.rs index 9578463a9..042f97410 100644 --- a/plugins/pdk/rust/host/nd_host_scheduler.rs +++ b/plugins/pdk/rust/host/nd_host_scheduler.rs @@ -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 Scheduler host service. // It is intended for use in Navidrome plugins built with extism-pdk. diff --git a/plugins/pdk/rust/host/nd_host_subsonicapi.rs b/plugins/pdk/rust/host/nd_host_subsonicapi.rs index d96995189..e32b6d72f 100644 --- a/plugins/pdk/rust/host/nd_host_subsonicapi.rs +++ b/plugins/pdk/rust/host/nd_host_subsonicapi.rs @@ -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 SubsonicAPI host service. // It is intended for use in Navidrome plugins built with extism-pdk. diff --git a/plugins/pdk/rust/host/nd_host_websocket.rs b/plugins/pdk/rust/host/nd_host_websocket.rs index 61392072a..58a399028 100644 --- a/plugins/pdk/rust/host/nd_host_websocket.rs +++ b/plugins/pdk/rust/host/nd_host_websocket.rs @@ -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 WebSocket host service. // It is intended for use in Navidrome plugins built with extism-pdk.