mirror of
https://github.com/navidrome/navidrome.git
synced 2026-08-31 07:30:32 +00:00
feat(plugins): implementation of the Navidrome Plugin Development Kit with generated client wrappers and service interfaces - Phase 2 (2)
Signed-off-by: Deluan <deluan@navidrome.org>
This commit is contained in:
parent
6fa9ef0dfe
commit
6d3c29912b
@ -1,433 +0,0 @@
|
||||
# hostgen
|
||||
|
||||
A code generator for Navidrome's plugin host functions. It reads Go interface definitions with special annotations and generates Extism host function wrappers.
|
||||
|
||||
## Usage
|
||||
|
||||
```bash
|
||||
hostgen -input <dir> -output <dir> -package <name> [-v] [-dry-run] [-host-only] [-plugin-only]
|
||||
```
|
||||
|
||||
### Flags
|
||||
|
||||
| Flag | Description | Default |
|
||||
|----------------|----------------------------------------------------------------|----------|
|
||||
| `-input` | Directory containing Go source files with annotated interfaces | Required |
|
||||
| `-output` | Directory where generated files will be written | Required |
|
||||
| `-package` | Package name for generated files | Required |
|
||||
| `-v` | Verbose output | `false` |
|
||||
| `-dry-run` | Parse and validate without writing files | `false` |
|
||||
| `-host-only` | Generate only host-side wrapper code | `false` |
|
||||
| `-plugin-only` | Generate only plugin/client-side wrapper code | `false` |
|
||||
| `-go` | Generate Go client wrappers | `true`* |
|
||||
| `-python` | Generate Python client wrappers | `false` |
|
||||
| `-rust` | Generate Rust client wrappers | `false` |
|
||||
|
||||
\* `-go` is enabled by default when neither `-python` nor `-rust` is specified. Use combinations like `-go -python -rust` to generate multiple languages.
|
||||
|
||||
By default, both host and Go plugin code are generated. Use `-host-only` or `-plugin-only` to generate only one type. Use `-python` to generate Python wrappers and `-rust` to generate Rust wrappers.
|
||||
|
||||
### Example
|
||||
|
||||
```bash
|
||||
go run ./plugins/cmd/hostgen \
|
||||
-input ./plugins/host \
|
||||
-output ./plugins/host \
|
||||
-package host
|
||||
```
|
||||
|
||||
Or via `go generate` (recommended):
|
||||
|
||||
```go
|
||||
//go:generate go run ../cmd/hostgen -input . -output . -package host
|
||||
package host
|
||||
```
|
||||
|
||||
## Annotations
|
||||
|
||||
### `//nd:hostservice`
|
||||
|
||||
Marks an interface as a host service that will have wrappers generated.
|
||||
|
||||
```go
|
||||
//nd:hostservice name=<ServiceName> permission=<permission>
|
||||
type MyService interface { ... }
|
||||
```
|
||||
|
||||
| Parameter | Description | Required |
|
||||
|--------------|-----------------------------------------------------------------|----------|
|
||||
| `name` | Service name used in generated type names and function prefixes | Yes |
|
||||
| `permission` | Permission required by plugins to use this service | Yes |
|
||||
|
||||
### `//nd:hostfunc`
|
||||
|
||||
Marks a method within a host service interface for export to plugins.
|
||||
|
||||
```go
|
||||
//nd:hostfunc [name=<export_name>]
|
||||
MethodName(ctx context.Context, ...) (result Type, err error)
|
||||
```
|
||||
|
||||
| Parameter | Description | Required |
|
||||
|-----------|-------------------------------------------------------------------------|----------|
|
||||
| `name` | Custom export name (default: `<servicename>_<methodname>` in lowercase) | No |
|
||||
|
||||
## Input Format
|
||||
|
||||
Host service interfaces must follow these conventions:
|
||||
|
||||
1. **First parameter must be `context.Context`** - Required for all methods
|
||||
2. **Last return value should be `error`** - For proper error handling
|
||||
3. **Annotations must be on consecutive lines** - No blank comment lines between doc and annotation
|
||||
|
||||
### Example Interface
|
||||
|
||||
```go
|
||||
package host
|
||||
|
||||
import "context"
|
||||
|
||||
// SubsonicAPIService provides access to Navidrome's Subsonic API.
|
||||
// This documentation becomes part of the generated code.
|
||||
//nd:hostservice name=SubsonicAPI permission=subsonicapi
|
||||
type SubsonicAPIService interface {
|
||||
// Call executes a Subsonic API request and returns the response.
|
||||
//nd:hostfunc
|
||||
Call(ctx context.Context, uri string) (response string, err error)
|
||||
}
|
||||
```
|
||||
|
||||
## Generated Output
|
||||
|
||||
For each annotated interface, hostgen generates:
|
||||
|
||||
### Request/Response Types
|
||||
|
||||
```go
|
||||
// SubsonicAPICallRequest is the request type for SubsonicAPI.Call.
|
||||
type SubsonicAPICallRequest struct {
|
||||
Uri string `json:"uri"`
|
||||
}
|
||||
|
||||
// SubsonicAPICallResponse is the response type for SubsonicAPI.Call.
|
||||
type SubsonicAPICallResponse struct {
|
||||
Response string `json:"response,omitempty"`
|
||||
Error string `json:"error,omitempty"`
|
||||
}
|
||||
```
|
||||
|
||||
### Registration Function
|
||||
|
||||
```go
|
||||
// RegisterSubsonicAPIHostFunctions registers SubsonicAPI service host functions.
|
||||
func RegisterSubsonicAPIHostFunctions(service SubsonicAPIService) []extism.HostFunction {
|
||||
return []extism.HostFunction{
|
||||
newSubsonicAPICallHostFunction(service),
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Host Function Wrappers
|
||||
|
||||
Each method gets a wrapper that:
|
||||
1. Reads JSON request from plugin memory
|
||||
2. Unmarshals to the request type
|
||||
3. Calls the service method
|
||||
4. Marshals the response
|
||||
5. Writes JSON response to plugin memory
|
||||
|
||||
## Supported Types
|
||||
|
||||
hostgen supports these Go types in method signatures:
|
||||
|
||||
| Type | JSON Representation |
|
||||
|-------------------------------|------------------------------------------|
|
||||
| `string`, `int`, `bool`, etc. | Native JSON types |
|
||||
| `[]T` (slices) | JSON arrays |
|
||||
| `map[K]V` (maps) | JSON objects |
|
||||
| `*T` (pointers) | Nullable fields |
|
||||
| `interface{}` / `any` | Converts to `any` |
|
||||
| Custom structs | JSON objects (must be JSON-serializable) |
|
||||
|
||||
### Multiple Return Values
|
||||
|
||||
Methods can return multiple values (plus error):
|
||||
|
||||
```go
|
||||
//nd:hostfunc
|
||||
Search(ctx context.Context, query string) (results []string, total int, hasMore bool, err error)
|
||||
```
|
||||
|
||||
Generates:
|
||||
|
||||
```go
|
||||
type ServiceSearchResponse struct {
|
||||
Results []string `json:"results,omitempty"`
|
||||
Total int `json:"total,omitempty"`
|
||||
HasMore bool `json:"hasMore,omitempty"`
|
||||
Error string `json:"error,omitempty"`
|
||||
}
|
||||
```
|
||||
|
||||
## Output Files
|
||||
|
||||
### Host Code (Navidrome-side)
|
||||
|
||||
Generated files are named `<servicename>_gen.go` (lowercase) and placed in the output directory. Each file includes:
|
||||
|
||||
- `// Code generated by hostgen. DO NOT EDIT.` header
|
||||
- Required imports (`context`, `encoding/json`, `extism`)
|
||||
- Request/response struct types
|
||||
- Registration function
|
||||
- Host function wrappers
|
||||
- Helper functions (`writeResponse`, `writeErrorResponse`)
|
||||
|
||||
### Go Client Library (Go/TinyGo WASM)
|
||||
|
||||
Generated files are named `nd_host_<servicename>.go` (lowercase) and placed in the `go/` subdirectory of the output directory. The `go/` directory is a complete Go module (`github.com/navidrome/navidrome/plugins/host/go`) with package name `ndhost`, intended for import by Navidrome plugins built with TinyGo.
|
||||
|
||||
The generator also creates:
|
||||
- `doc.go` - Package documentation listing all available services
|
||||
- `go.mod` - Go module file with required dependencies
|
||||
|
||||
Each service file includes:
|
||||
|
||||
- `// Code generated by hostgen. DO NOT EDIT.` header
|
||||
- Required imports (`encoding/json`, `errors`, `github.com/extism/go-pdk`)
|
||||
- `//go:wasmimport` declarations for each host function
|
||||
- Response struct types and any struct definitions from the service
|
||||
- Wrapper functions that handle memory allocation and JSON parsing
|
||||
|
||||
#### Using the Go SDK
|
||||
|
||||
Import the SDK in your plugin:
|
||||
|
||||
```go
|
||||
import ndhost "github.com/navidrome/navidrome/plugins/host/go"
|
||||
|
||||
// Use host services with the ndhost prefix
|
||||
result, err := ndhost.CacheGetString("my-key")
|
||||
scheduleID, err := ndhost.SchedulerScheduleOneTime(60, "payload", "")
|
||||
```
|
||||
|
||||
Add to your `go.mod`:
|
||||
|
||||
```
|
||||
require github.com/navidrome/navidrome/plugins/host/go v0.0.0
|
||||
replace github.com/navidrome/navidrome/plugins/host/go => ../../host/go
|
||||
```
|
||||
|
||||
See [plugins/host/go/README.md](../../host/go/README.md) for detailed documentation.
|
||||
|
||||
### Example Output Structure
|
||||
|
||||
```
|
||||
output/
|
||||
├── subsonicapi_gen.go # Host-side code (for Navidrome)
|
||||
├── go/
|
||||
│ ├── doc.go # Package documentation
|
||||
│ ├── go.mod # Go module file
|
||||
│ └── nd_host_subsonicapi.go # Plugin-side code (for TinyGo plugins)
|
||||
├── python/
|
||||
│ └── nd_host_subsonicapi.py # Plugin-side code (for Python plugins)
|
||||
└── rust/
|
||||
└── nd_host_subsonicapi.rs # Plugin-side code (for Rust plugins)
|
||||
```
|
||||
|
||||
### Python Client Code (extism-py WASM)
|
||||
|
||||
Generated files are named `nd_host_<servicename>.py` (lowercase) and placed in the `python/` subdirectory of the output directory. These files are intended for use in Navidrome plugins built with extism-py. Each file includes:
|
||||
|
||||
- `# Code generated by hostgen. DO NOT EDIT.` header
|
||||
- Required imports (`dataclasses`, `typing`, `extism`, `json`)
|
||||
- `HostFunctionError` exception class for error handling
|
||||
- `@extism.import_fn` declarations for raw host functions
|
||||
- `@dataclass` types for methods with multiple return values
|
||||
- Wrapper functions with type hints, docstrings, and snake_case names
|
||||
|
||||
#### Python Type Mapping
|
||||
|
||||
| Go Type | Python Type |
|
||||
|-------------------------|-------------|
|
||||
| `string` | `str` |
|
||||
| `int`, `int32`, `int64` | `int` |
|
||||
| `float32`, `float64` | `float` |
|
||||
| `bool` | `bool` |
|
||||
| `[]byte` | `bytes` |
|
||||
| Unknown | `Any` |
|
||||
|
||||
#### Python Function Naming
|
||||
|
||||
Functions follow PEP 8 snake_case convention:
|
||||
|
||||
| Go Method | Python Function |
|
||||
|-------------------------------|----------------------------------|
|
||||
| `SubsonicAPI.Call` | `subsonicapi_call()` |
|
||||
| `Scheduler.ScheduleRecurring` | `scheduler_schedule_recurring()` |
|
||||
| `Cache.GetString` | `cache_get_string()` |
|
||||
|
||||
#### Multi-Value Returns
|
||||
|
||||
Methods with multiple return values use dataclasses:
|
||||
|
||||
```python
|
||||
@dataclass
|
||||
class CacheGetStringResult:
|
||||
value: str
|
||||
exists: bool
|
||||
|
||||
def cache_get_string(key: str) -> CacheGetStringResult:
|
||||
...
|
||||
```
|
||||
|
||||
#### Python Plugin Usage
|
||||
|
||||
> **Important:** Due to a limitation in extism-py, you cannot directly import the generated Python wrappers.
|
||||
> The `@extism.import_fn` decorators are only detected when defined in the plugin's main `__init__.py` file.
|
||||
> Generated Python files serve as **reference/template code** - copy the needed functions into your plugin.
|
||||
|
||||
Example of copying the generated wrapper into your plugin's `__init__.py`:
|
||||
|
||||
```python
|
||||
import extism
|
||||
import json
|
||||
|
||||
# Copy host function declarations from generated files into your __init__.py
|
||||
@extism.import_fn("extism:host/user", "subsonicapi_call")
|
||||
def _host_subsonicapi_call(input_ptr: extism.JsonI64) -> extism.JsonI64:
|
||||
pass
|
||||
|
||||
def subsonicapi_call(endpoint: str) -> str:
|
||||
"""Call the SubsonicAPI with the given endpoint."""
|
||||
result = _host_subsonicapi_call(endpoint)
|
||||
return result
|
||||
|
||||
# Now use it in your plugin
|
||||
@extism.plugin_fn
|
||||
def my_plugin_function():
|
||||
try:
|
||||
response = subsonicapi_call("getAlbumList2?type=random&size=10")
|
||||
data = json.loads(response)
|
||||
except Exception as e:
|
||||
extism.log(extism.LogLevel.Error, f"API error: {e}")
|
||||
```
|
||||
|
||||
### Rust Client Code (extism-pdk WASM)
|
||||
|
||||
Generated files are named `nd_host_<servicename>.rs` (lowercase) and placed in the `rust/` subdirectory of the output directory. These files are intended for use in Navidrome plugins built with `extism-pdk`. Each file includes:
|
||||
|
||||
- `// Code generated by hostgen. DO NOT EDIT.` header
|
||||
- Required imports (`extism_pdk::*`, `serde`)
|
||||
- Request/response struct types with `#[derive(Serialize, Deserialize)]`
|
||||
- `#[host_fn]` extern blocks for raw host function imports
|
||||
- Public wrapper functions with `Result<T, Error>` returns and snake_case names
|
||||
|
||||
#### Rust Type Mapping
|
||||
|
||||
| Go Type | Rust Type | Notes |
|
||||
|-------------------------------|-------------------------------|------------------------------|
|
||||
| `string` | `String` / `&str` | `&str` for params |
|
||||
| `int`, `int32` | `i32` | |
|
||||
| `int64` | `i64` | |
|
||||
| `float32` | `f32` | |
|
||||
| `float64` | `f64` | |
|
||||
| `bool` | `bool` | |
|
||||
| `[]byte` | `Vec<u8>` | |
|
||||
| `[]T` | `Vec<T>` | |
|
||||
| `map[K]V` | `HashMap<K, V>` | From `std::collections` |
|
||||
| `*T` | `Option<T>` | |
|
||||
| `interface{}` / `any` | `serde_json::Value` | |
|
||||
|
||||
#### Rust Function Naming
|
||||
|
||||
Functions follow Rust snake_case convention:
|
||||
|
||||
| Go Method | Rust Function |
|
||||
|-------------------------------|----------------------------------|
|
||||
| `SubsonicAPI.Call` | `subsonicapi_call()` |
|
||||
| `Scheduler.ScheduleRecurring` | `scheduler_schedule_recurring()` |
|
||||
| `Cache.GetString` | `cache_get_string()` |
|
||||
|
||||
#### Rust Plugin Usage
|
||||
|
||||
The generated Rust wrappers form a library crate (`nd-host`) that plugins can depend on. Add the dependency to your plugin's `Cargo.toml`:
|
||||
|
||||
```toml
|
||||
[dependencies]
|
||||
extism-pdk = "1.2"
|
||||
nd-host = { path = "../../host/rust" }
|
||||
```
|
||||
|
||||
Then import and use the services:
|
||||
|
||||
```rust
|
||||
use nd_host::{cache, scheduler, artwork};
|
||||
|
||||
#[plugin_fn]
|
||||
pub fn my_callback(input: String) -> FnResult<String> {
|
||||
// Use cache service
|
||||
cache::cache_set_string("key", "value", 3600)?;
|
||||
let value = cache::cache_get_string("key")?;
|
||||
|
||||
// Schedule a task
|
||||
scheduler::scheduler_schedule_one_time(60, "payload", "task-id")?;
|
||||
|
||||
// Get artwork URL
|
||||
let url = artwork::artwork_get_track_url("track-id", 300)?;
|
||||
|
||||
Ok("done")
|
||||
}
|
||||
```
|
||||
|
||||
See [discord-rich-presence-rs](../examples/discord-rich-presence-rs/) for a complete example using all Rust host wrappers.
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Annotations Not Detected
|
||||
|
||||
Ensure annotations are on consecutive lines with no blank `//` lines:
|
||||
|
||||
```go
|
||||
// ✅ Correct
|
||||
// Documentation for the service.
|
||||
//nd:hostservice name=Test permission=test
|
||||
|
||||
// ❌ Wrong - blank comment line breaks detection
|
||||
// Documentation for the service.
|
||||
//
|
||||
//nd:hostservice name=Test permission=test
|
||||
```
|
||||
|
||||
### Methods Not Exported
|
||||
|
||||
Methods without `//nd:hostfunc` annotation are skipped. Ensure the annotation is directly above the method:
|
||||
|
||||
```go
|
||||
// ✅ Correct
|
||||
// Method documentation.
|
||||
//nd:hostfunc
|
||||
MyMethod(ctx context.Context) error
|
||||
|
||||
// ❌ Wrong - annotation not directly above method
|
||||
//nd:hostfunc
|
||||
|
||||
MyMethod(ctx context.Context) error
|
||||
```
|
||||
|
||||
### Generated Files Skipped
|
||||
|
||||
Files ending in `_gen.go` are automatically skipped during parsing to avoid processing previously generated code.
|
||||
|
||||
## Development
|
||||
|
||||
Run tests:
|
||||
|
||||
```bash
|
||||
go test -v ./plugins/cmd/hostgen/...
|
||||
```
|
||||
|
||||
The test suite includes:
|
||||
- CLI integration tests
|
||||
- Complex type handling (structs, slices, maps, pointers)
|
||||
- Multiple return value scenarios
|
||||
- Error cases and edge conditions
|
||||
@ -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
|
||||
}
|
||||
}
|
||||
@ -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
|
||||
}
|
||||
@ -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)
|
||||
}
|
||||
@ -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")
|
||||
}
|
||||
@ -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"))
|
||||
}
|
||||
@ -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"))
|
||||
})
|
||||
})
|
||||
})
|
||||
@ -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}}
|
||||
@ -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}}
|
||||
@ -1,103 +0,0 @@
|
||||
// Code generated by hostgen. DO NOT EDIT.
|
||||
//
|
||||
// This file contains client wrappers for the {{.Service.Name}} host service.
|
||||
// It is intended for use in Navidrome plugins built with extism-pdk.
|
||||
|
||||
use extism_pdk::*;
|
||||
use serde::{Deserialize, Serialize};
|
||||
{{- /* Generate struct definitions */ -}}
|
||||
{{- range .Service.Structs}}
|
||||
{{if .Doc}}
|
||||
{{rustDocComment .Doc}}
|
||||
{{else}}
|
||||
{{end}}#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct {{.Name}} {
|
||||
{{- range .Fields}}
|
||||
{{- if .NeedsDefault}}
|
||||
#[serde(default)]
|
||||
{{- end}}
|
||||
pub {{.RustName}}: {{fieldRustType .}},
|
||||
{{- end}}
|
||||
}
|
||||
{{- end}}
|
||||
{{- /* Generate request/response types */ -}}
|
||||
{{- range .Service.Methods}}
|
||||
{{- if .HasParams}}
|
||||
|
||||
#[derive(Debug, Clone, Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct {{requestType .}} {
|
||||
{{- range .Params}}
|
||||
{{.RustName}}: {{rustType .}},
|
||||
{{- end}}
|
||||
}
|
||||
{{- end}}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct {{responseType .}} {
|
||||
{{- range .Returns}}
|
||||
#[serde(default)]
|
||||
{{.RustName}}: {{rustType .}},
|
||||
{{- end}}
|
||||
#[serde(default)]
|
||||
error: Option<String>,
|
||||
}
|
||||
{{- end}}
|
||||
|
||||
#[host_fn]
|
||||
extern "ExtismHost" {
|
||||
{{- range .Service.Methods}}
|
||||
fn {{exportName .}}(input: Json<{{if .HasParams}}{{requestType .}}{{else}}serde_json::Value{{end}}>) -> Json<{{responseType .}}>;
|
||||
{{- end}}
|
||||
}
|
||||
|
||||
{{- /* Generate wrapper functions */ -}}
|
||||
{{range .Service.Methods}}
|
||||
|
||||
{{if .Doc}}{{rustDocComment .Doc}}{{else}}/// Calls the {{exportName .}} host function.{{end}}
|
||||
{{- if .HasParams}}
|
||||
///
|
||||
/// # Arguments
|
||||
{{- range .Params}}
|
||||
/// * `{{.RustName}}` - {{rustType .}} parameter.
|
||||
{{- end}}
|
||||
{{- end}}
|
||||
{{- if .HasReturns}}
|
||||
///
|
||||
/// # Returns
|
||||
{{- if eq (len .Returns) 1}}
|
||||
/// The {{(index .Returns 0).RustName}} value.
|
||||
{{- else}}
|
||||
/// A tuple of ({{range $i, $r := .Returns}}{{if $i}}, {{end}}{{$r.RustName}}{{end}}).
|
||||
{{- end}}
|
||||
{{- end}}
|
||||
///
|
||||
/// # Errors
|
||||
/// Returns an error if the host function call fails.
|
||||
pub fn {{rustFunc .}}({{range $i, $p := .Params}}{{if $i}}, {{end}}{{$p.RustName}}: {{rustParamType $p}}{{end}}) -> Result<{{if eq (len .Returns) 0}}(){{else if eq (len .Returns) 1}}{{rustType (index .Returns 0)}}{{else}}({{range $i, $r := .Returns}}{{if $i}}, {{end}}{{rustType $r}}{{end}}){{end}}, Error> {
|
||||
let response = unsafe {
|
||||
{{- if .HasParams}}
|
||||
{{exportName .}}(Json({{requestType .}} {
|
||||
{{- range .Params}}
|
||||
{{.RustName}}: {{.RustName}}{{if .NeedsToOwned}}.to_owned(){{end}},
|
||||
{{- end}}
|
||||
}))?
|
||||
{{- else}}
|
||||
{{exportName .}}(Json(serde_json::json!({})))?
|
||||
{{- end}}
|
||||
};
|
||||
|
||||
if let Some(err) = response.0.error {
|
||||
return Err(Error::msg(err));
|
||||
}
|
||||
{{if eq (len .Returns) 0}}
|
||||
Ok(())
|
||||
{{- else if eq (len .Returns) 1}}
|
||||
Ok(response.0.{{(index .Returns 0).RustName}})
|
||||
{{- else}}
|
||||
Ok(({{range $i, $r := .Returns}}{{if $i}}, {{end}}response.0.{{$r.RustName}}{{end}}))
|
||||
{{- end}}
|
||||
}
|
||||
{{- end}}
|
||||
@ -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}}
|
||||
@ -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
|
||||
@ -1,5 +0,0 @@
|
||||
module github.com/navidrome/navidrome/plugins/host/go
|
||||
|
||||
go 1.24
|
||||
|
||||
require github.com/extism/go-pdk v1.1.3
|
||||
@ -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
|
||||
}
|
||||
@ -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;
|
||||
@ -1,384 +0,0 @@
|
||||
package internal
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"unicode"
|
||||
)
|
||||
|
||||
// Service represents a parsed host service interface.
|
||||
type Service struct {
|
||||
Name string // Service name from annotation (e.g., "SubsonicAPI")
|
||||
Permission string // Manifest permission key (e.g., "subsonicapi")
|
||||
Interface string // Go interface name (e.g., "SubsonicAPIService")
|
||||
Methods []Method // Methods marked with //nd:hostfunc
|
||||
Doc string // Documentation comment for the service
|
||||
Structs []StructDef // Structs used by this service
|
||||
}
|
||||
|
||||
// StructDef represents a Go struct type definition.
|
||||
type StructDef struct {
|
||||
Name string // Go struct name (e.g., "Library")
|
||||
Fields []FieldDef // Struct fields
|
||||
Doc string // Documentation comment
|
||||
}
|
||||
|
||||
// FieldDef represents a field within a struct.
|
||||
type FieldDef struct {
|
||||
Name string // Go field name (e.g., "TotalSongs")
|
||||
Type string // Go type (e.g., "int32", "*string", "[]User")
|
||||
JSONTag string // JSON tag value (e.g., "totalSongs,omitempty")
|
||||
OmitEmpty bool // Whether the field has omitempty tag
|
||||
Doc string // Field documentation
|
||||
}
|
||||
|
||||
// OutputFileName returns the generated file name for this service.
|
||||
func (s Service) OutputFileName() string {
|
||||
return strings.ToLower(s.Name) + "_gen.go"
|
||||
}
|
||||
|
||||
// ExportPrefix returns the prefix for exported host function names.
|
||||
func (s Service) ExportPrefix() string {
|
||||
return strings.ToLower(s.Name)
|
||||
}
|
||||
|
||||
// KnownStructs returns a map of struct names defined in this service.
|
||||
func (s Service) KnownStructs() map[string]bool {
|
||||
result := make(map[string]bool)
|
||||
for _, st := range s.Structs {
|
||||
result[st.Name] = true
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// Method represents a host function method within a service.
|
||||
type Method struct {
|
||||
Name string // Go method name (e.g., "Call")
|
||||
ExportName string // Optional override for export name
|
||||
Params []Param // Method parameters (excluding context.Context)
|
||||
Returns []Param // Return values (excluding error)
|
||||
HasError bool // Whether the method returns an error
|
||||
Doc string // Documentation comment for the method
|
||||
}
|
||||
|
||||
// FunctionName returns the Extism host function export name.
|
||||
func (m Method) FunctionName(servicePrefix string) string {
|
||||
if m.ExportName != "" {
|
||||
return m.ExportName
|
||||
}
|
||||
return servicePrefix + "_" + strings.ToLower(m.Name)
|
||||
}
|
||||
|
||||
// RequestTypeName returns the generated request type name.
|
||||
func (m Method) RequestTypeName(serviceName string) string {
|
||||
return serviceName + m.Name + "Request"
|
||||
}
|
||||
|
||||
// ResponseTypeName returns the generated response type name.
|
||||
func (m Method) ResponseTypeName(serviceName string) string {
|
||||
return serviceName + m.Name + "Response"
|
||||
}
|
||||
|
||||
// HasParams returns true if the method has input parameters.
|
||||
func (m Method) HasParams() bool {
|
||||
return len(m.Params) > 0
|
||||
}
|
||||
|
||||
// HasReturns returns true if the method has return values (excluding error).
|
||||
func (m Method) HasReturns() bool {
|
||||
return len(m.Returns) > 0
|
||||
}
|
||||
|
||||
// Param represents a method parameter or return value.
|
||||
type Param struct {
|
||||
Name string // Parameter name
|
||||
Type string // Go type (e.g., "string", "int32", "[]byte")
|
||||
JSONName string // JSON field name (camelCase)
|
||||
}
|
||||
|
||||
// NewParam creates a Param with auto-generated JSON name.
|
||||
func NewParam(name, typ string) Param {
|
||||
return Param{
|
||||
Name: name,
|
||||
Type: typ,
|
||||
JSONName: toJSONName(name),
|
||||
}
|
||||
}
|
||||
|
||||
// toJSONName converts a Go identifier to camelCase JSON field name.
|
||||
// This matches Rust serde's rename_all = "camelCase" behavior.
|
||||
// Examples: "ConnectionID" -> "connectionId", "NewConnectionID" -> "newConnectionId"
|
||||
func toJSONName(name string) string {
|
||||
if name == "" {
|
||||
return ""
|
||||
}
|
||||
|
||||
runes := []rune(name)
|
||||
result := make([]rune, 0, len(runes))
|
||||
|
||||
for i, r := range runes {
|
||||
if i == 0 {
|
||||
// First character is always lowercase
|
||||
result = append(result, unicode.ToLower(r))
|
||||
} else if unicode.IsUpper(r) {
|
||||
// Check if this is part of an acronym (consecutive uppercase)
|
||||
// or a word boundary
|
||||
prevIsUpper := unicode.IsUpper(runes[i-1])
|
||||
nextIsLower := i+1 < len(runes) && unicode.IsLower(runes[i+1])
|
||||
|
||||
if prevIsUpper && !nextIsLower {
|
||||
// Middle of an acronym - lowercase it
|
||||
result = append(result, unicode.ToLower(r))
|
||||
} else if prevIsUpper && nextIsLower {
|
||||
// End of acronym followed by lowercase - this starts a new word
|
||||
// Keep uppercase
|
||||
result = append(result, r)
|
||||
} else {
|
||||
// Regular word boundary - keep uppercase
|
||||
result = append(result, r)
|
||||
}
|
||||
} else {
|
||||
result = append(result, r)
|
||||
}
|
||||
}
|
||||
|
||||
return string(result)
|
||||
}
|
||||
|
||||
// ToPythonType converts a Go type to its Python equivalent.
|
||||
func ToPythonType(goType string) string {
|
||||
switch goType {
|
||||
case "string":
|
||||
return "str"
|
||||
case "int", "int32", "int64":
|
||||
return "int"
|
||||
case "float32", "float64":
|
||||
return "float"
|
||||
case "bool":
|
||||
return "bool"
|
||||
case "[]byte":
|
||||
return "bytes"
|
||||
default:
|
||||
return "Any"
|
||||
}
|
||||
}
|
||||
|
||||
// ToSnakeCase converts a PascalCase or camelCase string to snake_case.
|
||||
// It handles consecutive uppercase letters correctly (e.g., "ScheduleID" -> "schedule_id").
|
||||
func ToSnakeCase(s string) string {
|
||||
var result strings.Builder
|
||||
runes := []rune(s)
|
||||
for i, r := range runes {
|
||||
if i > 0 && r >= 'A' && r <= 'Z' {
|
||||
// Add underscore before uppercase, but not if:
|
||||
// - Previous char was uppercase AND next char is uppercase or end of string
|
||||
// (this handles acronyms like "ID" in "NewScheduleID")
|
||||
prevUpper := runes[i-1] >= 'A' && runes[i-1] <= 'Z'
|
||||
nextUpper := i+1 < len(runes) && runes[i+1] >= 'A' && runes[i+1] <= 'Z'
|
||||
atEnd := i+1 == len(runes)
|
||||
|
||||
// Only skip underscore if we're in the middle of an acronym
|
||||
if !prevUpper || (!nextUpper && !atEnd) {
|
||||
result.WriteByte('_')
|
||||
}
|
||||
}
|
||||
result.WriteRune(r)
|
||||
}
|
||||
return strings.ToLower(result.String())
|
||||
}
|
||||
|
||||
// PythonFunctionName returns the Python function name for a method.
|
||||
func (m Method) PythonFunctionName(servicePrefix string) string {
|
||||
return ToSnakeCase(servicePrefix + m.Name)
|
||||
}
|
||||
|
||||
// PythonResultTypeName returns the Python dataclass name for multi-value returns.
|
||||
func (m Method) PythonResultTypeName(serviceName string) string {
|
||||
return serviceName + m.Name + "Result"
|
||||
}
|
||||
|
||||
// NeedsResultClass returns true if the method needs a dataclass for returns.
|
||||
func (m Method) NeedsResultClass() bool {
|
||||
return len(m.Returns) > 1
|
||||
}
|
||||
|
||||
// PythonType returns the Python type for this parameter.
|
||||
func (p Param) PythonType() string {
|
||||
return ToPythonType(p.Type)
|
||||
}
|
||||
|
||||
// PythonName returns the snake_case Python name for this parameter.
|
||||
func (p Param) PythonName() string {
|
||||
return ToSnakeCase(p.Name)
|
||||
}
|
||||
|
||||
// ToRustType converts a Go type to its Rust equivalent.
|
||||
func ToRustType(goType string) string {
|
||||
return ToRustTypeWithStructs(goType, nil)
|
||||
}
|
||||
|
||||
// RustParamType returns the Rust type for a function parameter (uses &str for strings).
|
||||
func RustParamType(goType string) string {
|
||||
if goType == "string" {
|
||||
return "&str"
|
||||
}
|
||||
return ToRustType(goType)
|
||||
}
|
||||
|
||||
// RustDefaultValue returns the default value for a Rust type.
|
||||
func RustDefaultValue(goType string) string {
|
||||
switch goType {
|
||||
case "string":
|
||||
return `String::new()`
|
||||
case "int", "int32":
|
||||
return "0"
|
||||
case "int64":
|
||||
return "0"
|
||||
case "float32", "float64":
|
||||
return "0.0"
|
||||
case "bool":
|
||||
return "false"
|
||||
default:
|
||||
if strings.HasPrefix(goType, "[]") {
|
||||
return "Vec::new()"
|
||||
}
|
||||
if strings.HasPrefix(goType, "map[") {
|
||||
return "std::collections::HashMap::new()"
|
||||
}
|
||||
if strings.HasPrefix(goType, "*") {
|
||||
return "None"
|
||||
}
|
||||
return "serde_json::Value::Null"
|
||||
}
|
||||
}
|
||||
|
||||
// RustFunctionName returns the Rust function name for a method (snake_case).
|
||||
// Uses just the method name without service prefix since the module provides namespacing.
|
||||
func (m Method) RustFunctionName(_ string) string {
|
||||
return ToSnakeCase(m.Name)
|
||||
}
|
||||
|
||||
// RustDocComment returns a properly formatted Rust doc comment.
|
||||
// Each line of the input doc string is prefixed with "/// ".
|
||||
func RustDocComment(doc string) string {
|
||||
if doc == "" {
|
||||
return ""
|
||||
}
|
||||
lines := strings.Split(doc, "\n")
|
||||
var result []string
|
||||
for _, line := range lines {
|
||||
result = append(result, "/// "+line)
|
||||
}
|
||||
return strings.Join(result, "\n")
|
||||
}
|
||||
|
||||
// RustType returns the Rust type for this parameter.
|
||||
func (p Param) RustType() string {
|
||||
return ToRustType(p.Type)
|
||||
}
|
||||
|
||||
// RustTypeWithStructs returns the Rust type using known struct names.
|
||||
func (p Param) RustTypeWithStructs(knownStructs map[string]bool) string {
|
||||
return ToRustTypeWithStructs(p.Type, knownStructs)
|
||||
}
|
||||
|
||||
// RustParamType returns the Rust type for this parameter when used as a function argument.
|
||||
func (p Param) RustParamType() string {
|
||||
return RustParamType(p.Type)
|
||||
}
|
||||
|
||||
// RustParamTypeWithStructs returns the Rust param type using known struct names.
|
||||
func (p Param) RustParamTypeWithStructs(knownStructs map[string]bool) string {
|
||||
if p.Type == "string" {
|
||||
return "&str"
|
||||
}
|
||||
return ToRustTypeWithStructs(p.Type, knownStructs)
|
||||
}
|
||||
|
||||
// RustName returns the snake_case Rust name for this parameter.
|
||||
func (p Param) RustName() string {
|
||||
return ToSnakeCase(p.Name)
|
||||
}
|
||||
|
||||
// NeedsToOwned returns true if the parameter needs .to_owned() when used.
|
||||
func (p Param) NeedsToOwned() bool {
|
||||
return p.Type == "string"
|
||||
}
|
||||
|
||||
// RustType returns the Rust type for this field, using known struct names.
|
||||
func (f FieldDef) RustType(knownStructs map[string]bool) string {
|
||||
return ToRustTypeWithStructs(f.Type, knownStructs)
|
||||
}
|
||||
|
||||
// RustName returns the snake_case Rust name for this field.
|
||||
func (f FieldDef) RustName() string {
|
||||
return ToSnakeCase(f.Name)
|
||||
}
|
||||
|
||||
// NeedsDefault returns true if the field needs #[serde(default)] attribute.
|
||||
// This is true for fields with omitempty tag.
|
||||
func (f FieldDef) NeedsDefault() bool {
|
||||
return f.OmitEmpty
|
||||
}
|
||||
|
||||
// ToRustTypeWithStructs converts a Go type to its Rust equivalent,
|
||||
// using known struct names instead of serde_json::Value.
|
||||
func ToRustTypeWithStructs(goType string, knownStructs map[string]bool) string {
|
||||
// Handle pointer types
|
||||
if strings.HasPrefix(goType, "*") {
|
||||
inner := ToRustTypeWithStructs(goType[1:], knownStructs)
|
||||
return "Option<" + inner + ">"
|
||||
}
|
||||
// Handle slice types
|
||||
if strings.HasPrefix(goType, "[]") {
|
||||
if goType == "[]byte" {
|
||||
return "Vec<u8>"
|
||||
}
|
||||
inner := ToRustTypeWithStructs(goType[2:], knownStructs)
|
||||
return "Vec<" + inner + ">"
|
||||
}
|
||||
// Handle map types
|
||||
if strings.HasPrefix(goType, "map[") {
|
||||
// Extract key and value types from map[K]V
|
||||
rest := goType[4:] // Remove "map["
|
||||
depth := 1
|
||||
keyEnd := 0
|
||||
for i, r := range rest {
|
||||
if r == '[' {
|
||||
depth++
|
||||
} else if r == ']' {
|
||||
depth--
|
||||
if depth == 0 {
|
||||
keyEnd = i
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
keyType := rest[:keyEnd]
|
||||
valueType := rest[keyEnd+1:]
|
||||
return "std::collections::HashMap<" + ToRustTypeWithStructs(keyType, knownStructs) + ", " + ToRustTypeWithStructs(valueType, knownStructs) + ">"
|
||||
}
|
||||
|
||||
switch goType {
|
||||
case "string":
|
||||
return "String"
|
||||
case "int", "int32":
|
||||
return "i32"
|
||||
case "int64":
|
||||
return "i64"
|
||||
case "float32":
|
||||
return "f32"
|
||||
case "float64":
|
||||
return "f64"
|
||||
case "bool":
|
||||
return "bool"
|
||||
case "interface{}", "any":
|
||||
return "serde_json::Value"
|
||||
default:
|
||||
// Check if this is a known struct type
|
||||
if knownStructs != nil && knownStructs[goType] {
|
||||
return goType
|
||||
}
|
||||
// For unknown custom types, fall back to Value
|
||||
return "serde_json::Value"
|
||||
}
|
||||
}
|
||||
@ -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
|
||||
}
|
||||
@ -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:"))
|
||||
|
||||
@ -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"))
|
||||
|
||||
@ -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.
|
||||
|
||||
@ -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.
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
// Code generated by hostgen. DO NOT EDIT.
|
||||
// Code generated by ndpgen. DO NOT EDIT.
|
||||
|
||||
package {{.Package}}
|
||||
|
||||
|
||||
@ -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
|
||||
//!
|
||||
|
||||
@ -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.
|
||||
@ -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.
|
||||
@ -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.
|
||||
@ -1,4 +1,4 @@
|
||||
// Code generated by hostgen. DO NOT EDIT.
|
||||
// Code generated by ndpgen. DO NOT EDIT.
|
||||
|
||||
package testpkg
|
||||
|
||||
@ -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.
|
||||
@ -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.
|
||||
@ -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.
|
||||
@ -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.
|
||||
@ -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.
|
||||
@ -1,4 +1,4 @@
|
||||
// Code generated by hostgen. DO NOT EDIT.
|
||||
// Code generated by ndpgen. DO NOT EDIT.
|
||||
|
||||
package testpkg
|
||||
|
||||
@ -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.
|
||||
@ -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.
|
||||
@ -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.
|
||||
@ -1,4 +1,4 @@
|
||||
// Code generated by hostgen. DO NOT EDIT.
|
||||
// Code generated by ndpgen. DO NOT EDIT.
|
||||
|
||||
package testpkg
|
||||
|
||||
@ -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.
|
||||
@ -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.
|
||||
@ -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.
|
||||
@ -1,4 +1,4 @@
|
||||
// Code generated by hostgen. DO NOT EDIT.
|
||||
// Code generated by ndpgen. DO NOT EDIT.
|
||||
|
||||
package testpkg
|
||||
|
||||
@ -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.
|
||||
@ -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.
|
||||
@ -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.
|
||||
@ -1,4 +1,4 @@
|
||||
// Code generated by hostgen. DO NOT EDIT.
|
||||
// Code generated by ndpgen. DO NOT EDIT.
|
||||
|
||||
package testpkg
|
||||
|
||||
@ -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.
|
||||
@ -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.
|
||||
@ -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.
|
||||
@ -1,4 +1,4 @@
|
||||
// Code generated by hostgen. DO NOT EDIT.
|
||||
// Code generated by ndpgen. DO NOT EDIT.
|
||||
|
||||
package testpkg
|
||||
|
||||
@ -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.
|
||||
@ -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.
|
||||
@ -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.
|
||||
@ -1,4 +1,4 @@
|
||||
// Code generated by hostgen. DO NOT EDIT.
|
||||
// Code generated by ndpgen. DO NOT EDIT.
|
||||
|
||||
package testpkg
|
||||
|
||||
@ -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.
|
||||
@ -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.
|
||||
@ -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.
|
||||
@ -1,4 +1,4 @@
|
||||
// Code generated by hostgen. DO NOT EDIT.
|
||||
// Code generated by ndpgen. DO NOT EDIT.
|
||||
|
||||
package testpkg
|
||||
|
||||
@ -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.
|
||||
@ -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.
|
||||
@ -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.
|
||||
@ -1,4 +1,4 @@
|
||||
// Code generated by hostgen. DO NOT EDIT.
|
||||
// Code generated by ndpgen. DO NOT EDIT.
|
||||
|
||||
package testpkg
|
||||
|
||||
@ -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.
|
||||
@ -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.
|
||||
@ -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.
|
||||
@ -1,4 +1,4 @@
|
||||
// Code generated by hostgen. DO NOT EDIT.
|
||||
// Code generated by ndpgen. DO NOT EDIT.
|
||||
|
||||
package testpkg
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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))
|
||||
}
|
||||
|
||||
@ -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" }
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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))
|
||||
}
|
||||
|
||||
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@ -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"
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
// Code generated by hostgen. DO NOT EDIT.
|
||||
// Code generated by ndpgen. DO NOT EDIT.
|
||||
|
||||
package host
|
||||
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
// Code generated by hostgen. DO NOT EDIT.
|
||||
// Code generated by ndpgen. DO NOT EDIT.
|
||||
|
||||
package host
|
||||
|
||||
|
||||
@ -27,7 +27,7 @@
|
||||
//
|
||||
// # Generated Code
|
||||
//
|
||||
// The hostgen tool reads annotated interfaces and generates Extism host function wrappers
|
||||
// The ndpgen tool reads annotated interfaces and generates Extism host function wrappers
|
||||
// that handle:
|
||||
// - JSON serialization/deserialization of request/response types
|
||||
// - Memory operations (ReadBytes, WriteBytes, Alloc)
|
||||
@ -37,5 +37,5 @@
|
||||
// Generated files follow the pattern <servicename>_gen.go and include a header comment
|
||||
// indicating they should not be edited manually.
|
||||
//
|
||||
//go:generate go run ../cmd/hostgen -input=. -output=. -python -go -rust
|
||||
//go:generate go run ../cmd/ndpgen -input=. -output=../pdk -go -python -rust
|
||||
package host
|
||||
|
||||
@ -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
|
||||
@ -1,5 +0,0 @@
|
||||
module github.com/navidrome/navidrome/plugins/host/go
|
||||
|
||||
go 1.24
|
||||
|
||||
require github.com/extism/go-pdk v1.1.3
|
||||
@ -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=
|
||||
@ -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
|
||||
}
|
||||
@ -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")
|
||||
}
|
||||
@ -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
|
||||
}
|
||||
@ -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")
|
||||
}
|
||||
@ -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
|
||||
}
|
||||
@ -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")
|
||||
}
|
||||
@ -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
|
||||
}
|
||||
@ -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")
|
||||
}
|
||||
@ -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
|
||||
}
|
||||
@ -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")
|
||||
}
|
||||
@ -1,69 +0,0 @@
|
||||
// Code generated by hostgen. DO NOT EDIT.
|
||||
//
|
||||
// This file contains client wrappers for the SubsonicAPI host service.
|
||||
// It is intended for use in Navidrome plugins built with TinyGo.
|
||||
//
|
||||
//go:build wasip1
|
||||
|
||||
package ndhost
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
|
||||
"github.com/extism/go-pdk"
|
||||
)
|
||||
|
||||
// subsonicapi_call is the host function provided by Navidrome.
|
||||
//
|
||||
//go:wasmimport extism:host/user subsonicapi_call
|
||||
func subsonicapi_call(uint64) uint64
|
||||
|
||||
// SubsonicAPICallRequest is the request type for SubsonicAPI.Call.
|
||||
type SubsonicAPICallRequest struct {
|
||||
Uri string `json:"uri"`
|
||||
}
|
||||
|
||||
// SubsonicAPICallResponse is the response type for SubsonicAPI.Call.
|
||||
type SubsonicAPICallResponse struct {
|
||||
ResponseJSON string `json:"responseJson,omitempty"`
|
||||
Error string `json:"error,omitempty"`
|
||||
}
|
||||
|
||||
// SubsonicAPICall calls the subsonicapi_call host function.
|
||||
// Call executes a Subsonic API request and returns the JSON response.
|
||||
//
|
||||
// The uri parameter should be the Subsonic API path without the server prefix,
|
||||
// e.g., "getAlbumList2?type=random&size=10". The response is returned as raw JSON.
|
||||
func SubsonicAPICall(uri string) (*SubsonicAPICallResponse, error) {
|
||||
// Marshal request to JSON
|
||||
req := SubsonicAPICallRequest{
|
||||
Uri: uri,
|
||||
}
|
||||
reqBytes, err := json.Marshal(req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
reqMem := pdk.AllocateBytes(reqBytes)
|
||||
defer reqMem.Free()
|
||||
|
||||
// Call the host function
|
||||
responsePtr := subsonicapi_call(reqMem.Offset())
|
||||
|
||||
// Read the response from memory
|
||||
responseMem := pdk.FindMemory(responsePtr)
|
||||
responseBytes := responseMem.ReadBytes()
|
||||
|
||||
// Parse the response
|
||||
var response SubsonicAPICallResponse
|
||||
if err := json.Unmarshal(responseBytes, &response); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Convert Error field to Go error
|
||||
if response.Error != "" {
|
||||
return nil, errors.New(response.Error)
|
||||
}
|
||||
|
||||
return &response, nil
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user