diff --git a/plugins/examples/webhook-rs/.gitignore b/plugins/.gitignore
similarity index 81%
rename from plugins/examples/webhook-rs/.gitignore
rename to plugins/.gitignore
index 8f0c20473..5026985e6 100644
--- a/plugins/examples/webhook-rs/.gitignore
+++ b/plugins/.gitignore
@@ -1,5 +1,4 @@
# Rust build artifacts
-/target/
-
# Cargo.lock is not needed for library crates (this is a cdylib)
-Cargo.lock
\ No newline at end of file
+Cargo.lock
+target
\ No newline at end of file
diff --git a/plugins/README.md b/plugins/README.md
index 6c48e34d4..593b92816 100644
--- a/plugins/README.md
+++ b/plugins/README.md
@@ -758,16 +758,17 @@ Generated SDKs for calling host services are in `plugins/host/go/` and `plugins/
See [examples/](examples/) for complete working plugins:
-| Plugin | Language | Capabilities | Host Services | Description |
-|----------------------------------------------------------|----------|-------------------------|---------------------------------------------|--------------------------------|
-| [minimal](examples/minimal/) | Go | MetadataAgent | – | Basic structure example |
-| [wikimedia](examples/wikimedia/) | Go | MetadataAgent | HTTP | Wikidata/Wikipedia integration |
-| [coverartarchive-py](examples/coverartarchive-py/) | Python | MetadataAgent | HTTP | Cover Art Archive |
-| [webhook-rs](examples/webhook-rs/) | Rust | Scrobbler | HTTP | HTTP webhooks |
-| [nowplaying-py](examples/nowplaying-py/) | Python | Lifecycle | Scheduler, SubsonicAPI | Periodic now-playing logger |
-| [library-inspector](examples/library-inspector/) | Rust | Lifecycle | Library, Scheduler | Periodic library stats logging |
-| [crypto-ticker](examples/crypto-ticker/) | Go | Lifecycle | WebSocket, Scheduler | Real-time crypto prices demo |
-| [discord-rich-presence](examples/discord-rich-presence/) | Go | Scrobbler | HTTP, WebSocket, Cache, Scheduler, Artwork | Discord integration |
+| Plugin | Language | Capabilities | Host Services | Description |
+|----------------------------------------------------------------|----------|---------------|--------------------------------------------|--------------------------------|
+| [minimal](examples/minimal/) | Go | MetadataAgent | – | Basic structure example |
+| [wikimedia](examples/wikimedia/) | Go | MetadataAgent | HTTP | Wikidata/Wikipedia integration |
+| [coverartarchive-py](examples/coverartarchive-py/) | Python | MetadataAgent | HTTP | Cover Art Archive |
+| [webhook-rs](examples/webhook-rs/) | Rust | Scrobbler | HTTP | HTTP webhooks |
+| [nowplaying-py](examples/nowplaying-py/) | Python | Lifecycle | Scheduler, SubsonicAPI | Periodic now-playing logger |
+| [library-inspector](examples/library-inspector/) | Rust | Lifecycle | Library, Scheduler | Periodic library stats logging |
+| [crypto-ticker](examples/crypto-ticker/) | Go | Lifecycle | WebSocket, Scheduler | Real-time crypto prices demo |
+| [discord-rich-presence](examples/discord-rich-presence/) | Go | Scrobbler | HTTP, WebSocket, Cache, Scheduler, Artwork | Discord integration |
+| [discord-rich-presence-rs](examples/discord-rich-presence-rs/) | Rust | Scrobbler | HTTP, WebSocket, Cache, Scheduler, Artwork | Discord integration (Rust) |
---
diff --git a/plugins/cmd/hostgen/README.md b/plugins/cmd/hostgen/README.md
index 1bbf748f5..d22cba3d9 100644
--- a/plugins/cmd/hostgen/README.md
+++ b/plugins/cmd/hostgen/README.md
@@ -21,10 +21,11 @@ hostgen -input
-output -package [-v] [-dry-run] [-host-only]
| `-plugin-only` | Generate only plugin/client-side wrapper code | `false` |
| `-go` | Generate Go client wrappers | `true`* |
| `-python` | Generate Python client wrappers | `false` |
+| `-rust` | Generate Rust client wrappers | `false` |
-\* `-go` is enabled by default when `-python` is not specified. Use both `-go -python` to generate both.
+\* `-go` is enabled by default when neither `-python` nor `-rust` is specified. Use combinations like `-go -python -rust` to generate multiple languages.
-By default, both host and Go plugin code are generated. Use `-host-only` or `-plugin-only` to generate only one type. Use `-python` to generate Python wrappers.
+By default, both host and Go plugin code are generated. Use `-host-only` or `-plugin-only` to generate only one type. Use `-python` to generate Python wrappers and `-rust` to generate Rust wrappers.
### Example
@@ -198,8 +199,10 @@ output/
├── subsonicapi_gen.go # Host-side code (for Navidrome)
├── go/
│ └── nd_host_subsonicapi.go # Plugin-side code (for TinyGo plugins)
-└── python/
- └── nd_host_subsonicapi.py # Plugin-side code (for Python plugins)
+├── python/
+│ └── nd_host_subsonicapi.py # Plugin-side code (for Python plugins)
+└── rust/
+ └── nd_host_subsonicapi.rs # Plugin-side code (for Rust plugins)
```
### Python Client Code (extism-py WASM)
@@ -280,6 +283,75 @@ def my_plugin_function():
extism.log(extism.LogLevel.Error, f"API error: {e}")
```
+### Rust Client Code (extism-pdk WASM)
+
+Generated files are named `nd_host_.rs` (lowercase) and placed in the `rust/` subdirectory of the output directory. These files are intended for use in Navidrome plugins built with `extism-pdk`. Each file includes:
+
+- `// Code generated by hostgen. DO NOT EDIT.` header
+- Required imports (`extism_pdk::*`, `serde`)
+- Request/response struct types with `#[derive(Serialize, Deserialize)]`
+- `#[host_fn]` extern blocks for raw host function imports
+- Public wrapper functions with `Result` returns and snake_case names
+
+#### Rust Type Mapping
+
+| Go Type | Rust Type | Notes |
+|-------------------------------|-------------------------------|------------------------------|
+| `string` | `String` / `&str` | `&str` for params |
+| `int`, `int32` | `i32` | |
+| `int64` | `i64` | |
+| `float32` | `f32` | |
+| `float64` | `f64` | |
+| `bool` | `bool` | |
+| `[]byte` | `Vec` | |
+| `[]T` | `Vec` | |
+| `map[K]V` | `HashMap` | From `std::collections` |
+| `*T` | `Option` | |
+| `interface{}` / `any` | `serde_json::Value` | |
+
+#### Rust Function Naming
+
+Functions follow Rust snake_case convention:
+
+| Go Method | Rust Function |
+|-------------------------------|----------------------------------|
+| `SubsonicAPI.Call` | `subsonicapi_call()` |
+| `Scheduler.ScheduleRecurring` | `scheduler_schedule_recurring()` |
+| `Cache.GetString` | `cache_get_string()` |
+
+#### Rust Plugin Usage
+
+The generated Rust wrappers form a library crate (`nd-host`) that plugins can depend on. Add the dependency to your plugin's `Cargo.toml`:
+
+```toml
+[dependencies]
+extism-pdk = "1.2"
+nd-host = { path = "../../host/rust" }
+```
+
+Then import and use the services:
+
+```rust
+use nd_host::{cache, scheduler, artwork};
+
+#[plugin_fn]
+pub fn my_callback(input: String) -> FnResult {
+ // Use cache service
+ cache::cache_set_string("key", "value", 3600)?;
+ let value = cache::cache_get_string("key")?;
+
+ // Schedule a task
+ scheduler::scheduler_schedule_one_time(60, "payload", "task-id")?;
+
+ // Get artwork URL
+ let url = artwork::artwork_get_track_url("track-id", 300)?;
+
+ Ok("done")
+}
+```
+
+See [discord-rich-presence-rs](../examples/discord-rich-presence-rs/) for a complete example using all Rust host wrappers.
+
## Troubleshooting
### Annotations Not Detected
diff --git a/plugins/cmd/hostgen/integration_test.go b/plugins/cmd/hostgen/integration_test.go
index 52bb80dff..733c94bf5 100644
--- a/plugins/cmd/hostgen/integration_test.go
+++ b/plugins/cmd/hostgen/integration_test.go
@@ -197,16 +197,17 @@ type ServiceB interface {
Describe("code generation", func() {
DescribeTable("generates correct host and client output",
- func(serviceFile, hostExpectedFile, goClientExpectedFile, pyClientExpectedFile string) {
+ func(serviceFile, hostExpectedFile, goClientExpectedFile, pyClientExpectedFile, rsClientExpectedFile string) {
serviceCode := readTestdata(serviceFile)
hostExpected := readTestdata(hostExpectedFile)
goClientExpected := readTestdata(goClientExpectedFile)
pyClientExpected := readTestdata(pyClientExpectedFile)
+ rsClientExpected := readTestdata(rsClientExpectedFile)
Expect(os.WriteFile(filepath.Join(testDir, "service.go"), []byte(serviceCode), 0600)).To(Succeed())
- // Generate host and both Go and Python client code
- cmd := exec.Command(hostgenBin, "-input", testDir, "-output", outputDir, "-package", "testpkg", "-go", "-python")
+ // Generate host and all client code (Go, Python, Rust)
+ cmd := exec.Command(hostgenBin, "-input", testDir, "-output", outputDir, "-package", "testpkg", "-go", "-python", "-rust")
output, err := cmd.CombinedOutput()
Expect(err).ToNot(HaveOccurred(), "Command failed: %s", output)
@@ -216,7 +217,7 @@ type ServiceB interface {
var hostFiles []string
for _, e := range entries {
- if e.Name() != "go" && e.Name() != "python" && !e.IsDir() {
+ if e.Name() != "go" && e.Name() != "python" && e.Name() != "rust" && !e.IsDir() {
hostFiles = append(hostFiles, e.Name())
}
}
@@ -260,37 +261,48 @@ type ServiceB interface {
Expect(err).ToNot(HaveOccurred())
Expect(string(pyClientActual)).To(Equal(pyClientExpected), "Python client code mismatch")
+
+ // Verify Rust client code
+ rustDir := filepath.Join(outputDir, "rust")
+ rsClientEntries, err := os.ReadDir(rustDir)
+ Expect(err).ToNot(HaveOccurred())
+ Expect(rsClientEntries).To(HaveLen(1), "Expected exactly one Rust client file")
+
+ rsClientActual, err := os.ReadFile(filepath.Join(rustDir, rsClientEntries[0].Name()))
+ Expect(err).ToNot(HaveOccurred())
+
+ Expect(string(rsClientActual)).To(Equal(rsClientExpected), "Rust client code mismatch")
},
Entry("simple string params",
- "echo_service.go", "echo_expected.go", "echo_client_expected.go", "echo_client_expected.py"),
+ "echo_service.go", "echo_expected.go", "echo_client_expected.go", "echo_client_expected.py", "echo_client_expected.rs"),
Entry("multiple simple params (int32)",
- "math_service.go", "math_expected.go", "math_client_expected.go", "math_client_expected.py"),
+ "math_service.go", "math_expected.go", "math_client_expected.go", "math_client_expected.py", "math_client_expected.rs"),
Entry("struct param with request type",
- "store_service.go", "store_expected.go", "store_client_expected.go", "store_client_expected.py"),
+ "store_service.go", "store_expected.go", "store_client_expected.go", "store_client_expected.py", "store_client_expected.rs"),
Entry("mixed simple and complex params",
- "list_service.go", "list_expected.go", "list_client_expected.go", "list_client_expected.py"),
+ "list_service.go", "list_expected.go", "list_client_expected.go", "list_client_expected.py", "list_client_expected.rs"),
Entry("method without error",
- "counter_service.go", "counter_expected.go", "counter_client_expected.go", "counter_client_expected.py"),
+ "counter_service.go", "counter_expected.go", "counter_client_expected.go", "counter_client_expected.py", "counter_client_expected.rs"),
Entry("no params, error only",
- "ping_service.go", "ping_expected.go", "ping_client_expected.go", "ping_client_expected.py"),
+ "ping_service.go", "ping_expected.go", "ping_client_expected.go", "ping_client_expected.py", "ping_client_expected.rs"),
Entry("map and interface types",
- "meta_service.go", "meta_expected.go", "meta_client_expected.go", "meta_client_expected.py"),
+ "meta_service.go", "meta_expected.go", "meta_client_expected.go", "meta_client_expected.py", "meta_client_expected.rs"),
Entry("pointer types",
- "users_service.go", "users_expected.go", "users_client_expected.go", "users_client_expected.py"),
+ "users_service.go", "users_expected.go", "users_client_expected.go", "users_client_expected.py", "users_client_expected.rs"),
Entry("multiple returns",
- "search_service.go", "search_expected.go", "search_client_expected.go", "search_client_expected.py"),
+ "search_service.go", "search_expected.go", "search_client_expected.go", "search_client_expected.py", "search_client_expected.rs"),
Entry("bytes",
- "codec_service.go", "codec_expected.go", "codec_client_expected.go", "codec_client_expected.py"),
+ "codec_service.go", "codec_expected.go", "codec_client_expected.go", "codec_client_expected.py", "codec_client_expected.rs"),
)
It("generates compilable host code for comprehensive service", func() {
diff --git a/plugins/cmd/hostgen/internal/generator.go b/plugins/cmd/hostgen/internal/generator.go
index d96614098..df0eb6da7 100644
--- a/plugins/cmd/hostgen/internal/generator.go
+++ b/plugins/cmd/hostgen/internal/generator.go
@@ -154,3 +154,39 @@ func pythonDefaultValue(p Param) string {
return ", None"
}
}
+
+// rustFuncMap returns the template functions for Rust client code generation.
+func rustFuncMap(svc Service) template.FuncMap {
+ return template.FuncMap{
+ "lower": strings.ToLower,
+ "exportName": func(m Method) string { return m.FunctionName(svc.ExportPrefix()) },
+ "requestType": func(m Method) string { return m.RequestTypeName(svc.Name) },
+ "responseType": func(m Method) string { return m.ResponseTypeName(svc.Name) },
+ "rustFunc": func(m Method) string { return m.RustFunctionName(svc.ExportPrefix()) },
+ "rustDocComment": RustDocComment,
+ }
+}
+
+// GenerateClientRust generates Rust client wrapper code for plugins.
+func GenerateClientRust(svc Service) ([]byte, error) {
+ tmplContent, err := templatesFS.ReadFile("templates/client_rs.rs.tmpl")
+ if err != nil {
+ return nil, fmt.Errorf("reading Rust client template: %w", err)
+ }
+
+ tmpl, err := template.New("client_rs").Funcs(rustFuncMap(svc)).Parse(string(tmplContent))
+ if err != nil {
+ return nil, fmt.Errorf("parsing template: %w", err)
+ }
+
+ data := templateData{
+ Service: svc,
+ }
+
+ var buf bytes.Buffer
+ if err := tmpl.Execute(&buf, data); err != nil {
+ return nil, fmt.Errorf("executing template: %w", err)
+ }
+
+ return buf.Bytes(), nil
+}
diff --git a/plugins/cmd/hostgen/internal/generator_test.go b/plugins/cmd/hostgen/internal/generator_test.go
index 5d3c40ae3..d9054898e 100644
--- a/plugins/cmd/hostgen/internal/generator_test.go
+++ b/plugins/cmd/hostgen/internal/generator_test.go
@@ -290,10 +290,15 @@ var _ = Describe("Generator", func() {
})
Describe("toJSONName", func() {
- It("should convert to camelCase", func() {
+ It("should convert to camelCase matching Rust serde behavior", func() {
Expect(toJSONName("InputValue")).To(Equal("inputValue"))
- Expect(toJSONName("URI")).To(Equal("uRI"))
+ Expect(toJSONName("URI")).To(Equal("uri"))
Expect(toJSONName("id")).To(Equal("id"))
+ Expect(toJSONName("ID")).To(Equal("id"))
+ Expect(toJSONName("ConnectionID")).To(Equal("connectionId"))
+ Expect(toJSONName("NewConnectionID")).To(Equal("newConnectionId"))
+ Expect(toJSONName("XMLHTTPRequest")).To(Equal("xmlhttpRequest"))
+ Expect(toJSONName("APIKey")).To(Equal("apiKey"))
})
It("should handle empty string", func() {
@@ -329,9 +334,16 @@ var _ = Describe("Generator", func() {
It("should convert PascalCase to snake_case", func() {
Expect(ToSnakeCase("ScheduleRecurring")).To(Equal("schedule_recurring"))
Expect(ToSnakeCase("GetString")).To(Equal("get_string"))
- Expect(ToSnakeCase("ID")).To(Equal("i_d"))
Expect(ToSnakeCase("simple")).To(Equal("simple"))
})
+
+ It("should handle acronyms correctly", func() {
+ Expect(ToSnakeCase("ID")).To(Equal("id"))
+ Expect(ToSnakeCase("ScheduleID")).To(Equal("schedule_id"))
+ Expect(ToSnakeCase("NewScheduleID")).To(Equal("new_schedule_id"))
+ Expect(ToSnakeCase("XMLParser")).To(Equal("xml_parser"))
+ Expect(ToSnakeCase("GetHTTPResponse")).To(Equal("get_http_response"))
+ })
})
Describe("Method.PythonFunctionName", func() {
diff --git a/plugins/cmd/hostgen/internal/templates/client_rs.rs.tmpl b/plugins/cmd/hostgen/internal/templates/client_rs.rs.tmpl
new file mode 100644
index 000000000..16afd76ac
--- /dev/null
+++ b/plugins/cmd/hostgen/internal/templates/client_rs.rs.tmpl
@@ -0,0 +1,86 @@
+// Code generated by hostgen. DO NOT EDIT.
+//
+// This file contains client wrappers for the {{.Service.Name}} host service.
+// It is intended for use in Navidrome plugins built with extism-pdk.
+
+use extism_pdk::*;
+use serde::{Deserialize, Serialize};
+{{- range .Service.Methods}}
+{{- if .HasParams}}
+
+#[derive(Debug, Clone, Serialize)]
+#[serde(rename_all = "camelCase")]
+struct {{requestType .}} {
+{{- range .Params}}
+ {{.RustName}}: {{.RustType}},
+{{- end}}
+}
+{{- end}}
+
+#[derive(Debug, Clone, Deserialize)]
+#[serde(rename_all = "camelCase")]
+struct {{responseType .}} {
+{{- range .Returns}}
+ #[serde(default)]
+ {{.RustName}}: {{.RustType}},
+{{- end}}
+ #[serde(default)]
+ error: Option,
+}
+{{- end}}
+
+#[host_fn]
+extern "ExtismHost" {
+{{- range .Service.Methods}}
+ fn {{exportName .}}(input: Json<{{if .HasParams}}{{requestType .}}{{else}}serde_json::Value{{end}}>) -> Json<{{responseType .}}>;
+{{- end}}
+}
+
+{{- /* Generate wrapper functions */ -}}
+{{range .Service.Methods}}
+
+{{if .Doc}}{{rustDocComment .Doc}}{{else}}/// Calls the {{exportName .}} host function.{{end}}
+{{- if .HasParams}}
+///
+/// # Arguments
+{{- range .Params}}
+/// * `{{.RustName}}` - {{.RustType}} parameter.
+{{- end}}
+{{- end}}
+{{- if .HasReturns}}
+///
+/// # Returns
+{{- if eq (len .Returns) 1}}
+/// The {{(index .Returns 0).RustName}} value.
+{{- else}}
+/// A tuple of ({{range $i, $r := .Returns}}{{if $i}}, {{end}}{{$r.RustName}}{{end}}).
+{{- end}}
+{{- end}}
+///
+/// # Errors
+/// Returns an error if the host function call fails.
+pub fn {{rustFunc .}}({{range $i, $p := .Params}}{{if $i}}, {{end}}{{$p.RustName}}: {{$p.RustParamType}}{{end}}) -> Result<{{if eq (len .Returns) 0}}(){{else if eq (len .Returns) 1}}{{(index .Returns 0).RustType}}{{else}}({{range $i, $r := .Returns}}{{if $i}}, {{end}}{{$r.RustType}}{{end}}){{end}}, Error> {
+ let response = unsafe {
+{{- if .HasParams}}
+ {{exportName .}}(Json({{requestType .}} {
+{{- range .Params}}
+ {{.RustName}}: {{.RustName}}{{if .NeedsToOwned}}.to_owned(){{end}},
+{{- end}}
+ }))?
+{{- else}}
+ {{exportName .}}(Json(serde_json::json!({})))?
+{{- end}}
+ };
+
+ if let Some(err) = response.0.error {
+ return Err(Error::msg(err));
+ }
+{{if eq (len .Returns) 0}}
+ Ok(())
+{{- else if eq (len .Returns) 1}}
+ Ok(response.0.{{(index .Returns 0).RustName}})
+{{- else}}
+ Ok(({{range $i, $r := .Returns}}{{if $i}}, {{end}}response.0.{{$r.RustName}}{{end}}))
+{{- end}}
+}
+{{- end}}
diff --git a/plugins/cmd/hostgen/internal/types.go b/plugins/cmd/hostgen/internal/types.go
index 9bcc7a190..381f756b8 100644
--- a/plugins/cmd/hostgen/internal/types.go
+++ b/plugins/cmd/hostgen/internal/types.go
@@ -2,6 +2,7 @@ package internal
import (
"strings"
+ "unicode"
)
// Service represents a parsed host service interface.
@@ -78,12 +79,43 @@ func NewParam(name, typ string) Param {
}
// toJSONName converts a Go identifier to camelCase JSON field name.
+// This matches Rust serde's rename_all = "camelCase" behavior.
+// Examples: "ConnectionID" -> "connectionId", "NewConnectionID" -> "newConnectionId"
func toJSONName(name string) string {
if name == "" {
return ""
}
- // Simple conversion: lowercase first letter
- return strings.ToLower(name[:1]) + name[1:]
+
+ runes := []rune(name)
+ result := make([]rune, 0, len(runes))
+
+ for i, r := range runes {
+ if i == 0 {
+ // First character is always lowercase
+ result = append(result, unicode.ToLower(r))
+ } else if unicode.IsUpper(r) {
+ // Check if this is part of an acronym (consecutive uppercase)
+ // or a word boundary
+ prevIsUpper := unicode.IsUpper(runes[i-1])
+ nextIsLower := i+1 < len(runes) && unicode.IsLower(runes[i+1])
+
+ if prevIsUpper && !nextIsLower {
+ // Middle of an acronym - lowercase it
+ result = append(result, unicode.ToLower(r))
+ } else if prevIsUpper && nextIsLower {
+ // End of acronym followed by lowercase - this starts a new word
+ // Keep uppercase
+ result = append(result, r)
+ } else {
+ // Regular word boundary - keep uppercase
+ result = append(result, r)
+ }
+ } else {
+ result = append(result, r)
+ }
+ }
+
+ return string(result)
}
// ToPythonType converts a Go type to its Python equivalent.
@@ -105,11 +137,23 @@ func ToPythonType(goType string) string {
}
// ToSnakeCase converts a PascalCase or camelCase string to snake_case.
+// It handles consecutive uppercase letters correctly (e.g., "ScheduleID" -> "schedule_id").
func ToSnakeCase(s string) string {
var result strings.Builder
- for i, r := range s {
+ runes := []rune(s)
+ for i, r := range runes {
if i > 0 && r >= 'A' && r <= 'Z' {
- result.WriteByte('_')
+ // Add underscore before uppercase, but not if:
+ // - Previous char was uppercase AND next char is uppercase or end of string
+ // (this handles acronyms like "ID" in "NewScheduleID")
+ prevUpper := runes[i-1] >= 'A' && runes[i-1] <= 'Z'
+ nextUpper := i+1 < len(runes) && runes[i+1] >= 'A' && runes[i+1] <= 'Z'
+ atEnd := i+1 == len(runes)
+
+ // Only skip underscore if we're in the middle of an acronym
+ if !prevUpper || (!nextUpper && !atEnd) {
+ result.WriteByte('_')
+ }
}
result.WriteRune(r)
}
@@ -140,3 +184,136 @@ func (p Param) PythonType() string {
func (p Param) PythonName() string {
return ToSnakeCase(p.Name)
}
+
+// ToRustType converts a Go type to its Rust equivalent.
+func ToRustType(goType string) string {
+ // Handle pointer types
+ if strings.HasPrefix(goType, "*") {
+ inner := ToRustType(goType[1:])
+ return "Option<" + inner + ">"
+ }
+ // Handle slice types
+ if strings.HasPrefix(goType, "[]") {
+ if goType == "[]byte" {
+ return "Vec"
+ }
+ inner := ToRustType(goType[2:])
+ return "Vec<" + inner + ">"
+ }
+ // Handle map types
+ if strings.HasPrefix(goType, "map[") {
+ // Extract key and value types from map[K]V
+ rest := goType[4:] // Remove "map["
+ depth := 1
+ keyEnd := 0
+ for i, r := range rest {
+ if r == '[' {
+ depth++
+ } else if r == ']' {
+ depth--
+ if depth == 0 {
+ keyEnd = i
+ break
+ }
+ }
+ }
+ keyType := rest[:keyEnd]
+ valueType := rest[keyEnd+1:]
+ return "std::collections::HashMap<" + ToRustType(keyType) + ", " + ToRustType(valueType) + ">"
+ }
+
+ switch goType {
+ case "string":
+ return "String"
+ case "int", "int32":
+ return "i32"
+ case "int64":
+ return "i64"
+ case "float32":
+ return "f32"
+ case "float64":
+ return "f64"
+ case "bool":
+ return "bool"
+ case "interface{}", "any":
+ return "serde_json::Value"
+ default:
+ // For custom struct types, use Value as they need custom definition
+ return "serde_json::Value"
+ }
+}
+
+// RustParamType returns the Rust type for a function parameter (uses &str for strings).
+func RustParamType(goType string) string {
+ if goType == "string" {
+ return "&str"
+ }
+ return ToRustType(goType)
+}
+
+// RustDefaultValue returns the default value for a Rust type.
+func RustDefaultValue(goType string) string {
+ switch goType {
+ case "string":
+ return `String::new()`
+ case "int", "int32":
+ return "0"
+ case "int64":
+ return "0"
+ case "float32", "float64":
+ return "0.0"
+ case "bool":
+ return "false"
+ default:
+ if strings.HasPrefix(goType, "[]") {
+ return "Vec::new()"
+ }
+ if strings.HasPrefix(goType, "map[") {
+ return "std::collections::HashMap::new()"
+ }
+ if strings.HasPrefix(goType, "*") {
+ return "None"
+ }
+ return "serde_json::Value::Null"
+ }
+}
+
+// RustFunctionName returns the Rust function name for a method (snake_case).
+// Uses just the method name without service prefix since the module provides namespacing.
+func (m Method) RustFunctionName(_ string) string {
+ return ToSnakeCase(m.Name)
+}
+
+// RustDocComment returns a properly formatted Rust doc comment.
+// Each line of the input doc string is prefixed with "/// ".
+func RustDocComment(doc string) string {
+ if doc == "" {
+ return ""
+ }
+ lines := strings.Split(doc, "\n")
+ var result []string
+ for _, line := range lines {
+ result = append(result, "/// "+line)
+ }
+ return strings.Join(result, "\n")
+}
+
+// RustType returns the Rust type for this parameter.
+func (p Param) RustType() string {
+ return ToRustType(p.Type)
+}
+
+// RustParamType returns the Rust type for this parameter when used as a function argument.
+func (p Param) RustParamType() string {
+ return RustParamType(p.Type)
+}
+
+// RustName returns the snake_case Rust name for this parameter.
+func (p Param) RustName() string {
+ return ToSnakeCase(p.Name)
+}
+
+// NeedsToOwned returns true if the parameter needs .to_owned() when used.
+func (p Param) NeedsToOwned() bool {
+ return p.Type == "string"
+}
diff --git a/plugins/cmd/hostgen/main.go b/plugins/cmd/hostgen/main.go
index c57c12536..545d601a5 100644
--- a/plugins/cmd/hostgen/main.go
+++ b/plugins/cmd/hostgen/main.go
@@ -11,8 +11,9 @@
// -package Output package name (default: inferred from output directory)
// -host-only Generate only host-side code (default: false)
// -plugin-only Generate only plugin/client-side code (default: false)
-// -go Generate Go client wrappers (default: true when not using -python)
+// -go Generate Go client wrappers (default: true when not using -python/-rust)
// -python Generate Python client wrappers (default: false)
+// -rust Generate Rust client wrappers (default: false)
// -v Verbose output
// -dry-run Preview generated code without writing files
package main
@@ -37,6 +38,7 @@ func main() {
pluginOnly = flag.Bool("plugin-only", false, "Generate only plugin/client-side code")
goClient = flag.Bool("go", false, "Generate Go client wrappers")
pyClient = flag.Bool("python", false, "Generate Python client wrappers")
+ rsClient = flag.Bool("rust", false, "Generate Rust client wrappers")
verbose = flag.Bool("v", false, "Verbose output")
dryRun = flag.Bool("dry-run", false, "Preview generated code without writing files")
)
@@ -72,11 +74,13 @@ func main() {
// Determine what to generate
generateHost := !*pluginOnly
// Default: generate Go clients if no language flag is specified
- // If -python is specified without -go, only generate Python
+ // If -python or -rust is specified without -go, only generate those
// If -go is specified, generate Go
- // If both are specified, generate both
- generateGoClient := !*hostOnly && (*goClient || !*pyClient)
+ // If multiple are specified, generate all specified
+ anyLangFlag := *goClient || *pyClient || *rsClient
+ generateGoClient := !*hostOnly && (*goClient || !anyLangFlag)
generatePyClient := !*hostOnly && *pyClient
+ generateRsClient := !*hostOnly && *rsClient
if *verbose {
fmt.Printf("Input directory: %s\n", absInput)
@@ -85,6 +89,7 @@ func main() {
fmt.Printf("Generate host code: %v\n", generateHost)
fmt.Printf("Generate Go client code: %v\n", generateGoClient)
fmt.Printf("Generate Python client code: %v\n", generatePyClient)
+ fmt.Printf("Generate Rust client code: %v\n", generateRsClient)
}
// Parse source files
@@ -133,6 +138,14 @@ func main() {
os.Exit(1)
}
}
+
+ // Generate Rust client-side code
+ if generateRsClient {
+ if err := generateRustClientCode(svc, absOutput, *dryRun, *verbose); err != nil {
+ fmt.Fprintf(os.Stderr, "Error generating Rust client code for %s: %v\n", svc.Name, err)
+ os.Exit(1)
+ }
+ }
}
}
@@ -231,3 +244,34 @@ func generatePythonClientCode(svc internal.Service, outputDir string, dryRun, ve
}
return nil
}
+
+// generateRustClientCode generates Rust client-side code for a service.
+func generateRustClientCode(svc internal.Service, outputDir string, dryRun, verbose bool) error {
+ code, err := internal.GenerateClientRust(svc)
+ if err != nil {
+ return fmt.Errorf("generating code: %w", err)
+ }
+
+ // Rust code goes in rust/ subdirectory
+ clientDir := filepath.Join(outputDir, "rust")
+ clientFile := filepath.Join(clientDir, "nd_host_"+strings.ToLower(svc.Name)+".rs")
+
+ if dryRun {
+ fmt.Printf("=== %s ===\n%s\n", clientFile, code)
+ return nil
+ }
+
+ // Create rust/ subdirectory if needed
+ if err := os.MkdirAll(clientDir, 0755); err != nil {
+ return fmt.Errorf("creating rust client directory: %w", err)
+ }
+
+ if err := os.WriteFile(clientFile, code, 0600); err != nil {
+ return fmt.Errorf("writing file: %w", err)
+ }
+
+ if verbose {
+ fmt.Printf("Generated Rust client code: %s\n", clientFile)
+ }
+ return nil
+}
diff --git a/plugins/cmd/hostgen/testdata/codec_client_expected.rs b/plugins/cmd/hostgen/testdata/codec_client_expected.rs
new file mode 100644
index 000000000..c4b5e7e7c
--- /dev/null
+++ b/plugins/cmd/hostgen/testdata/codec_client_expected.rs
@@ -0,0 +1,51 @@
+// 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 extism-pdk.
+
+use extism_pdk::*;
+use serde::{Deserialize, Serialize};
+
+#[derive(Debug, Clone, Serialize)]
+#[serde(rename_all = "camelCase")]
+struct CodecEncodeRequest {
+ data: Vec,
+}
+
+#[derive(Debug, Clone, Deserialize)]
+#[serde(rename_all = "camelCase")]
+struct CodecEncodeResponse {
+ #[serde(default)]
+ result: Vec,
+ #[serde(default)]
+ error: Option,
+}
+
+#[host_fn]
+extern "ExtismHost" {
+ fn codec_encode(input: Json) -> Json;
+}
+
+/// Calls the codec_encode host function.
+///
+/// # Arguments
+/// * `data` - Vec parameter.
+///
+/// # Returns
+/// The result value.
+///
+/// # Errors
+/// Returns an error if the host function call fails.
+pub fn encode(data: Vec) -> Result, Error> {
+ let response = unsafe {
+ codec_encode(Json(CodecEncodeRequest {
+ data: data,
+ }))?
+ };
+
+ if let Some(err) = response.0.error {
+ return Err(Error::msg(err));
+ }
+
+ Ok(response.0.result)
+}
diff --git a/plugins/cmd/hostgen/testdata/comprehensive_client_expected.py b/plugins/cmd/hostgen/testdata/comprehensive_client_expected.py
new file mode 100644
index 000000000..bcb6e2f12
--- /dev/null
+++ b/plugins/cmd/hostgen/testdata/comprehensive_client_expected.py
@@ -0,0 +1,341 @@
+# Code generated by hostgen. DO NOT EDIT.
+#
+# This file contains client wrappers for the Comprehensive host service.
+# It is intended for use in Navidrome plugins built with extism-py.
+#
+# IMPORTANT: Due to a limitation in extism-py, you cannot import this file directly.
+# The @extism.import_fn decorators are only detected when defined in the plugin's
+# main __init__.py file. Copy the needed functions from this file into your plugin.
+
+from dataclasses import dataclass
+from typing import Any
+
+import extism
+import json
+
+
+class HostFunctionError(Exception):
+ """Raised when a host function returns an error."""
+ pass
+
+
+@extism.import_fn("extism:host/user", "comprehensive_simpleparams")
+def _comprehensive_simpleparams(offset: int) -> int:
+ """Raw host function - do not call directly."""
+ ...
+
+
+@extism.import_fn("extism:host/user", "comprehensive_structparam")
+def _comprehensive_structparam(offset: int) -> int:
+ """Raw host function - do not call directly."""
+ ...
+
+
+@extism.import_fn("extism:host/user", "comprehensive_mixedparams")
+def _comprehensive_mixedparams(offset: int) -> int:
+ """Raw host function - do not call directly."""
+ ...
+
+
+@extism.import_fn("extism:host/user", "comprehensive_noerror")
+def _comprehensive_noerror(offset: int) -> int:
+ """Raw host function - do not call directly."""
+ ...
+
+
+@extism.import_fn("extism:host/user", "comprehensive_noparams")
+def _comprehensive_noparams(offset: int) -> int:
+ """Raw host function - do not call directly."""
+ ...
+
+
+@extism.import_fn("extism:host/user", "comprehensive_noparamsnoreturns")
+def _comprehensive_noparamsnoreturns(offset: int) -> int:
+ """Raw host function - do not call directly."""
+ ...
+
+
+@extism.import_fn("extism:host/user", "comprehensive_pointerparams")
+def _comprehensive_pointerparams(offset: int) -> int:
+ """Raw host function - do not call directly."""
+ ...
+
+
+@extism.import_fn("extism:host/user", "comprehensive_mapparams")
+def _comprehensive_mapparams(offset: int) -> int:
+ """Raw host function - do not call directly."""
+ ...
+
+
+@extism.import_fn("extism:host/user", "comprehensive_multiplereturns")
+def _comprehensive_multiplereturns(offset: int) -> int:
+ """Raw host function - do not call directly."""
+ ...
+
+
+@extism.import_fn("extism:host/user", "comprehensive_byteslice")
+def _comprehensive_byteslice(offset: int) -> int:
+ """Raw host function - do not call directly."""
+ ...
+
+
+@dataclass
+class ComprehensiveMultipleReturnsResult:
+ """Result type for comprehensive_multiple_returns."""
+ results: Any
+ total: int
+
+
+def comprehensive_simple_params(name: str, count: int) -> str:
+ """Call the comprehensive_simpleparams host function.
+
+ Args:
+ name: str parameter.
+ count: int parameter.
+
+ Returns:
+ str: The result value.
+
+ Raises:
+ HostFunctionError: If the host function returns an error.
+ """
+ request = {
+ "name": name,
+ "count": count,
+ }
+ request_bytes = json.dumps(request).encode("utf-8")
+ request_mem = extism.memory.alloc(request_bytes)
+ response_offset = _comprehensive_simpleparams(request_mem.offset)
+ response_mem = extism.memory.find(response_offset)
+ response = json.loads(extism.memory.string(response_mem))
+
+ if response.get("error"):
+ raise HostFunctionError(response["error"])
+
+ return response.get("result", "")
+
+
+def comprehensive_struct_param(user: Any) -> None:
+ """Call the comprehensive_structparam host function.
+
+ Args:
+ user: Any parameter.
+
+ Raises:
+ HostFunctionError: If the host function returns an error.
+ """
+ request = {
+ "user": user,
+ }
+ request_bytes = json.dumps(request).encode("utf-8")
+ request_mem = extism.memory.alloc(request_bytes)
+ response_offset = _comprehensive_structparam(request_mem.offset)
+ response_mem = extism.memory.find(response_offset)
+ response = json.loads(extism.memory.string(response_mem))
+
+ if response.get("error"):
+ raise HostFunctionError(response["error"])
+
+
+
+def comprehensive_mixed_params(id: str, filter: Any) -> int:
+ """Call the comprehensive_mixedparams host function.
+
+ Args:
+ id: str parameter.
+ filter: Any parameter.
+
+ Returns:
+ int: The result value.
+
+ Raises:
+ HostFunctionError: If the host function returns an error.
+ """
+ request = {
+ "id": id,
+ "filter": filter,
+ }
+ request_bytes = json.dumps(request).encode("utf-8")
+ request_mem = extism.memory.alloc(request_bytes)
+ response_offset = _comprehensive_mixedparams(request_mem.offset)
+ response_mem = extism.memory.find(response_offset)
+ response = json.loads(extism.memory.string(response_mem))
+
+ if response.get("error"):
+ raise HostFunctionError(response["error"])
+
+ return response.get("result", 0)
+
+
+def comprehensive_no_error(name: str) -> str:
+ """Call the comprehensive_noerror host function.
+
+ Args:
+ name: str parameter.
+
+ Returns:
+ str: The result value.
+
+ Raises:
+ HostFunctionError: If the host function returns an error.
+ """
+ request = {
+ "name": name,
+ }
+ request_bytes = json.dumps(request).encode("utf-8")
+ request_mem = extism.memory.alloc(request_bytes)
+ response_offset = _comprehensive_noerror(request_mem.offset)
+ response_mem = extism.memory.find(response_offset)
+ response = json.loads(extism.memory.string(response_mem))
+
+ if response.get("error"):
+ raise HostFunctionError(response["error"])
+
+ return response.get("result", "")
+
+
+def comprehensive_no_params() -> None:
+ """Call the comprehensive_noparams host function.
+
+ Raises:
+ HostFunctionError: If the host function returns an error.
+ """
+ request_bytes = b"{}"
+ request_mem = extism.memory.alloc(request_bytes)
+ response_offset = _comprehensive_noparams(request_mem.offset)
+ response_mem = extism.memory.find(response_offset)
+ response = json.loads(extism.memory.string(response_mem))
+
+ if response.get("error"):
+ raise HostFunctionError(response["error"])
+
+
+
+def comprehensive_no_params_no_returns() -> None:
+ """Call the comprehensive_noparamsnoreturns host function.
+
+ Raises:
+ HostFunctionError: If the host function returns an error.
+ """
+ request_bytes = b"{}"
+ request_mem = extism.memory.alloc(request_bytes)
+ response_offset = _comprehensive_noparamsnoreturns(request_mem.offset)
+ response_mem = extism.memory.find(response_offset)
+ response = json.loads(extism.memory.string(response_mem))
+
+ if response.get("error"):
+ raise HostFunctionError(response["error"])
+
+
+
+def comprehensive_pointer_params(id: Any, user: Any) -> Any:
+ """Call the comprehensive_pointerparams host function.
+
+ Args:
+ id: Any parameter.
+ user: Any parameter.
+
+ Returns:
+ Any: The result value.
+
+ Raises:
+ HostFunctionError: If the host function returns an error.
+ """
+ request = {
+ "id": id,
+ "user": user,
+ }
+ request_bytes = json.dumps(request).encode("utf-8")
+ request_mem = extism.memory.alloc(request_bytes)
+ response_offset = _comprehensive_pointerparams(request_mem.offset)
+ response_mem = extism.memory.find(response_offset)
+ response = json.loads(extism.memory.string(response_mem))
+
+ if response.get("error"):
+ raise HostFunctionError(response["error"])
+
+ return response.get("result", None)
+
+
+def comprehensive_map_params(data: Any) -> Any:
+ """Call the comprehensive_mapparams host function.
+
+ Args:
+ data: Any parameter.
+
+ Returns:
+ Any: The result value.
+
+ Raises:
+ HostFunctionError: If the host function returns an error.
+ """
+ request = {
+ "data": data,
+ }
+ request_bytes = json.dumps(request).encode("utf-8")
+ request_mem = extism.memory.alloc(request_bytes)
+ response_offset = _comprehensive_mapparams(request_mem.offset)
+ response_mem = extism.memory.find(response_offset)
+ response = json.loads(extism.memory.string(response_mem))
+
+ if response.get("error"):
+ raise HostFunctionError(response["error"])
+
+ return response.get("result", None)
+
+
+def comprehensive_multiple_returns(query: str) -> ComprehensiveMultipleReturnsResult:
+ """Call the comprehensive_multiplereturns host function.
+
+ Args:
+ query: str parameter.
+
+ Returns:
+ ComprehensiveMultipleReturnsResult containing results, total,.
+
+ Raises:
+ HostFunctionError: If the host function returns an error.
+ """
+ request = {
+ "query": query,
+ }
+ request_bytes = json.dumps(request).encode("utf-8")
+ request_mem = extism.memory.alloc(request_bytes)
+ response_offset = _comprehensive_multiplereturns(request_mem.offset)
+ response_mem = extism.memory.find(response_offset)
+ response = json.loads(extism.memory.string(response_mem))
+
+ if response.get("error"):
+ raise HostFunctionError(response["error"])
+
+ return ComprehensiveMultipleReturnsResult(
+ results=response.get("results", None),
+ total=response.get("total", 0),
+ )
+
+
+def comprehensive_byte_slice(data: bytes) -> bytes:
+ """Call the comprehensive_byteslice host function.
+
+ Args:
+ data: bytes parameter.
+
+ Returns:
+ bytes: The result value.
+
+ Raises:
+ HostFunctionError: If the host function returns an error.
+ """
+ request = {
+ "data": data,
+ }
+ request_bytes = json.dumps(request).encode("utf-8")
+ request_mem = extism.memory.alloc(request_bytes)
+ response_offset = _comprehensive_byteslice(request_mem.offset)
+ response_mem = extism.memory.find(response_offset)
+ response = json.loads(extism.memory.string(response_mem))
+
+ if response.get("error"):
+ raise HostFunctionError(response["error"])
+
+ return response.get("result", b"")
diff --git a/plugins/cmd/hostgen/testdata/comprehensive_client_expected.rs b/plugins/cmd/hostgen/testdata/comprehensive_client_expected.rs
new file mode 100644
index 000000000..772e127f0
--- /dev/null
+++ b/plugins/cmd/hostgen/testdata/comprehensive_client_expected.rs
@@ -0,0 +1,385 @@
+// Code generated by hostgen. DO NOT EDIT.
+//
+// This file contains client wrappers for the Comprehensive host service.
+// It is intended for use in Navidrome plugins built with extism-pdk.
+
+use extism_pdk::*;
+use serde::{Deserialize, Serialize};
+
+#[derive(Debug, Clone, Serialize)]
+#[serde(rename_all = "camelCase")]
+struct ComprehensiveSimpleParamsRequest {
+ name: String,
+ count: i32,
+}
+
+#[derive(Debug, Clone, Deserialize)]
+#[serde(rename_all = "camelCase")]
+struct ComprehensiveSimpleParamsResponse {
+ #[serde(default)]
+ result: String,
+ #[serde(default)]
+ error: Option,
+}
+
+#[derive(Debug, Clone, Serialize)]
+#[serde(rename_all = "camelCase")]
+struct ComprehensiveStructParamRequest {
+ user: serde_json::Value,
+}
+
+#[derive(Debug, Clone, Deserialize)]
+#[serde(rename_all = "camelCase")]
+struct ComprehensiveStructParamResponse {
+ #[serde(default)]
+ error: Option,
+}
+
+#[derive(Debug, Clone, Serialize)]
+#[serde(rename_all = "camelCase")]
+struct ComprehensiveMixedParamsRequest {
+ id: String,
+ filter: serde_json::Value,
+}
+
+#[derive(Debug, Clone, Deserialize)]
+#[serde(rename_all = "camelCase")]
+struct ComprehensiveMixedParamsResponse {
+ #[serde(default)]
+ result: i32,
+ #[serde(default)]
+ error: Option,
+}
+
+#[derive(Debug, Clone, Serialize)]
+#[serde(rename_all = "camelCase")]
+struct ComprehensiveNoErrorRequest {
+ name: String,
+}
+
+#[derive(Debug, Clone, Deserialize)]
+#[serde(rename_all = "camelCase")]
+struct ComprehensiveNoErrorResponse {
+ #[serde(default)]
+ result: String,
+ #[serde(default)]
+ error: Option,
+}
+
+#[derive(Debug, Clone, Deserialize)]
+#[serde(rename_all = "camelCase")]
+struct ComprehensiveNoParamsResponse {
+ #[serde(default)]
+ error: Option,
+}
+
+#[derive(Debug, Clone, Deserialize)]
+#[serde(rename_all = "camelCase")]
+struct ComprehensiveNoParamsNoReturnsResponse {
+ #[serde(default)]
+ error: Option,
+}
+
+#[derive(Debug, Clone, Serialize)]
+#[serde(rename_all = "camelCase")]
+struct ComprehensivePointerParamsRequest {
+ id: Option,
+ user: Option,
+}
+
+#[derive(Debug, Clone, Deserialize)]
+#[serde(rename_all = "camelCase")]
+struct ComprehensivePointerParamsResponse {
+ #[serde(default)]
+ result: Option,
+ #[serde(default)]
+ error: Option,
+}
+
+#[derive(Debug, Clone, Serialize)]
+#[serde(rename_all = "camelCase")]
+struct ComprehensiveMapParamsRequest {
+ data: std::collections::HashMap,
+}
+
+#[derive(Debug, Clone, Deserialize)]
+#[serde(rename_all = "camelCase")]
+struct ComprehensiveMapParamsResponse {
+ #[serde(default)]
+ result: serde_json::Value,
+ #[serde(default)]
+ error: Option,
+}
+
+#[derive(Debug, Clone, Serialize)]
+#[serde(rename_all = "camelCase")]
+struct ComprehensiveMultipleReturnsRequest {
+ query: String,
+}
+
+#[derive(Debug, Clone, Deserialize)]
+#[serde(rename_all = "camelCase")]
+struct ComprehensiveMultipleReturnsResponse {
+ #[serde(default)]
+ results: Vec,
+ #[serde(default)]
+ total: i32,
+ #[serde(default)]
+ error: Option,
+}
+
+#[derive(Debug, Clone, Serialize)]
+#[serde(rename_all = "camelCase")]
+struct ComprehensiveByteSliceRequest {
+ data: Vec,
+}
+
+#[derive(Debug, Clone, Deserialize)]
+#[serde(rename_all = "camelCase")]
+struct ComprehensiveByteSliceResponse {
+ #[serde(default)]
+ result: Vec,
+ #[serde(default)]
+ error: Option,
+}
+
+#[host_fn]
+extern "ExtismHost" {
+ fn comprehensive_simpleparams(input: Json) -> Json;
+ fn comprehensive_structparam(input: Json) -> Json;
+ fn comprehensive_mixedparams(input: Json) -> Json;
+ fn comprehensive_noerror(input: Json) -> Json;
+ fn comprehensive_noparams(input: Json) -> Json;
+ fn comprehensive_noparamsnoreturns(input: Json) -> Json;
+ fn comprehensive_pointerparams(input: Json) -> Json;
+ fn comprehensive_mapparams(input: Json) -> Json;
+ fn comprehensive_multiplereturns(input: Json) -> Json;
+ fn comprehensive_byteslice(input: Json) -> Json;
+}
+
+/// Calls the comprehensive_simpleparams host function.
+///
+/// # Arguments
+/// * `name` - String parameter.
+/// * `count` - i32 parameter.
+///
+/// # Returns
+/// The result value.
+///
+/// # Errors
+/// Returns an error if the host function call fails.
+pub fn simple_params(name: &str, count: i32) -> Result {
+ let response = unsafe {
+ comprehensive_simpleparams(Json(ComprehensiveSimpleParamsRequest {
+ name: name.to_owned(),
+ count: count,
+ }))?
+ };
+
+ if let Some(err) = response.0.error {
+ return Err(Error::msg(err));
+ }
+
+ Ok(response.0.result)
+}
+
+/// Calls the comprehensive_structparam host function.
+///
+/// # Arguments
+/// * `user` - serde_json::Value parameter.
+///
+/// # Errors
+/// Returns an error if the host function call fails.
+pub fn struct_param(user: serde_json::Value) -> Result<(), Error> {
+ let response = unsafe {
+ comprehensive_structparam(Json(ComprehensiveStructParamRequest {
+ user: user,
+ }))?
+ };
+
+ if let Some(err) = response.0.error {
+ return Err(Error::msg(err));
+ }
+
+ Ok(())
+}
+
+/// Calls the comprehensive_mixedparams host function.
+///
+/// # Arguments
+/// * `id` - String parameter.
+/// * `filter` - serde_json::Value parameter.
+///
+/// # Returns
+/// The result value.
+///
+/// # Errors
+/// Returns an error if the host function call fails.
+pub fn mixed_params(id: &str, filter: serde_json::Value) -> Result {
+ let response = unsafe {
+ comprehensive_mixedparams(Json(ComprehensiveMixedParamsRequest {
+ id: id.to_owned(),
+ filter: filter,
+ }))?
+ };
+
+ if let Some(err) = response.0.error {
+ return Err(Error::msg(err));
+ }
+
+ Ok(response.0.result)
+}
+
+/// Calls the comprehensive_noerror host function.
+///
+/// # Arguments
+/// * `name` - String parameter.
+///
+/// # Returns
+/// The result value.
+///
+/// # Errors
+/// Returns an error if the host function call fails.
+pub fn no_error(name: &str) -> Result {
+ let response = unsafe {
+ comprehensive_noerror(Json(ComprehensiveNoErrorRequest {
+ name: name.to_owned(),
+ }))?
+ };
+
+ if let Some(err) = response.0.error {
+ return Err(Error::msg(err));
+ }
+
+ Ok(response.0.result)
+}
+
+/// Calls the comprehensive_noparams host function.
+///
+/// # Errors
+/// Returns an error if the host function call fails.
+pub fn no_params() -> Result<(), Error> {
+ let response = unsafe {
+ comprehensive_noparams(Json(serde_json::json!({})))?
+ };
+
+ if let Some(err) = response.0.error {
+ return Err(Error::msg(err));
+ }
+
+ Ok(())
+}
+
+/// Calls the comprehensive_noparamsnoreturns host function.
+///
+/// # Errors
+/// Returns an error if the host function call fails.
+pub fn no_params_no_returns() -> Result<(), Error> {
+ let response = unsafe {
+ comprehensive_noparamsnoreturns(Json(serde_json::json!({})))?
+ };
+
+ if let Some(err) = response.0.error {
+ return Err(Error::msg(err));
+ }
+
+ Ok(())
+}
+
+/// Calls the comprehensive_pointerparams host function.
+///
+/// # Arguments
+/// * `id` - Option parameter.
+/// * `user` - Option parameter.
+///
+/// # Returns
+/// The result value.
+///
+/// # Errors
+/// Returns an error if the host function call fails.
+pub fn pointer_params(id: Option, user: Option) -> Result