mirror of
https://github.com/navidrome/navidrome.git
synced 2026-05-03 06:51:16 +00:00
feat(plugins): generate client wrappers for host functions
Signed-off-by: Deluan <deluan@navidrome.org>
This commit is contained in:
parent
a0a5168f5f
commit
ba27a8ceef
1
go.mod
1
go.mod
@ -21,6 +21,7 @@ require (
|
|||||||
github.com/djherbis/stream v1.4.0
|
github.com/djherbis/stream v1.4.0
|
||||||
github.com/djherbis/times v1.6.0
|
github.com/djherbis/times v1.6.0
|
||||||
github.com/dustin/go-humanize v1.0.1
|
github.com/dustin/go-humanize v1.0.1
|
||||||
|
github.com/extism/go-pdk v1.1.3
|
||||||
github.com/extism/go-sdk v1.7.1
|
github.com/extism/go-sdk v1.7.1
|
||||||
github.com/fatih/structs v1.1.0
|
github.com/fatih/structs v1.1.0
|
||||||
github.com/go-chi/chi/v5 v5.2.3
|
github.com/go-chi/chi/v5 v5.2.3
|
||||||
|
|||||||
2
go.sum
2
go.sum
@ -57,6 +57,8 @@ github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkp
|
|||||||
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
|
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
|
||||||
github.com/dylibso/observe-sdk/go v0.0.0-20240819160327-2d926c5d788a h1:UwSIFv5g5lIvbGgtf3tVwC7Ky9rmMFBp0RMs+6f6YqE=
|
github.com/dylibso/observe-sdk/go v0.0.0-20240819160327-2d926c5d788a h1:UwSIFv5g5lIvbGgtf3tVwC7Ky9rmMFBp0RMs+6f6YqE=
|
||||||
github.com/dylibso/observe-sdk/go v0.0.0-20240819160327-2d926c5d788a/go.mod h1:C8DzXehI4zAbrdlbtOByKX6pfivJTBiV9Jjqv56Yd9Q=
|
github.com/dylibso/observe-sdk/go v0.0.0-20240819160327-2d926c5d788a/go.mod h1:C8DzXehI4zAbrdlbtOByKX6pfivJTBiV9Jjqv56Yd9Q=
|
||||||
|
github.com/extism/go-pdk v1.1.3 h1:hfViMPWrqjN6u67cIYRALZTZLk/enSPpNKa+rZ9X2SQ=
|
||||||
|
github.com/extism/go-pdk v1.1.3/go.mod h1:Gz+LIU/YCKnKXhgge8yo5Yu1F/lbv7KtKFkiCSzW/P4=
|
||||||
github.com/extism/go-sdk v1.7.1 h1:lWJos6uY+tRFdlIHR+SJjwFDApY7OypS/2nMhiVQ9Sw=
|
github.com/extism/go-sdk v1.7.1 h1:lWJos6uY+tRFdlIHR+SJjwFDApY7OypS/2nMhiVQ9Sw=
|
||||||
github.com/extism/go-sdk v1.7.1/go.mod h1:IT+Xdg5AZM9hVtpFUA+uZCJMge/hbvshl8bwzLtFyKA=
|
github.com/extism/go-sdk v1.7.1/go.mod h1:IT+Xdg5AZM9hVtpFUA+uZCJMge/hbvshl8bwzLtFyKA=
|
||||||
github.com/fatih/structs v1.1.0 h1:Q7juDM0QtcnhCpeyLGQKyg4TOIghuNXrkL32pHAUMxo=
|
github.com/fatih/structs v1.1.0 h1:Q7juDM0QtcnhCpeyLGQKyg4TOIghuNXrkL32pHAUMxo=
|
||||||
|
|||||||
@ -5,18 +5,22 @@ A code generator for Navidrome's plugin host functions. It reads Go interface de
|
|||||||
## Usage
|
## Usage
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
hostgen -input <dir> -output <dir> -package <name> [-v] [-dry-run]
|
hostgen -input <dir> -output <dir> -package <name> [-v] [-dry-run] [-host-only] [-plugin-only]
|
||||||
```
|
```
|
||||||
|
|
||||||
### Flags
|
### Flags
|
||||||
|
|
||||||
| Flag | Description | Default |
|
| Flag | Description | Default |
|
||||||
|------------|----------------------------------------------------------------|----------|
|
|----------------|----------------------------------------------------------------|----------|
|
||||||
| `-input` | Directory containing Go source files with annotated interfaces | Required |
|
| `-input` | Directory containing Go source files with annotated interfaces | Required |
|
||||||
| `-output` | Directory where generated files will be written | Required |
|
| `-output` | Directory where generated files will be written | Required |
|
||||||
| `-package` | Package name for generated files | Required |
|
| `-package` | Package name for generated files | Required |
|
||||||
| `-v` | Verbose output | `false` |
|
| `-v` | Verbose output | `false` |
|
||||||
| `-dry-run` | Parse and validate without writing files | `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` |
|
||||||
|
|
||||||
|
By default, both host and plugin code are generated. Use `-host-only` or `-plugin-only` to generate only one type.
|
||||||
|
|
||||||
### Example
|
### Example
|
||||||
|
|
||||||
@ -162,7 +166,9 @@ type ServiceSearchResponse struct {
|
|||||||
|
|
||||||
## Output Files
|
## Output Files
|
||||||
|
|
||||||
Generated files are named `<servicename>_gen.go` (lowercase). Each file includes:
|
### 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
|
- `// Code generated by hostgen. DO NOT EDIT.` header
|
||||||
- Required imports (`context`, `encoding/json`, `extism`)
|
- Required imports (`context`, `encoding/json`, `extism`)
|
||||||
@ -171,6 +177,25 @@ Generated files are named `<servicename>_gen.go` (lowercase). Each file includes
|
|||||||
- Host function wrappers
|
- Host function wrappers
|
||||||
- Helper functions (`writeResponse`, `writeErrorResponse`)
|
- Helper functions (`writeResponse`, `writeErrorResponse`)
|
||||||
|
|
||||||
|
### Plugin/Client Code (TinyGo WASM)
|
||||||
|
|
||||||
|
Generated files are named `nd_host_<servicename>.go` (lowercase) and placed in the `go/` subdirectory of the output directory. These files are intended for use in Navidrome plugins built with TinyGo. Each 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
|
||||||
|
- Wrapper functions that handle memory allocation and JSON parsing
|
||||||
|
|
||||||
|
### Example Output Structure
|
||||||
|
|
||||||
|
```
|
||||||
|
output/
|
||||||
|
├── subsonicapi_gen.go # Host-side code (for Navidrome)
|
||||||
|
└── go/
|
||||||
|
└── nd_host_subsonicapi.go # Plugin-side code (for TinyGo plugins)
|
||||||
|
```
|
||||||
|
|
||||||
## Troubleshooting
|
## Troubleshooting
|
||||||
|
|
||||||
### Annotations Not Detected
|
### Annotations Not Detected
|
||||||
|
|||||||
@ -150,69 +150,146 @@ type ServiceB interface {
|
|||||||
Expect(filepath.Join(outputDir, "servicea_gen.go")).To(BeAnExistingFile())
|
Expect(filepath.Join(outputDir, "servicea_gen.go")).To(BeAnExistingFile())
|
||||||
Expect(filepath.Join(outputDir, "serviceb_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() {
|
Describe("code generation", func() {
|
||||||
DescribeTable("generates correct output",
|
DescribeTable("generates correct host and client output",
|
||||||
func(serviceFile, expectedFile string) {
|
func(serviceFile, hostExpectedFile, clientExpectedFile string) {
|
||||||
serviceCode := readTestdata(serviceFile)
|
serviceCode := readTestdata(serviceFile)
|
||||||
expectedCode := readTestdata(expectedFile)
|
hostExpected := readTestdata(hostExpectedFile)
|
||||||
|
clientExpected := readTestdata(clientExpectedFile)
|
||||||
|
|
||||||
Expect(os.WriteFile(filepath.Join(testDir, "service.go"), []byte(serviceCode), 0600)).To(Succeed())
|
Expect(os.WriteFile(filepath.Join(testDir, "service.go"), []byte(serviceCode), 0600)).To(Succeed())
|
||||||
|
|
||||||
|
// Generate both host and client code in one run
|
||||||
cmd := exec.Command(hostgenBin, "-input", testDir, "-output", outputDir, "-package", "testpkg")
|
cmd := exec.Command(hostgenBin, "-input", testDir, "-output", outputDir, "-package", "testpkg")
|
||||||
output, err := cmd.CombinedOutput()
|
output, err := cmd.CombinedOutput()
|
||||||
Expect(err).ToNot(HaveOccurred(), "Command failed: %s", output)
|
Expect(err).ToNot(HaveOccurred(), "Command failed: %s", output)
|
||||||
|
|
||||||
|
// Verify host code
|
||||||
entries, err := os.ReadDir(outputDir)
|
entries, err := os.ReadDir(outputDir)
|
||||||
Expect(err).ToNot(HaveOccurred())
|
Expect(err).ToNot(HaveOccurred())
|
||||||
Expect(entries).To(HaveLen(1), "Expected exactly one generated file")
|
|
||||||
|
|
||||||
actual, err := os.ReadFile(filepath.Join(outputDir, entries[0].Name()))
|
var hostFiles []string
|
||||||
|
for _, e := range entries {
|
||||||
|
if e.Name() != "go" && !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())
|
Expect(err).ToNot(HaveOccurred())
|
||||||
|
|
||||||
// Format both for comparison
|
formattedHostActual, err := format.Source(hostActual)
|
||||||
formattedActual, err := format.Source(actual)
|
Expect(err).ToNot(HaveOccurred(), "Generated host code is not valid Go:\n%s", hostActual)
|
||||||
Expect(err).ToNot(HaveOccurred(), "Generated code is not valid Go:\n%s", actual)
|
|
||||||
|
|
||||||
formattedExpected, err := format.Source([]byte(expectedCode))
|
formattedHostExpected, err := format.Source([]byte(hostExpected))
|
||||||
Expect(err).ToNot(HaveOccurred(), "Expected code is not valid Go")
|
Expect(err).ToNot(HaveOccurred(), "Expected host code is not valid Go")
|
||||||
|
|
||||||
Expect(string(formattedActual)).To(Equal(string(formattedExpected)))
|
Expect(string(formattedHostActual)).To(Equal(string(formattedHostExpected)), "Host code mismatch")
|
||||||
|
|
||||||
|
// Verify client code
|
||||||
|
goDir := filepath.Join(outputDir, "go")
|
||||||
|
clientEntries, err := os.ReadDir(goDir)
|
||||||
|
Expect(err).ToNot(HaveOccurred())
|
||||||
|
Expect(clientEntries).To(HaveLen(1), "Expected exactly one client file")
|
||||||
|
|
||||||
|
clientActual, err := os.ReadFile(filepath.Join(goDir, clientEntries[0].Name()))
|
||||||
|
Expect(err).ToNot(HaveOccurred())
|
||||||
|
|
||||||
|
formattedClientActual, err := format.Source(clientActual)
|
||||||
|
Expect(err).ToNot(HaveOccurred(), "Generated client code is not valid Go:\n%s", clientActual)
|
||||||
|
|
||||||
|
formattedClientExpected, err := format.Source([]byte(clientExpected))
|
||||||
|
Expect(err).ToNot(HaveOccurred(), "Expected client code is not valid Go")
|
||||||
|
|
||||||
|
Expect(string(formattedClientActual)).To(Equal(string(formattedClientExpected)), "Client code mismatch")
|
||||||
},
|
},
|
||||||
|
|
||||||
Entry("simple string params - no request type needed",
|
Entry("simple string params",
|
||||||
"echo_service.go", "echo_expected.go"),
|
"echo_service.go", "echo_expected.go", "echo_client_expected.go"),
|
||||||
|
|
||||||
Entry("multiple simple params",
|
Entry("multiple simple params (int32)",
|
||||||
"math_service.go", "math_expected.go"),
|
"math_service.go", "math_expected.go", "math_client_expected.go"),
|
||||||
|
|
||||||
Entry("struct param with request type",
|
Entry("struct param with request type",
|
||||||
"store_service.go", "store_expected.go"),
|
"store_service.go", "store_expected.go", "store_client_expected.go"),
|
||||||
|
|
||||||
Entry("mixed simple and complex params",
|
Entry("mixed simple and complex params",
|
||||||
"list_service.go", "list_expected.go"),
|
"list_service.go", "list_expected.go", "list_client_expected.go"),
|
||||||
|
|
||||||
Entry("method without error",
|
Entry("method without error",
|
||||||
"counter_service.go", "counter_expected.go"),
|
"counter_service.go", "counter_expected.go", "counter_client_expected.go"),
|
||||||
|
|
||||||
Entry("no params, error only",
|
Entry("no params, error only",
|
||||||
"ping_service.go", "ping_expected.go"),
|
"ping_service.go", "ping_expected.go", "ping_client_expected.go"),
|
||||||
|
|
||||||
Entry("map and interface types",
|
Entry("map and interface types",
|
||||||
"meta_service.go", "meta_expected.go"),
|
"meta_service.go", "meta_expected.go", "meta_client_expected.go"),
|
||||||
|
|
||||||
Entry("pointer types",
|
Entry("pointer types",
|
||||||
"users_service.go", "users_expected.go"),
|
"users_service.go", "users_expected.go", "users_client_expected.go"),
|
||||||
|
|
||||||
Entry("multiple returns",
|
Entry("multiple returns",
|
||||||
"search_service.go", "search_expected.go"),
|
"search_service.go", "search_expected.go", "search_client_expected.go"),
|
||||||
|
|
||||||
Entry("bytes",
|
Entry("bytes",
|
||||||
"codec_service.go", "codec_expected.go"),
|
"codec_service.go", "codec_expected.go", "codec_client_expected.go"),
|
||||||
)
|
)
|
||||||
|
|
||||||
It("generates compilable code for comprehensive service", func() {
|
It("generates compilable host code for comprehensive service", func() {
|
||||||
serviceCode := readTestdata("comprehensive_service.go")
|
serviceCode := readTestdata("comprehensive_service.go")
|
||||||
|
|
||||||
Expect(os.WriteFile(filepath.Join(testDir, "service.go"), []byte(serviceCode), 0600)).To(Succeed())
|
Expect(os.WriteFile(filepath.Join(testDir, "service.go"), []byte(serviceCode), 0600)).To(Succeed())
|
||||||
@ -221,8 +298,8 @@ type ServiceB interface {
|
|||||||
goMod := "module testpkg\n\ngo 1.23\n\nrequire github.com/extism/go-sdk v1.7.1\n"
|
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())
|
Expect(os.WriteFile(filepath.Join(testDir, "go.mod"), []byte(goMod), 0600)).To(Succeed())
|
||||||
|
|
||||||
// Generate
|
// Generate host code only
|
||||||
cmd := exec.Command(hostgenBin, "-input", testDir, "-output", testDir, "-package", "testpkg")
|
cmd := exec.Command(hostgenBin, "-input", testDir, "-output", testDir, "-package", "testpkg", "-host-only")
|
||||||
output, err := cmd.CombinedOutput()
|
output, err := cmd.CombinedOutput()
|
||||||
Expect(err).ToNot(HaveOccurred(), "Generation failed: %s", output)
|
Expect(err).ToNot(HaveOccurred(), "Generation failed: %s", output)
|
||||||
|
|
||||||
@ -238,6 +315,89 @@ type ServiceB interface {
|
|||||||
buildOutput, err := buildCmd.CombinedOutput()
|
buildOutput, err := buildCmd.CombinedOutput()
|
||||||
Expect(err).ToNot(HaveOccurred(), "Build failed: %s", buildOutput)
|
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(1), "Expected exactly one generated client file")
|
||||||
|
|
||||||
|
content, err := os.ReadFile(filepath.Join(goDir, entries[0].Name()))
|
||||||
|
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()"))
|
||||||
|
|
||||||
|
// Move generated file to clientDir root for compilation
|
||||||
|
Expect(os.Rename(filepath.Join(goDir, entries[0].Name()), filepath.Join(clientDir, "nd_host.go"))).To(Succeed())
|
||||||
|
|
||||||
|
// Create go.mod for client code
|
||||||
|
goMod := "module main\n\ngo 1.23\n\nrequire github.com/extism/go-pdk v1.1.1\n"
|
||||||
|
Expect(os.WriteFile(filepath.Join(clientDir, "go.mod"), []byte(goMod), 0600)).To(Succeed())
|
||||||
|
|
||||||
|
// Add a simple main function for the plugin
|
||||||
|
mainGo := `package main
|
||||||
|
|
||||||
|
func main() {}
|
||||||
|
`
|
||||||
|
Expect(os.WriteFile(filepath.Join(clientDir, "main.go"), []byte(mainGo), 0600)).To(Succeed())
|
||||||
|
|
||||||
|
// Add type definitions needed by the generated code
|
||||||
|
typesGo := `package main
|
||||||
|
|
||||||
|
type User2 struct {
|
||||||
|
ID string
|
||||||
|
Name string
|
||||||
|
}
|
||||||
|
|
||||||
|
type Filter2 struct {
|
||||||
|
Active bool
|
||||||
|
}
|
||||||
|
`
|
||||||
|
Expect(os.WriteFile(filepath.Join(clientDir, "types.go"), []byte(typesGo), 0600)).To(Succeed())
|
||||||
|
|
||||||
|
// Tidy dependencies
|
||||||
|
goTidyCmd := exec.Command("go", "mod", "tidy")
|
||||||
|
goTidyCmd.Dir = clientDir
|
||||||
|
goTidyOutput, err := goTidyCmd.CombinedOutput()
|
||||||
|
Expect(err).ToNot(HaveOccurred(), "go mod tidy 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 = clientDir
|
||||||
|
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(clientDir, "plugin.wasm")).To(BeAnExistingFile())
|
||||||
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@ -2,14 +2,18 @@ package internal
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
|
"embed"
|
||||||
"fmt"
|
"fmt"
|
||||||
"strings"
|
"strings"
|
||||||
"text/template"
|
"text/template"
|
||||||
)
|
)
|
||||||
|
|
||||||
// GenerateService generates the host function wrapper code for a service.
|
//go:embed templates/*.tmpl
|
||||||
func GenerateService(svc Service, pkgName string) ([]byte, error) {
|
var templatesFS embed.FS
|
||||||
tmpl, err := template.New("service").Funcs(template.FuncMap{
|
|
||||||
|
// hostFuncMap returns the template functions for host code generation.
|
||||||
|
func hostFuncMap(svc Service) template.FuncMap {
|
||||||
|
return template.FuncMap{
|
||||||
"lower": strings.ToLower,
|
"lower": strings.ToLower,
|
||||||
"title": strings.Title,
|
"title": strings.Title,
|
||||||
"exportName": func(m Method) string { return m.FunctionName(svc.ExportPrefix()) },
|
"exportName": func(m Method) string { return m.FunctionName(svc.ExportPrefix()) },
|
||||||
@ -27,7 +31,39 @@ func GenerateService(svc Service, pkgName string) ([]byte, error) {
|
|||||||
"readParam": generateReadParam,
|
"readParam": generateReadParam,
|
||||||
"writeReturn": generateWriteReturn,
|
"writeReturn": generateWriteReturn,
|
||||||
"encodeReturn": generateEncodeReturn,
|
"encodeReturn": generateEncodeReturn,
|
||||||
}).Parse(serviceTemplate)
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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()) },
|
||||||
|
"responseType": func(m Method) string { return m.ResponseTypeName(svc.Name) },
|
||||||
|
"isSimple": IsSimpleType,
|
||||||
|
"isString": IsStringType,
|
||||||
|
"isBytes": IsBytesType,
|
||||||
|
"needsJSON": NeedsJSON,
|
||||||
|
"needsRespType": func(m Method) bool { return m.NeedsResponseType() },
|
||||||
|
"isErrorOnly": func(m Method) bool { return m.IsErrorOnly() },
|
||||||
|
"wasmParamType": wasmParamType,
|
||||||
|
"wasmReturnType": wasmReturnType,
|
||||||
|
"wrapperReturnType": func(m Method, svcName string) string { return wrapperReturnType(m, svcName) },
|
||||||
|
"clientCallArg": clientCallArg,
|
||||||
|
"decodeResult": decodeResult,
|
||||||
|
"formatDoc": formatDoc,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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 {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("parsing template: %w", err)
|
return nil, fmt.Errorf("parsing template: %w", err)
|
||||||
}
|
}
|
||||||
@ -48,10 +84,43 @@ func GenerateService(svc Service, pkgName string) ([]byte, error) {
|
|||||||
return buf.Bytes(), nil
|
return buf.Bytes(), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GenerateService generates the host function wrapper code for a service.
|
||||||
|
// Deprecated: Use GenerateHost instead.
|
||||||
|
func GenerateService(svc Service, pkgName string) ([]byte, error) {
|
||||||
|
return GenerateHost(svc, pkgName)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GenerateClientGo generates client wrapper code for plugins to call host functions.
|
||||||
|
func GenerateClientGo(svc Service) ([]byte, error) {
|
||||||
|
tmplContent, err := templatesFS.ReadFile("templates/client_go.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,
|
||||||
|
NeedsJSON: serviceClientNeedsJSON(svc),
|
||||||
|
NeedsErrors: serviceClientNeedsErrors(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 {
|
type templateData struct {
|
||||||
Package string
|
Package string
|
||||||
Service Service
|
Service Service
|
||||||
NeedsJSON bool
|
NeedsJSON bool
|
||||||
|
NeedsErrors bool // Client: needs "errors" import
|
||||||
NeedsWriteHelper bool
|
NeedsWriteHelper bool
|
||||||
NeedsErrorHelper bool
|
NeedsErrorHelper bool
|
||||||
}
|
}
|
||||||
@ -87,6 +156,35 @@ func serviceNeedsWriteHelper(svc Service) bool {
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// serviceClientNeedsJSON returns true if any method needs JSON encoding in client code.
|
||||||
|
// This is true if any method has a response type (complex returns) or if any param/return needs JSON.
|
||||||
|
func serviceClientNeedsJSON(svc Service) bool {
|
||||||
|
for _, m := range svc.Methods {
|
||||||
|
// Response types use JSON for serialization
|
||||||
|
if m.NeedsResponseType() {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
// Parameters that need JSON marshaling
|
||||||
|
for _, p := range m.Params {
|
||||||
|
if NeedsJSON(p.Type) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// serviceClientNeedsErrors returns true if any method needs the errors package in client code.
|
||||||
|
// This is true if any method returns an error.
|
||||||
|
func serviceClientNeedsErrors(svc Service) bool {
|
||||||
|
for _, m := range svc.Methods {
|
||||||
|
if m.HasError {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
// serviceNeedsErrorHelper returns true if any method needs error handling with JSON.
|
// serviceNeedsErrorHelper returns true if any method needs error handling with JSON.
|
||||||
func serviceNeedsErrorHelper(svc Service) bool {
|
func serviceNeedsErrorHelper(svc Service) bool {
|
||||||
for _, m := range svc.Methods {
|
for _, m := range svc.Methods {
|
||||||
@ -206,165 +304,94 @@ func generateEncodeReturn(p Param, varName string) string {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const serviceTemplate = `// Code generated by hostgen. DO NOT EDIT.
|
// Client-side helper functions for template
|
||||||
|
|
||||||
package {{.Package}}
|
// wasmParamType returns the WASM parameter type for a Go parameter.
|
||||||
|
func wasmParamType(p Param) string {
|
||||||
import (
|
if IsSimpleType(p.Type) {
|
||||||
"context"
|
return p.Type
|
||||||
{{- if .NeedsJSON}}
|
}
|
||||||
"encoding/json"
|
// All pointer types (string, []byte, complex) use uint64 offset
|
||||||
{{- end}}
|
return "uint64"
|
||||||
|
|
||||||
extism "github.com/extism/go-sdk"
|
|
||||||
)
|
|
||||||
|
|
||||||
{{- /* Generate request/response types only when needed */ -}}
|
|
||||||
{{range .Service.Methods}}
|
|
||||||
{{- if needsRequestType .}}
|
|
||||||
|
|
||||||
// {{requestType .}} is the request type for {{$.Service.Name}}.{{.Name}}.
|
|
||||||
type {{requestType .}} struct {
|
|
||||||
{{- range .Params}}
|
|
||||||
{{title .Name}} {{.Type}} ` + "`" + `json:"{{.JSONName}}"` + "`" + `
|
|
||||||
{{- end}}
|
|
||||||
}
|
}
|
||||||
{{- end}}
|
|
||||||
{{- if needsRespType .}}
|
|
||||||
|
|
||||||
// {{responseType .}} is the response type for {{$.Service.Name}}.{{.Name}}.
|
// wasmReturnType returns the WASM return type declaration for a method.
|
||||||
type {{responseType .}} struct {
|
func wasmReturnType(m Method) string {
|
||||||
{{- range .Returns}}
|
// Methods with JSON responses or error-only return uint64 (pointer)
|
||||||
{{title .Name}} {{.Type}} ` + "`" + `json:"{{.JSONName}},omitempty"` + "`" + `
|
if m.NeedsResponseType() || m.IsErrorOnly() {
|
||||||
{{- end}}
|
return "uint64"
|
||||||
Error string ` + "`" + `json:"error,omitempty"` + "`" + `
|
}
|
||||||
|
// Simple return types
|
||||||
|
if len(m.Returns) == 1 && IsSimpleType(m.Returns[0].Type) {
|
||||||
|
return m.Returns[0].Type
|
||||||
|
}
|
||||||
|
// No returns or multiple returns - use uint64 for pointer
|
||||||
|
if len(m.Returns) == 0 {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
return "uint64"
|
||||||
}
|
}
|
||||||
{{- end}}
|
|
||||||
{{end}}
|
|
||||||
|
|
||||||
// Register{{.Service.Name}}HostFunctions registers {{.Service.Name}} service host functions.
|
// wrapperReturnType returns the Go return type for the wrapper function.
|
||||||
// The returned host functions should be added to the plugin's configuration.
|
func wrapperReturnType(m Method, svcName string) string {
|
||||||
func Register{{.Service.Name}}HostFunctions(service {{.Service.Interface}}) []extism.HostFunction {
|
if m.NeedsResponseType() {
|
||||||
return []extism.HostFunction{
|
return fmt.Sprintf("(*%s%sResponse, error)", svcName, m.Name)
|
||||||
{{- range .Service.Methods}}
|
}
|
||||||
new{{$.Service.Name}}{{.Name}}HostFunction(service),
|
if m.IsErrorOnly() {
|
||||||
{{- end}}
|
return "error"
|
||||||
|
}
|
||||||
|
if len(m.Returns) == 1 {
|
||||||
|
return m.Returns[0].Type
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
// clientCallArg returns the argument expression for calling the host function.
|
||||||
|
func clientCallArg(p Param) string {
|
||||||
|
if IsSimpleType(p.Type) {
|
||||||
|
return p.Name
|
||||||
|
}
|
||||||
|
// Pointer types use .Offset()
|
||||||
|
return p.Name + "Mem.Offset()"
|
||||||
|
}
|
||||||
|
|
||||||
|
// decodeResult generates code to decode a simple return value.
|
||||||
|
func decodeResult(p Param, varName string) string {
|
||||||
|
switch p.Type {
|
||||||
|
case "int32":
|
||||||
|
return fmt.Sprintf("int32(%s)", varName)
|
||||||
|
case "uint32":
|
||||||
|
return fmt.Sprintf("uint32(%s)", varName)
|
||||||
|
case "int64":
|
||||||
|
return fmt.Sprintf("int64(%s)", varName)
|
||||||
|
case "uint64":
|
||||||
|
return varName
|
||||||
|
case "float32":
|
||||||
|
return fmt.Sprintf("math.Float32frombits(uint32(%s))", varName)
|
||||||
|
case "float64":
|
||||||
|
return fmt.Sprintf("math.Float64frombits(%s)", varName)
|
||||||
|
case "bool":
|
||||||
|
return fmt.Sprintf("%s != 0", varName)
|
||||||
|
case "string":
|
||||||
|
// pdk.FindMemory returns a value type, ReadBytes has pointer receiver
|
||||||
|
return fmt.Sprintf("func() string { m := pdk.FindMemory(%s); return string(m.ReadBytes()) }()", varName)
|
||||||
|
case "[]byte":
|
||||||
|
return fmt.Sprintf("func() []byte { m := pdk.FindMemory(%s); return m.ReadBytes() }()", varName)
|
||||||
|
default:
|
||||||
|
return varName
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
{{range .Service.Methods}}
|
|
||||||
|
|
||||||
func new{{$.Service.Name}}{{.Name}}HostFunction(service {{$.Service.Interface}}) extism.HostFunction {
|
// formatDoc formats a documentation string for Go comments.
|
||||||
return extism.NewHostFunctionWithStack(
|
// It prefixes each line with "// " and trims trailing whitespace.
|
||||||
"{{exportName .}}",
|
func formatDoc(doc string) string {
|
||||||
func(ctx context.Context, p *extism.CurrentPlugin, stack []uint64) {
|
if doc == "" {
|
||||||
{{- if .HasParams}}
|
return ""
|
||||||
{{- if needsRequestType .}}
|
|
||||||
// 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
|
|
||||||
}
|
|
||||||
{{- else}}
|
|
||||||
// Read parameters from stack
|
|
||||||
{{- range $i, $p := .Params}}
|
|
||||||
{{readParam $p $i}}
|
|
||||||
{{- end}}
|
|
||||||
{{- end}}
|
|
||||||
{{- end}}
|
|
||||||
|
|
||||||
// Call the service method
|
|
||||||
{{- $m := .}}
|
|
||||||
{{- if .HasReturns}}
|
|
||||||
{{- if .HasError}}
|
|
||||||
{{range $i, $r := .Returns}}{{if $i}}, {{end}}{{lower $r.Name}}{{end}}, err := service.{{.Name}}(ctx{{range .Params}}, {{if needsRequestType $m}}req.{{title .Name}}{{else}}{{.Name}}{{end}}{{end}})
|
|
||||||
{{- else}}
|
|
||||||
{{range $i, $r := .Returns}}{{if $i}}, {{end}}{{lower $r.Name}}{{end}} := service.{{.Name}}(ctx{{range .Params}}, {{if needsRequestType $m}}req.{{title .Name}}{{else}}{{.Name}}{{end}}{{end}})
|
|
||||||
{{- end}}
|
|
||||||
{{- else if .HasError}}
|
|
||||||
err {{if hasErrFromRead .}}={{else}}:={{end}} service.{{.Name}}(ctx{{range .Params}}, {{if needsRequestType $m}}req.{{title .Name}}{{else}}{{.Name}}{{end}}{{end}})
|
|
||||||
{{- else}}
|
|
||||||
service.{{.Name}}(ctx{{range .Params}}, {{if needsRequestType $m}}req.{{title .Name}}{{else}}{{.Name}}{{end}}{{end}})
|
|
||||||
{{- end}}
|
|
||||||
{{- if .HasError}}
|
|
||||||
if err != nil {
|
|
||||||
{{- if isErrorOnly .}}
|
|
||||||
// Write error string to plugin memory
|
|
||||||
if ptr, err := p.WriteString(err.Error()); err == nil {
|
|
||||||
stack[0] = ptr
|
|
||||||
}
|
|
||||||
{{- else if needsRespType .}}
|
|
||||||
{{$.Service.Name | lower}}WriteError(p, stack, err)
|
|
||||||
{{- end}}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
{{- end}}
|
|
||||||
|
|
||||||
{{- if isErrorOnly .}}
|
|
||||||
// Write empty string to indicate success
|
|
||||||
if ptr, err := p.WriteString(""); err == nil {
|
|
||||||
stack[0] = ptr
|
|
||||||
}
|
|
||||||
{{- else if needsRespType .}}
|
|
||||||
// Write JSON response to plugin memory
|
|
||||||
resp := {{responseType .}}{
|
|
||||||
{{- range .Returns}}
|
|
||||||
{{title .Name}}: {{lower .Name}},
|
|
||||||
{{- end}}
|
|
||||||
}
|
|
||||||
{{$.Service.Name | lower}}WriteResponse(p, stack, resp)
|
|
||||||
{{- else if .HasReturns}}
|
|
||||||
// Write return values to stack
|
|
||||||
{{- range $i, $r := .Returns}}
|
|
||||||
{{writeReturn $r $i (lower $r.Name)}}
|
|
||||||
{{- end}}
|
|
||||||
{{- end}}
|
|
||||||
},
|
|
||||||
{{- if needsRequestType $m}}
|
|
||||||
[]extism.ValueType{extism.ValueTypePTR},
|
|
||||||
{{- else}}
|
|
||||||
[]extism.ValueType{ {{- range $i, $p := .Params}}{{if $i}}, {{end}}{{valueType $p.Type}}{{end}}{{if not .HasParams}}{{end}} },
|
|
||||||
{{- end}}
|
|
||||||
{{- if or (needsRespType .) (isErrorOnly .)}}
|
|
||||||
[]extism.ValueType{extism.ValueTypePTR},
|
|
||||||
{{- else}}
|
|
||||||
[]extism.ValueType{ {{- range $i, $r := .Returns}}{{if $i}}, {{end}}{{valueType $r.Type}}{{end}}{{if not .HasReturns}}{{end}} },
|
|
||||||
{{- end}}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
{{end}}
|
|
||||||
{{- if .NeedsWriteHelper}}
|
|
||||||
|
|
||||||
// {{.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)
|
lines := strings.Split(strings.TrimSpace(doc), "\n")
|
||||||
if err != nil {
|
var result []string
|
||||||
stack[0] = 0
|
for _, line := range lines {
|
||||||
return
|
result = append(result, "// "+strings.TrimRight(line, " \t"))
|
||||||
}
|
}
|
||||||
stack[0] = respPtr
|
return strings.Join(result, "\n")
|
||||||
}
|
}
|
||||||
{{- end}}
|
|
||||||
{{- if .NeedsErrorHelper}}
|
|
||||||
|
|
||||||
// {{.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
|
|
||||||
}
|
|
||||||
{{- end}}
|
|
||||||
`
|
|
||||||
|
|||||||
139
plugins/cmd/hostgen/internal/templates/client_go.go.tmpl
Normal file
139
plugins/cmd/hostgen/internal/templates/client_go.go.tmpl
Normal file
@ -0,0 +1,139 @@
|
|||||||
|
// 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.
|
||||||
|
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
{{- if .NeedsJSON}}
|
||||||
|
"encoding/json"
|
||||||
|
{{- end}}
|
||||||
|
{{- if .NeedsErrors}}
|
||||||
|
"errors"
|
||||||
|
{{- end}}
|
||||||
|
|
||||||
|
"github.com/extism/go-pdk"
|
||||||
|
)
|
||||||
|
|
||||||
|
{{- /* 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 .}}({{range $i, $p := .Params}}{{if $i}}, {{end}}{{wasmParamType $p}}{{end}}) {{wasmReturnType .}}
|
||||||
|
{{- end}}
|
||||||
|
|
||||||
|
{{- /* Generate response types for methods that need them */ -}}
|
||||||
|
{{range .Service.Methods}}
|
||||||
|
{{- if needsRespType .}}
|
||||||
|
|
||||||
|
// {{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}}
|
||||||
|
{{- 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}}) {{wrapperReturnType . $.Service.Name}} {
|
||||||
|
{{- if needsRespType .}}
|
||||||
|
{{- /* Complex response - use JSON */}}
|
||||||
|
{{- range .Params}}
|
||||||
|
{{- if isString .Type}}
|
||||||
|
{{.Name}}Mem := pdk.AllocateString({{.Name}})
|
||||||
|
defer {{.Name}}Mem.Free()
|
||||||
|
{{- else if isBytes .Type}}
|
||||||
|
{{.Name}}Mem := pdk.AllocateBytes({{.Name}})
|
||||||
|
defer {{.Name}}Mem.Free()
|
||||||
|
{{- else if needsJSON .Type}}
|
||||||
|
{{.Name}}Bytes, err := json.Marshal({{.Name}})
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
{{.Name}}Mem := pdk.AllocateBytes({{.Name}}Bytes)
|
||||||
|
defer {{.Name}}Mem.Free()
|
||||||
|
{{- end}}
|
||||||
|
{{- end}}
|
||||||
|
|
||||||
|
// Call the host function
|
||||||
|
responsePtr := {{exportName .}}({{range $i, $p := .Params}}{{if $i}}, {{end}}{{clientCallArg $p}}{{end}})
|
||||||
|
|
||||||
|
// 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
|
||||||
|
}
|
||||||
|
|
||||||
|
if response.Error != "" {
|
||||||
|
return nil, errors.New(response.Error)
|
||||||
|
}
|
||||||
|
|
||||||
|
return &response, nil
|
||||||
|
{{- else if isErrorOnly .}}
|
||||||
|
{{- /* Error-only response - string result */}}
|
||||||
|
{{- range .Params}}
|
||||||
|
{{- if isString .Type}}
|
||||||
|
{{.Name}}Mem := pdk.AllocateString({{.Name}})
|
||||||
|
defer {{.Name}}Mem.Free()
|
||||||
|
{{- else if isBytes .Type}}
|
||||||
|
{{.Name}}Mem := pdk.AllocateBytes({{.Name}})
|
||||||
|
defer {{.Name}}Mem.Free()
|
||||||
|
{{- else if needsJSON .Type}}
|
||||||
|
{{.Name}}Bytes, err := json.Marshal({{.Name}})
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
{{.Name}}Mem := pdk.AllocateBytes({{.Name}}Bytes)
|
||||||
|
defer {{.Name}}Mem.Free()
|
||||||
|
{{- end}}
|
||||||
|
{{- end}}
|
||||||
|
|
||||||
|
// Call the host function
|
||||||
|
responsePtr := {{exportName .}}({{range $i, $p := .Params}}{{if $i}}, {{end}}{{clientCallArg $p}}{{end}})
|
||||||
|
|
||||||
|
// Read the response from memory
|
||||||
|
responseMem := pdk.FindMemory(responsePtr)
|
||||||
|
errStr := string(responseMem.ReadBytes())
|
||||||
|
|
||||||
|
if errStr != "" {
|
||||||
|
return errors.New(errStr)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
{{- else}}
|
||||||
|
{{- /* Simple return types - direct stack values */}}
|
||||||
|
{{- range .Params}}
|
||||||
|
{{- if isString .Type}}
|
||||||
|
{{.Name}}Mem := pdk.AllocateString({{.Name}})
|
||||||
|
defer {{.Name}}Mem.Free()
|
||||||
|
{{- else if isBytes .Type}}
|
||||||
|
{{.Name}}Mem := pdk.AllocateBytes({{.Name}})
|
||||||
|
defer {{.Name}}Mem.Free()
|
||||||
|
{{- end}}
|
||||||
|
{{- end}}
|
||||||
|
|
||||||
|
// Call the host function
|
||||||
|
{{- if .HasReturns}}
|
||||||
|
result := {{exportName .}}({{range $i, $p := .Params}}{{if $i}}, {{end}}{{clientCallArg $p}}{{end}})
|
||||||
|
return {{decodeResult (index .Returns 0) "result"}}
|
||||||
|
{{- else}}
|
||||||
|
{{exportName .}}({{range $i, $p := .Params}}{{if $i}}, {{end}}{{clientCallArg $p}}{{end}})
|
||||||
|
{{- end}}
|
||||||
|
{{- end}}
|
||||||
|
}
|
||||||
|
{{- end}}
|
||||||
161
plugins/cmd/hostgen/internal/templates/host.go.tmpl
Normal file
161
plugins/cmd/hostgen/internal/templates/host.go.tmpl
Normal file
@ -0,0 +1,161 @@
|
|||||||
|
// Code generated by hostgen. DO NOT EDIT.
|
||||||
|
|
||||||
|
package {{.Package}}
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
{{- if .NeedsJSON}}
|
||||||
|
"encoding/json"
|
||||||
|
{{- end}}
|
||||||
|
|
||||||
|
extism "github.com/extism/go-sdk"
|
||||||
|
)
|
||||||
|
|
||||||
|
{{- /* Generate request/response types only when needed */ -}}
|
||||||
|
{{range .Service.Methods}}
|
||||||
|
{{- if needsRequestType .}}
|
||||||
|
|
||||||
|
// {{requestType .}} is the request type for {{$.Service.Name}}.{{.Name}}.
|
||||||
|
type {{requestType .}} struct {
|
||||||
|
{{- range .Params}}
|
||||||
|
{{title .Name}} {{.Type}} `json:"{{.JSONName}}"`
|
||||||
|
{{- end}}
|
||||||
|
}
|
||||||
|
{{- end}}
|
||||||
|
{{- if needsRespType .}}
|
||||||
|
|
||||||
|
// {{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}}
|
||||||
|
{{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}}
|
||||||
|
{{- if needsRequestType .}}
|
||||||
|
// 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
|
||||||
|
}
|
||||||
|
{{- else}}
|
||||||
|
// Read parameters from stack
|
||||||
|
{{- range $i, $p := .Params}}
|
||||||
|
{{readParam $p $i}}
|
||||||
|
{{- end}}
|
||||||
|
{{- end}}
|
||||||
|
{{- end}}
|
||||||
|
|
||||||
|
// Call the service method
|
||||||
|
{{- $m := .}}
|
||||||
|
{{- if .HasReturns}}
|
||||||
|
{{- if .HasError}}
|
||||||
|
{{range $i, $r := .Returns}}{{if $i}}, {{end}}{{lower $r.Name}}{{end}}, err := service.{{.Name}}(ctx{{range .Params}}, {{if needsRequestType $m}}req.{{title .Name}}{{else}}{{.Name}}{{end}}{{end}})
|
||||||
|
{{- else}}
|
||||||
|
{{range $i, $r := .Returns}}{{if $i}}, {{end}}{{lower $r.Name}}{{end}} := service.{{.Name}}(ctx{{range .Params}}, {{if needsRequestType $m}}req.{{title .Name}}{{else}}{{.Name}}{{end}}{{end}})
|
||||||
|
{{- end}}
|
||||||
|
{{- else if .HasError}}
|
||||||
|
err {{if hasErrFromRead .}}={{else}}:={{end}} service.{{.Name}}(ctx{{range .Params}}, {{if needsRequestType $m}}req.{{title .Name}}{{else}}{{.Name}}{{end}}{{end}})
|
||||||
|
{{- else}}
|
||||||
|
service.{{.Name}}(ctx{{range .Params}}, {{if needsRequestType $m}}req.{{title .Name}}{{else}}{{.Name}}{{end}}{{end}})
|
||||||
|
{{- end}}
|
||||||
|
{{- if .HasError}}
|
||||||
|
if err != nil {
|
||||||
|
{{- if isErrorOnly .}}
|
||||||
|
// Write error string to plugin memory
|
||||||
|
if ptr, err := p.WriteString(err.Error()); err == nil {
|
||||||
|
stack[0] = ptr
|
||||||
|
}
|
||||||
|
{{- else if needsRespType .}}
|
||||||
|
{{$.Service.Name | lower}}WriteError(p, stack, err)
|
||||||
|
{{- end}}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
{{- end}}
|
||||||
|
|
||||||
|
{{- if isErrorOnly .}}
|
||||||
|
// Write empty string to indicate success
|
||||||
|
if ptr, err := p.WriteString(""); err == nil {
|
||||||
|
stack[0] = ptr
|
||||||
|
}
|
||||||
|
{{- else if needsRespType .}}
|
||||||
|
// Write JSON response to plugin memory
|
||||||
|
resp := {{responseType .}}{
|
||||||
|
{{- range .Returns}}
|
||||||
|
{{title .Name}}: {{lower .Name}},
|
||||||
|
{{- end}}
|
||||||
|
}
|
||||||
|
{{$.Service.Name | lower}}WriteResponse(p, stack, resp)
|
||||||
|
{{- else if .HasReturns}}
|
||||||
|
// Write return values to stack
|
||||||
|
{{- range $i, $r := .Returns}}
|
||||||
|
{{writeReturn $r $i (lower $r.Name)}}
|
||||||
|
{{- end}}
|
||||||
|
{{- end}}
|
||||||
|
},
|
||||||
|
{{- if needsRequestType $m}}
|
||||||
|
[]extism.ValueType{extism.ValueTypePTR},
|
||||||
|
{{- else}}
|
||||||
|
[]extism.ValueType{ {{- range $i, $p := .Params}}{{if $i}}, {{end}}{{valueType $p.Type}}{{end}}{{if not .HasParams}}{{end}} },
|
||||||
|
{{- end}}
|
||||||
|
{{- if or (needsRespType .) (isErrorOnly .)}}
|
||||||
|
[]extism.ValueType{extism.ValueTypePTR},
|
||||||
|
{{- else}}
|
||||||
|
[]extism.ValueType{ {{- range $i, $r := .Returns}}{{if $i}}, {{end}}{{valueType $r.Type}}{{end}}{{if not .HasReturns}}{{end}} },
|
||||||
|
{{- end}}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
{{end}}
|
||||||
|
{{- if .NeedsWriteHelper}}
|
||||||
|
|
||||||
|
// {{.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
|
||||||
|
}
|
||||||
|
{{- end}}
|
||||||
|
{{- if .NeedsErrorHelper}}
|
||||||
|
|
||||||
|
// {{.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
|
||||||
|
}
|
||||||
|
{{- end}}
|
||||||
@ -6,11 +6,13 @@
|
|||||||
//
|
//
|
||||||
// Flags:
|
// Flags:
|
||||||
//
|
//
|
||||||
// -input Input directory containing Go source files with annotated interfaces
|
// -input Input directory containing Go source files with annotated interfaces
|
||||||
// -output Output directory for generated files (default: same as input)
|
// -output Output directory for generated files (default: same as input)
|
||||||
// -package Output package name (default: inferred from output directory)
|
// -package Output package name (default: inferred from output directory)
|
||||||
// -v Verbose output
|
// -host-only Generate only host-side code (default: false)
|
||||||
// -dry-run Preview generated code without writing files
|
// -plugin-only Generate only plugin/client-side code (default: false)
|
||||||
|
// -v Verbose output
|
||||||
|
// -dry-run Preview generated code without writing files
|
||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
@ -19,20 +21,29 @@ import (
|
|||||||
"go/format"
|
"go/format"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
|
||||||
"github.com/navidrome/navidrome/plugins/cmd/hostgen/internal"
|
"github.com/navidrome/navidrome/plugins/cmd/hostgen/internal"
|
||||||
)
|
)
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
var (
|
var (
|
||||||
inputDir = flag.String("input", ".", "Input directory containing Go source files")
|
inputDir = flag.String("input", ".", "Input directory containing Go source files")
|
||||||
outputDir = flag.String("output", "", "Output directory for generated files (default: same as input)")
|
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)")
|
pkgName = flag.String("package", "", "Output package name (default: inferred from output directory)")
|
||||||
verbose = flag.Bool("v", false, "Verbose output")
|
hostOnly = flag.Bool("host-only", false, "Generate only host-side code")
|
||||||
dryRun = flag.Bool("dry-run", false, "Preview generated code without writing files")
|
pluginOnly = flag.Bool("plugin-only", false, "Generate only plugin/client-side code")
|
||||||
|
verbose = flag.Bool("v", false, "Verbose output")
|
||||||
|
dryRun = flag.Bool("dry-run", false, "Preview generated code without writing files")
|
||||||
)
|
)
|
||||||
flag.Parse()
|
flag.Parse()
|
||||||
|
|
||||||
|
// Validate conflicting flags
|
||||||
|
if *hostOnly && *pluginOnly {
|
||||||
|
fmt.Fprintf(os.Stderr, "Error: -host-only and -plugin-only cannot be used together\n")
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
if *outputDir == "" {
|
if *outputDir == "" {
|
||||||
*outputDir = *inputDir
|
*outputDir = *inputDir
|
||||||
}
|
}
|
||||||
@ -54,10 +65,16 @@ func main() {
|
|||||||
*pkgName = filepath.Base(absOutput)
|
*pkgName = filepath.Base(absOutput)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Determine what to generate
|
||||||
|
generateHost := !*pluginOnly
|
||||||
|
generateClient := !*hostOnly
|
||||||
|
|
||||||
if *verbose {
|
if *verbose {
|
||||||
fmt.Printf("Input directory: %s\n", absInput)
|
fmt.Printf("Input directory: %s\n", absInput)
|
||||||
fmt.Printf("Output directory: %s\n", absOutput)
|
fmt.Printf("Output directory: %s\n", absOutput)
|
||||||
fmt.Printf("Package name: %s\n", *pkgName)
|
fmt.Printf("Package name: %s\n", *pkgName)
|
||||||
|
fmt.Printf("Generate host code: %v\n", generateHost)
|
||||||
|
fmt.Printf("Generate client code: %v\n", generateClient)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Parse source files
|
// Parse source files
|
||||||
@ -83,34 +100,85 @@ func main() {
|
|||||||
|
|
||||||
// Generate code for each service
|
// Generate code for each service
|
||||||
for _, svc := range services {
|
for _, svc := range services {
|
||||||
code, err := internal.GenerateService(svc, *pkgName)
|
// Generate host-side code
|
||||||
if err != nil {
|
if generateHost {
|
||||||
fmt.Fprintf(os.Stderr, "Error generating code for %s: %v\n", svc.Name, err)
|
if err := generateHostCode(svc, *pkgName, absOutput, *dryRun, *verbose); err != nil {
|
||||||
os.Exit(1)
|
fmt.Fprintf(os.Stderr, "Error generating host code for %s: %v\n", svc.Name, err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Format the generated code
|
// Generate client-side code
|
||||||
formatted, err := format.Source(code)
|
if generateClient {
|
||||||
if err != nil {
|
if err := generateClientCode(svc, absOutput, *dryRun, *verbose); err != nil {
|
||||||
fmt.Fprintf(os.Stderr, "Error formatting generated code for %s: %v\n", svc.Name, err)
|
fmt.Fprintf(os.Stderr, "Error generating client code for %s: %v\n", svc.Name, err)
|
||||||
fmt.Fprintf(os.Stderr, "Raw code:\n%s\n", code)
|
os.Exit(1)
|
||||||
os.Exit(1)
|
}
|
||||||
}
|
|
||||||
|
|
||||||
outputFile := filepath.Join(absOutput, svc.OutputFileName())
|
|
||||||
|
|
||||||
if *dryRun {
|
|
||||||
fmt.Printf("=== %s ===\n%s\n", outputFile, formatted)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := os.WriteFile(outputFile, formatted, 0600); err != nil {
|
|
||||||
fmt.Fprintf(os.Stderr, "Error writing %s: %v\n", outputFile, err)
|
|
||||||
os.Exit(1)
|
|
||||||
}
|
|
||||||
|
|
||||||
if *verbose {
|
|
||||||
fmt.Printf("Generated %s\n", outputFile)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 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
|
||||||
|
}
|
||||||
|
|
||||||
|
// generateClientCode generates client-side code for a service.
|
||||||
|
func generateClientCode(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)
|
||||||
|
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(clientFile, formatted, 0600); err != nil {
|
||||||
|
return fmt.Errorf("writing file: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if verbose {
|
||||||
|
fmt.Printf("Generated client code: %s\n", clientFile)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|||||||
49
plugins/cmd/hostgen/testdata/codec_client_expected.go
vendored
Normal file
49
plugins/cmd/hostgen/testdata/codec_client_expected.go
vendored
Normal file
@ -0,0 +1,49 @@
|
|||||||
|
// Code generated by hostgen. DO NOT EDIT.
|
||||||
|
//
|
||||||
|
// This file contains client wrappers for the Codec host service.
|
||||||
|
// It is intended for use in Navidrome plugins built with TinyGo.
|
||||||
|
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
|
||||||
|
"github.com/extism/go-pdk"
|
||||||
|
)
|
||||||
|
|
||||||
|
// codec_encode is the host function provided by Navidrome.
|
||||||
|
//
|
||||||
|
//go:wasmimport extism:host/user codec_encode
|
||||||
|
func codec_encode(uint64) uint64
|
||||||
|
|
||||||
|
// CodecEncodeResponse is the response type for Codec.Encode.
|
||||||
|
type CodecEncodeResponse struct {
|
||||||
|
Result []byte `json:"result,omitempty"`
|
||||||
|
Error string `json:"error,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// CodecEncode calls the codec_encode host function.
|
||||||
|
func CodecEncode(data []byte) (*CodecEncodeResponse, error) {
|
||||||
|
dataMem := pdk.AllocateBytes(data)
|
||||||
|
defer dataMem.Free()
|
||||||
|
|
||||||
|
// Call the host function
|
||||||
|
responsePtr := codec_encode(dataMem.Offset())
|
||||||
|
|
||||||
|
// Read the response from memory
|
||||||
|
responseMem := pdk.FindMemory(responsePtr)
|
||||||
|
responseBytes := responseMem.ReadBytes()
|
||||||
|
|
||||||
|
// Parse the response
|
||||||
|
var response CodecEncodeResponse
|
||||||
|
if err := json.Unmarshal(responseBytes, &response); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if response.Error != "" {
|
||||||
|
return nil, errors.New(response.Error)
|
||||||
|
}
|
||||||
|
|
||||||
|
return &response, nil
|
||||||
|
}
|
||||||
25
plugins/cmd/hostgen/testdata/counter_client_expected.go
vendored
Normal file
25
plugins/cmd/hostgen/testdata/counter_client_expected.go
vendored
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
// Code generated by hostgen. DO NOT EDIT.
|
||||||
|
//
|
||||||
|
// This file contains client wrappers for the Counter host service.
|
||||||
|
// It is intended for use in Navidrome plugins built with TinyGo.
|
||||||
|
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/extism/go-pdk"
|
||||||
|
)
|
||||||
|
|
||||||
|
// counter_count is the host function provided by Navidrome.
|
||||||
|
//
|
||||||
|
//go:wasmimport extism:host/user counter_count
|
||||||
|
func counter_count(uint64) int32
|
||||||
|
|
||||||
|
// CounterCount calls the counter_count host function.
|
||||||
|
func CounterCount(name string) int32 {
|
||||||
|
nameMem := pdk.AllocateString(name)
|
||||||
|
defer nameMem.Free()
|
||||||
|
|
||||||
|
// Call the host function
|
||||||
|
result := counter_count(nameMem.Offset())
|
||||||
|
return int32(result)
|
||||||
|
}
|
||||||
49
plugins/cmd/hostgen/testdata/echo_client_expected.go
vendored
Normal file
49
plugins/cmd/hostgen/testdata/echo_client_expected.go
vendored
Normal file
@ -0,0 +1,49 @@
|
|||||||
|
// Code generated by hostgen. DO NOT EDIT.
|
||||||
|
//
|
||||||
|
// This file contains client wrappers for the Echo host service.
|
||||||
|
// It is intended for use in Navidrome plugins built with TinyGo.
|
||||||
|
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
|
||||||
|
"github.com/extism/go-pdk"
|
||||||
|
)
|
||||||
|
|
||||||
|
// echo_echo is the host function provided by Navidrome.
|
||||||
|
//
|
||||||
|
//go:wasmimport extism:host/user echo_echo
|
||||||
|
func echo_echo(uint64) uint64
|
||||||
|
|
||||||
|
// EchoEchoResponse is the response type for Echo.Echo.
|
||||||
|
type EchoEchoResponse struct {
|
||||||
|
Reply string `json:"reply,omitempty"`
|
||||||
|
Error string `json:"error,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// EchoEcho calls the echo_echo host function.
|
||||||
|
func EchoEcho(message string) (*EchoEchoResponse, error) {
|
||||||
|
messageMem := pdk.AllocateString(message)
|
||||||
|
defer messageMem.Free()
|
||||||
|
|
||||||
|
// Call the host function
|
||||||
|
responsePtr := echo_echo(messageMem.Offset())
|
||||||
|
|
||||||
|
// Read the response from memory
|
||||||
|
responseMem := pdk.FindMemory(responsePtr)
|
||||||
|
responseBytes := responseMem.ReadBytes()
|
||||||
|
|
||||||
|
// Parse the response
|
||||||
|
var response EchoEchoResponse
|
||||||
|
if err := json.Unmarshal(responseBytes, &response); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if response.Error != "" {
|
||||||
|
return nil, errors.New(response.Error)
|
||||||
|
}
|
||||||
|
|
||||||
|
return &response, nil
|
||||||
|
}
|
||||||
55
plugins/cmd/hostgen/testdata/list_client_expected.go
vendored
Normal file
55
plugins/cmd/hostgen/testdata/list_client_expected.go
vendored
Normal file
@ -0,0 +1,55 @@
|
|||||||
|
// Code generated by hostgen. DO NOT EDIT.
|
||||||
|
//
|
||||||
|
// This file contains client wrappers for the List host service.
|
||||||
|
// It is intended for use in Navidrome plugins built with TinyGo.
|
||||||
|
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
|
||||||
|
"github.com/extism/go-pdk"
|
||||||
|
)
|
||||||
|
|
||||||
|
// list_items is the host function provided by Navidrome.
|
||||||
|
//
|
||||||
|
//go:wasmimport extism:host/user list_items
|
||||||
|
func list_items(uint64, uint64) uint64
|
||||||
|
|
||||||
|
// ListItemsResponse is the response type for List.Items.
|
||||||
|
type ListItemsResponse struct {
|
||||||
|
Count int32 `json:"count,omitempty"`
|
||||||
|
Error string `json:"error,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// ListItems calls the list_items host function.
|
||||||
|
func ListItems(name string, filter Filter) (*ListItemsResponse, error) {
|
||||||
|
nameMem := pdk.AllocateString(name)
|
||||||
|
defer nameMem.Free()
|
||||||
|
filterBytes, err := json.Marshal(filter)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
filterMem := pdk.AllocateBytes(filterBytes)
|
||||||
|
defer filterMem.Free()
|
||||||
|
|
||||||
|
// Call the host function
|
||||||
|
responsePtr := list_items(nameMem.Offset(), filterMem.Offset())
|
||||||
|
|
||||||
|
// Read the response from memory
|
||||||
|
responseMem := pdk.FindMemory(responsePtr)
|
||||||
|
responseBytes := responseMem.ReadBytes()
|
||||||
|
|
||||||
|
// Parse the response
|
||||||
|
var response ListItemsResponse
|
||||||
|
if err := json.Unmarshal(responseBytes, &response); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if response.Error != "" {
|
||||||
|
return nil, errors.New(response.Error)
|
||||||
|
}
|
||||||
|
|
||||||
|
return &response, nil
|
||||||
|
}
|
||||||
47
plugins/cmd/hostgen/testdata/math_client_expected.go
vendored
Normal file
47
plugins/cmd/hostgen/testdata/math_client_expected.go
vendored
Normal file
@ -0,0 +1,47 @@
|
|||||||
|
// Code generated by hostgen. DO NOT EDIT.
|
||||||
|
//
|
||||||
|
// This file contains client wrappers for the Math host service.
|
||||||
|
// It is intended for use in Navidrome plugins built with TinyGo.
|
||||||
|
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
|
||||||
|
"github.com/extism/go-pdk"
|
||||||
|
)
|
||||||
|
|
||||||
|
// math_add is the host function provided by Navidrome.
|
||||||
|
//
|
||||||
|
//go:wasmimport extism:host/user math_add
|
||||||
|
func math_add(int32, int32) uint64
|
||||||
|
|
||||||
|
// MathAddResponse is the response type for Math.Add.
|
||||||
|
type MathAddResponse struct {
|
||||||
|
Result int32 `json:"result,omitempty"`
|
||||||
|
Error string `json:"error,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// MathAdd calls the math_add host function.
|
||||||
|
func MathAdd(a int32, b int32) (*MathAddResponse, error) {
|
||||||
|
|
||||||
|
// Call the host function
|
||||||
|
responsePtr := math_add(a, b)
|
||||||
|
|
||||||
|
// Read the response from memory
|
||||||
|
responseMem := pdk.FindMemory(responsePtr)
|
||||||
|
responseBytes := responseMem.ReadBytes()
|
||||||
|
|
||||||
|
// Parse the response
|
||||||
|
var response MathAddResponse
|
||||||
|
if err := json.Unmarshal(responseBytes, &response); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if response.Error != "" {
|
||||||
|
return nil, errors.New(response.Error)
|
||||||
|
}
|
||||||
|
|
||||||
|
return &response, nil
|
||||||
|
}
|
||||||
77
plugins/cmd/hostgen/testdata/meta_client_expected.go
vendored
Normal file
77
plugins/cmd/hostgen/testdata/meta_client_expected.go
vendored
Normal file
@ -0,0 +1,77 @@
|
|||||||
|
// Code generated by hostgen. DO NOT EDIT.
|
||||||
|
//
|
||||||
|
// This file contains client wrappers for the Meta host service.
|
||||||
|
// It is intended for use in Navidrome plugins built with TinyGo.
|
||||||
|
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
|
||||||
|
"github.com/extism/go-pdk"
|
||||||
|
)
|
||||||
|
|
||||||
|
// meta_get is the host function provided by Navidrome.
|
||||||
|
//
|
||||||
|
//go:wasmimport extism:host/user meta_get
|
||||||
|
func meta_get(uint64) uint64
|
||||||
|
|
||||||
|
// meta_set is the host function provided by Navidrome.
|
||||||
|
//
|
||||||
|
//go:wasmimport extism:host/user meta_set
|
||||||
|
func meta_set(uint64) uint64
|
||||||
|
|
||||||
|
// MetaGetResponse is the response type for Meta.Get.
|
||||||
|
type MetaGetResponse struct {
|
||||||
|
Value any `json:"value,omitempty"`
|
||||||
|
Error string `json:"error,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// MetaGet calls the meta_get host function.
|
||||||
|
func MetaGet(key string) (*MetaGetResponse, error) {
|
||||||
|
keyMem := pdk.AllocateString(key)
|
||||||
|
defer keyMem.Free()
|
||||||
|
|
||||||
|
// Call the host function
|
||||||
|
responsePtr := meta_get(keyMem.Offset())
|
||||||
|
|
||||||
|
// Read the response from memory
|
||||||
|
responseMem := pdk.FindMemory(responsePtr)
|
||||||
|
responseBytes := responseMem.ReadBytes()
|
||||||
|
|
||||||
|
// Parse the response
|
||||||
|
var response MetaGetResponse
|
||||||
|
if err := json.Unmarshal(responseBytes, &response); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if response.Error != "" {
|
||||||
|
return nil, errors.New(response.Error)
|
||||||
|
}
|
||||||
|
|
||||||
|
return &response, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// MetaSet calls the meta_set host function.
|
||||||
|
func MetaSet(data map[string]any) error {
|
||||||
|
dataBytes, err := json.Marshal(data)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
dataMem := pdk.AllocateBytes(dataBytes)
|
||||||
|
defer dataMem.Free()
|
||||||
|
|
||||||
|
// Call the host function
|
||||||
|
responsePtr := meta_set(dataMem.Offset())
|
||||||
|
|
||||||
|
// Read the response from memory
|
||||||
|
responseMem := pdk.FindMemory(responsePtr)
|
||||||
|
errStr := string(responseMem.ReadBytes())
|
||||||
|
|
||||||
|
if errStr != "" {
|
||||||
|
return errors.New(errStr)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
34
plugins/cmd/hostgen/testdata/ping_client_expected.go
vendored
Normal file
34
plugins/cmd/hostgen/testdata/ping_client_expected.go
vendored
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
// Code generated by hostgen. DO NOT EDIT.
|
||||||
|
//
|
||||||
|
// This file contains client wrappers for the Ping host service.
|
||||||
|
// It is intended for use in Navidrome plugins built with TinyGo.
|
||||||
|
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
|
||||||
|
"github.com/extism/go-pdk"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ping_ping is the host function provided by Navidrome.
|
||||||
|
//
|
||||||
|
//go:wasmimport extism:host/user ping_ping
|
||||||
|
func ping_ping() uint64
|
||||||
|
|
||||||
|
// PingPing calls the ping_ping host function.
|
||||||
|
func PingPing() error {
|
||||||
|
|
||||||
|
// Call the host function
|
||||||
|
responsePtr := ping_ping()
|
||||||
|
|
||||||
|
// Read the response from memory
|
||||||
|
responseMem := pdk.FindMemory(responsePtr)
|
||||||
|
errStr := string(responseMem.ReadBytes())
|
||||||
|
|
||||||
|
if errStr != "" {
|
||||||
|
return errors.New(errStr)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
50
plugins/cmd/hostgen/testdata/search_client_expected.go
vendored
Normal file
50
plugins/cmd/hostgen/testdata/search_client_expected.go
vendored
Normal file
@ -0,0 +1,50 @@
|
|||||||
|
// Code generated by hostgen. DO NOT EDIT.
|
||||||
|
//
|
||||||
|
// This file contains client wrappers for the Search host service.
|
||||||
|
// It is intended for use in Navidrome plugins built with TinyGo.
|
||||||
|
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
|
||||||
|
"github.com/extism/go-pdk"
|
||||||
|
)
|
||||||
|
|
||||||
|
// search_find is the host function provided by Navidrome.
|
||||||
|
//
|
||||||
|
//go:wasmimport extism:host/user search_find
|
||||||
|
func search_find(uint64) uint64
|
||||||
|
|
||||||
|
// SearchFindResponse is the response type for Search.Find.
|
||||||
|
type SearchFindResponse struct {
|
||||||
|
Results []Result `json:"results,omitempty"`
|
||||||
|
Total int32 `json:"total,omitempty"`
|
||||||
|
Error string `json:"error,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// SearchFind calls the search_find host function.
|
||||||
|
func SearchFind(query string) (*SearchFindResponse, error) {
|
||||||
|
queryMem := pdk.AllocateString(query)
|
||||||
|
defer queryMem.Free()
|
||||||
|
|
||||||
|
// Call the host function
|
||||||
|
responsePtr := search_find(queryMem.Offset())
|
||||||
|
|
||||||
|
// Read the response from memory
|
||||||
|
responseMem := pdk.FindMemory(responsePtr)
|
||||||
|
responseBytes := responseMem.ReadBytes()
|
||||||
|
|
||||||
|
// Parse the response
|
||||||
|
var response SearchFindResponse
|
||||||
|
if err := json.Unmarshal(responseBytes, &response); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if response.Error != "" {
|
||||||
|
return nil, errors.New(response.Error)
|
||||||
|
}
|
||||||
|
|
||||||
|
return &response, nil
|
||||||
|
}
|
||||||
53
plugins/cmd/hostgen/testdata/store_client_expected.go
vendored
Normal file
53
plugins/cmd/hostgen/testdata/store_client_expected.go
vendored
Normal file
@ -0,0 +1,53 @@
|
|||||||
|
// Code generated by hostgen. DO NOT EDIT.
|
||||||
|
//
|
||||||
|
// This file contains client wrappers for the Store host service.
|
||||||
|
// It is intended for use in Navidrome plugins built with TinyGo.
|
||||||
|
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
|
||||||
|
"github.com/extism/go-pdk"
|
||||||
|
)
|
||||||
|
|
||||||
|
// store_save is the host function provided by Navidrome.
|
||||||
|
//
|
||||||
|
//go:wasmimport extism:host/user store_save
|
||||||
|
func store_save(uint64) uint64
|
||||||
|
|
||||||
|
// StoreSaveResponse is the response type for Store.Save.
|
||||||
|
type StoreSaveResponse struct {
|
||||||
|
Id string `json:"id,omitempty"`
|
||||||
|
Error string `json:"error,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// StoreSave calls the store_save host function.
|
||||||
|
func StoreSave(item Item) (*StoreSaveResponse, error) {
|
||||||
|
itemBytes, err := json.Marshal(item)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
itemMem := pdk.AllocateBytes(itemBytes)
|
||||||
|
defer itemMem.Free()
|
||||||
|
|
||||||
|
// Call the host function
|
||||||
|
responsePtr := store_save(itemMem.Offset())
|
||||||
|
|
||||||
|
// Read the response from memory
|
||||||
|
responseMem := pdk.FindMemory(responsePtr)
|
||||||
|
responseBytes := responseMem.ReadBytes()
|
||||||
|
|
||||||
|
// Parse the response
|
||||||
|
var response StoreSaveResponse
|
||||||
|
if err := json.Unmarshal(responseBytes, &response); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if response.Error != "" {
|
||||||
|
return nil, errors.New(response.Error)
|
||||||
|
}
|
||||||
|
|
||||||
|
return &response, nil
|
||||||
|
}
|
||||||
59
plugins/cmd/hostgen/testdata/users_client_expected.go
vendored
Normal file
59
plugins/cmd/hostgen/testdata/users_client_expected.go
vendored
Normal file
@ -0,0 +1,59 @@
|
|||||||
|
// Code generated by hostgen. DO NOT EDIT.
|
||||||
|
//
|
||||||
|
// This file contains client wrappers for the Users host service.
|
||||||
|
// It is intended for use in Navidrome plugins built with TinyGo.
|
||||||
|
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
|
||||||
|
"github.com/extism/go-pdk"
|
||||||
|
)
|
||||||
|
|
||||||
|
// users_get is the host function provided by Navidrome.
|
||||||
|
//
|
||||||
|
//go:wasmimport extism:host/user users_get
|
||||||
|
func users_get(uint64, uint64) uint64
|
||||||
|
|
||||||
|
// UsersGetResponse is the response type for Users.Get.
|
||||||
|
type UsersGetResponse struct {
|
||||||
|
Result *User `json:"result,omitempty"`
|
||||||
|
Error string `json:"error,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// UsersGet calls the users_get host function.
|
||||||
|
func UsersGet(id *string, filter *User) (*UsersGetResponse, error) {
|
||||||
|
idBytes, err := json.Marshal(id)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
idMem := pdk.AllocateBytes(idBytes)
|
||||||
|
defer idMem.Free()
|
||||||
|
filterBytes, err := json.Marshal(filter)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
filterMem := pdk.AllocateBytes(filterBytes)
|
||||||
|
defer filterMem.Free()
|
||||||
|
|
||||||
|
// Call the host function
|
||||||
|
responsePtr := users_get(idMem.Offset(), filterMem.Offset())
|
||||||
|
|
||||||
|
// Read the response from memory
|
||||||
|
responseMem := pdk.FindMemory(responsePtr)
|
||||||
|
responseBytes := responseMem.ReadBytes()
|
||||||
|
|
||||||
|
// Parse the response
|
||||||
|
var response UsersGetResponse
|
||||||
|
if err := json.Unmarshal(responseBytes, &response); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if response.Error != "" {
|
||||||
|
return nil, errors.New(response.Error)
|
||||||
|
}
|
||||||
|
|
||||||
|
return &response, nil
|
||||||
|
}
|
||||||
53
plugins/host/go/nd_host_subsonicapi.go
Normal file
53
plugins/host/go/nd_host_subsonicapi.go
Normal file
@ -0,0 +1,53 @@
|
|||||||
|
// 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.
|
||||||
|
|
||||||
|
package main
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
// 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) {
|
||||||
|
uriMem := pdk.AllocateString(uri)
|
||||||
|
defer uriMem.Free()
|
||||||
|
|
||||||
|
// Call the host function
|
||||||
|
responsePtr := subsonicapi_call(uriMem.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
|
||||||
|
}
|
||||||
|
|
||||||
|
if response.Error != "" {
|
||||||
|
return nil, errors.New(response.Error)
|
||||||
|
}
|
||||||
|
|
||||||
|
return &response, nil
|
||||||
|
}
|
||||||
2
plugins/testdata/Makefile
vendored
2
plugins/testdata/Makefile
vendored
@ -11,7 +11,7 @@ all: $(PLUGINS:%=%.wasm)
|
|||||||
clean:
|
clean:
|
||||||
rm -f $(PLUGINS:%=%.wasm)
|
rm -f $(PLUGINS:%=%.wasm)
|
||||||
|
|
||||||
%.wasm: %/main.go %/go.mod
|
%.wasm: %/*.go %/go.mod
|
||||||
ifdef TINYGO
|
ifdef TINYGO
|
||||||
cd $* && tinygo build -target wasip1 -buildmode=c-shared -o ../$@ .
|
cd $* && tinygo build -target wasip1 -buildmode=c-shared -o ../$@ .
|
||||||
else
|
else
|
||||||
|
|||||||
@ -1,3 +1,8 @@
|
|||||||
|
// 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.
|
||||||
|
|
||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
@ -7,26 +12,28 @@ import (
|
|||||||
"github.com/extism/go-pdk"
|
"github.com/extism/go-pdk"
|
||||||
)
|
)
|
||||||
|
|
||||||
// subsonicapiCall is the host function provided by Navidrome to call the Subsonic API.
|
// subsonicapi_call is the host function provided by Navidrome.
|
||||||
// It takes a URI string and returns a JSON response.
|
|
||||||
//
|
//
|
||||||
//go:wasmimport extism:host/user subsonicapi_call
|
//go:wasmimport extism:host/user subsonicapi_call
|
||||||
func subsonicapiCall(uri uint64) uint64
|
func subsonicapi_call(uint64) uint64
|
||||||
|
|
||||||
// SubsonicAPICallResponse matches the host response format.
|
// SubsonicAPICallResponse is the response type for SubsonicAPI.Call.
|
||||||
type SubsonicAPICallResponse struct {
|
type SubsonicAPICallResponse struct {
|
||||||
ResponseJSON string `json:"responseJSON,omitempty"`
|
ResponseJSON string `json:"responseJSON,omitempty"`
|
||||||
Error string `json:"error,omitempty"`
|
Error string `json:"error,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// SubsonicAPICall is a wrapper around the host subsonicapi_call function.
|
// 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) {
|
func SubsonicAPICall(uri string) (*SubsonicAPICallResponse, error) {
|
||||||
// Allocate memory for the URI string
|
uriMem := pdk.AllocateString(uri)
|
||||||
mem := pdk.AllocateString(uri)
|
defer uriMem.Free()
|
||||||
defer mem.Free()
|
|
||||||
|
|
||||||
// Call the host function
|
// Call the host function
|
||||||
responsePtr := subsonicapiCall(mem.Offset())
|
responsePtr := subsonicapi_call(uriMem.Offset())
|
||||||
|
|
||||||
// Read the response from memory
|
// Read the response from memory
|
||||||
responseMem := pdk.FindMemory(responsePtr)
|
responseMem := pdk.FindMemory(responsePtr)
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user