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/times v1.6.0
|
||||
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/fatih/structs v1.1.0
|
||||
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/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/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/go.mod h1:IT+Xdg5AZM9hVtpFUA+uZCJMge/hbvshl8bwzLtFyKA=
|
||||
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
|
||||
|
||||
```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
|
||||
|
||||
| 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` |
|
||||
| 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` |
|
||||
|
||||
By default, both host and plugin code are generated. Use `-host-only` or `-plugin-only` to generate only one type.
|
||||
|
||||
### Example
|
||||
|
||||
@ -162,7 +166,9 @@ type ServiceSearchResponse struct {
|
||||
|
||||
## 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
|
||||
- Required imports (`context`, `encoding/json`, `extism`)
|
||||
@ -171,6 +177,25 @@ Generated files are named `<servicename>_gen.go` (lowercase). Each file includes
|
||||
- Host function wrappers
|
||||
- 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
|
||||
|
||||
### Annotations Not Detected
|
||||
|
||||
@ -150,69 +150,146 @@ type ServiceB interface {
|
||||
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 output",
|
||||
func(serviceFile, expectedFile string) {
|
||||
DescribeTable("generates correct host and client output",
|
||||
func(serviceFile, hostExpectedFile, clientExpectedFile string) {
|
||||
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())
|
||||
|
||||
// Generate both host and client code in one run
|
||||
cmd := exec.Command(hostgenBin, "-input", testDir, "-output", outputDir, "-package", "testpkg")
|
||||
output, err := cmd.CombinedOutput()
|
||||
Expect(err).ToNot(HaveOccurred(), "Command failed: %s", output)
|
||||
|
||||
// Verify host code
|
||||
entries, err := os.ReadDir(outputDir)
|
||||
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())
|
||||
|
||||
// Format both for comparison
|
||||
formattedActual, err := format.Source(actual)
|
||||
Expect(err).ToNot(HaveOccurred(), "Generated code is not valid Go:\n%s", actual)
|
||||
formattedHostActual, err := format.Source(hostActual)
|
||||
Expect(err).ToNot(HaveOccurred(), "Generated host code is not valid Go:\n%s", hostActual)
|
||||
|
||||
formattedExpected, err := format.Source([]byte(expectedCode))
|
||||
Expect(err).ToNot(HaveOccurred(), "Expected code is not valid Go")
|
||||
formattedHostExpected, err := format.Source([]byte(hostExpected))
|
||||
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",
|
||||
"echo_service.go", "echo_expected.go"),
|
||||
Entry("simple string params",
|
||||
"echo_service.go", "echo_expected.go", "echo_client_expected.go"),
|
||||
|
||||
Entry("multiple simple params",
|
||||
"math_service.go", "math_expected.go"),
|
||||
Entry("multiple simple params (int32)",
|
||||
"math_service.go", "math_expected.go", "math_client_expected.go"),
|
||||
|
||||
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",
|
||||
"list_service.go", "list_expected.go"),
|
||||
"list_service.go", "list_expected.go", "list_client_expected.go"),
|
||||
|
||||
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",
|
||||
"ping_service.go", "ping_expected.go"),
|
||||
"ping_service.go", "ping_expected.go", "ping_client_expected.go"),
|
||||
|
||||
Entry("map and interface types",
|
||||
"meta_service.go", "meta_expected.go"),
|
||||
"meta_service.go", "meta_expected.go", "meta_client_expected.go"),
|
||||
|
||||
Entry("pointer types",
|
||||
"users_service.go", "users_expected.go"),
|
||||
"users_service.go", "users_expected.go", "users_client_expected.go"),
|
||||
|
||||
Entry("multiple returns",
|
||||
"search_service.go", "search_expected.go"),
|
||||
"search_service.go", "search_expected.go", "search_client_expected.go"),
|
||||
|
||||
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")
|
||||
|
||||
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"
|
||||
Expect(os.WriteFile(filepath.Join(testDir, "go.mod"), []byte(goMod), 0600)).To(Succeed())
|
||||
|
||||
// Generate
|
||||
cmd := exec.Command(hostgenBin, "-input", testDir, "-output", testDir, "-package", "testpkg")
|
||||
// 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)
|
||||
|
||||
@ -238,6 +315,89 @@ type ServiceB interface {
|
||||
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(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 (
|
||||
"bytes"
|
||||
"embed"
|
||||
"fmt"
|
||||
"strings"
|
||||
"text/template"
|
||||
)
|
||||
|
||||
// GenerateService generates the host function wrapper code for a service.
|
||||
func GenerateService(svc Service, pkgName string) ([]byte, error) {
|
||||
tmpl, err := template.New("service").Funcs(template.FuncMap{
|
||||
//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()) },
|
||||
@ -27,7 +31,39 @@ func GenerateService(svc Service, pkgName string) ([]byte, error) {
|
||||
"readParam": generateReadParam,
|
||||
"writeReturn": generateWriteReturn,
|
||||
"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 {
|
||||
return nil, fmt.Errorf("parsing template: %w", err)
|
||||
}
|
||||
@ -48,10 +84,43 @@ func GenerateService(svc Service, pkgName string) ([]byte, error) {
|
||||
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 {
|
||||
Package string
|
||||
Service Service
|
||||
NeedsJSON bool
|
||||
NeedsErrors bool // Client: needs "errors" import
|
||||
NeedsWriteHelper bool
|
||||
NeedsErrorHelper bool
|
||||
}
|
||||
@ -87,6 +156,35 @@ func serviceNeedsWriteHelper(svc Service) bool {
|
||||
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.
|
||||
func serviceNeedsErrorHelper(svc Service) bool {
|
||||
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}}
|
||||
|
||||
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}}
|
||||
// wasmParamType returns the WASM parameter type for a Go parameter.
|
||||
func wasmParamType(p Param) string {
|
||||
if IsSimpleType(p.Type) {
|
||||
return p.Type
|
||||
}
|
||||
// All pointer types (string, []byte, complex) use uint64 offset
|
||||
return "uint64"
|
||||
}
|
||||
{{- 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"` + "`" + `
|
||||
// wasmReturnType returns the WASM return type declaration for a method.
|
||||
func wasmReturnType(m Method) string {
|
||||
// Methods with JSON responses or error-only return uint64 (pointer)
|
||||
if m.NeedsResponseType() || m.IsErrorOnly() {
|
||||
return "uint64"
|
||||
}
|
||||
// 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.
|
||||
// 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}}
|
||||
// wrapperReturnType returns the Go return type for the wrapper function.
|
||||
func wrapperReturnType(m Method, svcName string) string {
|
||||
if m.NeedsResponseType() {
|
||||
return fmt.Sprintf("(*%s%sResponse, error)", svcName, m.Name)
|
||||
}
|
||||
if m.IsErrorOnly() {
|
||||
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 {
|
||||
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
|
||||
// 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 ""
|
||||
}
|
||||
respPtr, err := p.WriteBytes(respBytes)
|
||||
if err != nil {
|
||||
stack[0] = 0
|
||||
return
|
||||
lines := strings.Split(strings.TrimSpace(doc), "\n")
|
||||
var result []string
|
||||
for _, line := range lines {
|
||||
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:
|
||||
//
|
||||
// -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)
|
||||
// -v Verbose output
|
||||
// -dry-run Preview generated code without writing files
|
||||
// -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)
|
||||
// -v Verbose output
|
||||
// -dry-run Preview generated code without writing files
|
||||
package main
|
||||
|
||||
import (
|
||||
@ -19,20 +21,29 @@ import (
|
||||
"go/format"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/navidrome/navidrome/plugins/cmd/hostgen/internal"
|
||||
)
|
||||
|
||||
func main() {
|
||||
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)")
|
||||
verbose = flag.Bool("v", false, "Verbose output")
|
||||
dryRun = flag.Bool("dry-run", false, "Preview generated code without writing files")
|
||||
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")
|
||||
verbose = flag.Bool("v", false, "Verbose output")
|
||||
dryRun = flag.Bool("dry-run", false, "Preview generated code without writing files")
|
||||
)
|
||||
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 == "" {
|
||||
*outputDir = *inputDir
|
||||
}
|
||||
@ -54,10 +65,16 @@ func main() {
|
||||
*pkgName = filepath.Base(absOutput)
|
||||
}
|
||||
|
||||
// Determine what to generate
|
||||
generateHost := !*pluginOnly
|
||||
generateClient := !*hostOnly
|
||||
|
||||
if *verbose {
|
||||
fmt.Printf("Input directory: %s\n", absInput)
|
||||
fmt.Printf("Output directory: %s\n", absOutput)
|
||||
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
|
||||
@ -83,34 +100,85 @@ func main() {
|
||||
|
||||
// Generate code for each service
|
||||
for _, svc := range services {
|
||||
code, err := internal.GenerateService(svc, *pkgName)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Error generating code for %s: %v\n", svc.Name, err)
|
||||
os.Exit(1)
|
||||
// Generate host-side code
|
||||
if generateHost {
|
||||
if err := generateHostCode(svc, *pkgName, absOutput, *dryRun, *verbose); err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Error generating host code for %s: %v\n", svc.Name, err)
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
// Format the generated code
|
||||
formatted, err := format.Source(code)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Error formatting generated code for %s: %v\n", svc.Name, err)
|
||||
fmt.Fprintf(os.Stderr, "Raw code:\n%s\n", code)
|
||||
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)
|
||||
// Generate client-side code
|
||||
if generateClient {
|
||||
if err := generateClientCode(svc, absOutput, *dryRun, *verbose); err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Error generating client code for %s: %v\n", svc.Name, err)
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 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:
|
||||
rm -f $(PLUGINS:%=%.wasm)
|
||||
|
||||
%.wasm: %/main.go %/go.mod
|
||||
%.wasm: %/*.go %/go.mod
|
||||
ifdef TINYGO
|
||||
cd $* && tinygo build -target wasip1 -buildmode=c-shared -o ../$@ .
|
||||
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
|
||||
|
||||
import (
|
||||
@ -7,26 +12,28 @@ import (
|
||||
"github.com/extism/go-pdk"
|
||||
)
|
||||
|
||||
// subsonicapiCall is the host function provided by Navidrome to call the Subsonic API.
|
||||
// It takes a URI string and returns a JSON response.
|
||||
// subsonicapi_call is the host function provided by Navidrome.
|
||||
//
|
||||
//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 {
|
||||
ResponseJSON string `json:"responseJSON,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) {
|
||||
// Allocate memory for the URI string
|
||||
mem := pdk.AllocateString(uri)
|
||||
defer mem.Free()
|
||||
uriMem := pdk.AllocateString(uri)
|
||||
defer uriMem.Free()
|
||||
|
||||
// Call the host function
|
||||
responsePtr := subsonicapiCall(mem.Offset())
|
||||
responsePtr := subsonicapi_call(uriMem.Offset())
|
||||
|
||||
// Read the response from memory
|
||||
responseMem := pdk.FindMemory(responsePtr)
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user