mirror of
https://github.com/navidrome/navidrome.git
synced 2026-03-04 06:35:52 +00:00
feat(plugins): enhance Rust code generation with typed struct support and improved type handling
Signed-off-by: Deluan <deluan@navidrome.org>
This commit is contained in:
parent
10e5f44617
commit
13ca6149a9
@ -157,6 +157,7 @@ func pythonDefaultValue(p Param) string {
|
||||
|
||||
// rustFuncMap returns the template functions for Rust client code generation.
|
||||
func rustFuncMap(svc Service) template.FuncMap {
|
||||
knownStructs := svc.KnownStructs()
|
||||
return template.FuncMap{
|
||||
"lower": strings.ToLower,
|
||||
"exportName": func(m Method) string { return m.FunctionName(svc.ExportPrefix()) },
|
||||
@ -164,6 +165,9 @@ func rustFuncMap(svc Service) template.FuncMap {
|
||||
"responseType": func(m Method) string { return m.ResponseTypeName(svc.Name) },
|
||||
"rustFunc": func(m Method) string { return m.RustFunctionName(svc.ExportPrefix()) },
|
||||
"rustDocComment": RustDocComment,
|
||||
"rustType": func(p Param) string { return p.RustTypeWithStructs(knownStructs) },
|
||||
"rustParamType": func(p Param) string { return p.RustParamTypeWithStructs(knownStructs) },
|
||||
"fieldRustType": func(f FieldDef) string { return f.RustType(knownStructs) },
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -58,6 +58,13 @@ func parseFile(fset *token.FileSet, path string) ([]Service, error) {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// First pass: collect all struct definitions in the file
|
||||
allStructs := parseStructs(f)
|
||||
structMap := make(map[string]StructDef)
|
||||
for _, s := range allStructs {
|
||||
structMap[s.Name] = s
|
||||
}
|
||||
|
||||
var services []Service
|
||||
|
||||
for _, decl := range f.Decls {
|
||||
@ -91,7 +98,8 @@ func parseFile(fset *token.FileSet, path string) ([]Service, error) {
|
||||
Doc: cleanDoc(docText),
|
||||
}
|
||||
|
||||
// Parse methods
|
||||
// Parse methods and collect referenced types
|
||||
referencedTypes := make(map[string]bool)
|
||||
for _, method := range interfaceType.Methods.List {
|
||||
if len(method.Names) == 0 {
|
||||
continue // Embedded interface
|
||||
@ -114,6 +122,21 @@ func parseFile(fset *token.FileSet, path string) ([]Service, error) {
|
||||
return nil, fmt.Errorf("parsing method %s.%s: %w", typeSpec.Name.Name, method.Names[0].Name, err)
|
||||
}
|
||||
service.Methods = append(service.Methods, m)
|
||||
|
||||
// Collect referenced types from params and returns
|
||||
for _, p := range m.Params {
|
||||
collectReferencedTypes(p.Type, referencedTypes)
|
||||
}
|
||||
for _, r := range m.Returns {
|
||||
collectReferencedTypes(r.Type, referencedTypes)
|
||||
}
|
||||
}
|
||||
|
||||
// Attach referenced structs to the service
|
||||
for typeName := range referencedTypes {
|
||||
if s, exists := structMap[typeName]; exists {
|
||||
service.Structs = append(service.Structs, s)
|
||||
}
|
||||
}
|
||||
|
||||
if len(service.Methods) > 0 {
|
||||
@ -125,6 +148,163 @@ func parseFile(fset *token.FileSet, path string) ([]Service, error) {
|
||||
return services, nil
|
||||
}
|
||||
|
||||
// parseStructs extracts all struct type definitions from a parsed Go file.
|
||||
func parseStructs(f *ast.File) []StructDef {
|
||||
var structs []StructDef
|
||||
|
||||
for _, decl := range f.Decls {
|
||||
genDecl, ok := decl.(*ast.GenDecl)
|
||||
if !ok || genDecl.Tok != token.TYPE {
|
||||
continue
|
||||
}
|
||||
|
||||
for _, spec := range genDecl.Specs {
|
||||
typeSpec, ok := spec.(*ast.TypeSpec)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
|
||||
structType, ok := typeSpec.Type.(*ast.StructType)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
|
||||
docText, _ := getDocComment(genDecl, typeSpec)
|
||||
s := StructDef{
|
||||
Name: typeSpec.Name.Name,
|
||||
Doc: cleanDoc(docText),
|
||||
}
|
||||
|
||||
// Parse struct fields
|
||||
for _, field := range structType.Fields.List {
|
||||
if len(field.Names) == 0 {
|
||||
continue // Embedded field
|
||||
}
|
||||
|
||||
fieldDef := parseStructField(field)
|
||||
s.Fields = append(s.Fields, fieldDef...)
|
||||
}
|
||||
|
||||
structs = append(structs, s)
|
||||
}
|
||||
}
|
||||
|
||||
return structs
|
||||
}
|
||||
|
||||
// parseStructField parses a struct field and returns FieldDef for each name.
|
||||
func parseStructField(field *ast.Field) []FieldDef {
|
||||
var fields []FieldDef
|
||||
typeName := typeToString(field.Type)
|
||||
|
||||
// Parse struct tag for JSON field name and omitempty
|
||||
jsonTag := ""
|
||||
omitEmpty := false
|
||||
if field.Tag != nil {
|
||||
tag := field.Tag.Value
|
||||
// Remove backticks
|
||||
tag = strings.Trim(tag, "`")
|
||||
// Parse json tag
|
||||
jsonTag, omitEmpty = parseJSONTag(tag)
|
||||
}
|
||||
|
||||
// Get doc comment
|
||||
var doc string
|
||||
if field.Doc != nil {
|
||||
doc = cleanDoc(field.Doc.Text())
|
||||
}
|
||||
|
||||
for _, name := range field.Names {
|
||||
fieldJSONTag := jsonTag
|
||||
if fieldJSONTag == "" {
|
||||
// Default to field name with camelCase
|
||||
fieldJSONTag = toJSONName(name.Name)
|
||||
}
|
||||
fields = append(fields, FieldDef{
|
||||
Name: name.Name,
|
||||
Type: typeName,
|
||||
JSONTag: fieldJSONTag,
|
||||
OmitEmpty: omitEmpty,
|
||||
Doc: doc,
|
||||
})
|
||||
}
|
||||
|
||||
return fields
|
||||
}
|
||||
|
||||
// parseJSONTag extracts the json field name and omitempty flag from a struct tag.
|
||||
func parseJSONTag(tag string) (name string, omitEmpty bool) {
|
||||
// Find json:"..." in the tag
|
||||
for _, part := range strings.Split(tag, " ") {
|
||||
if strings.HasPrefix(part, `json:"`) {
|
||||
value := strings.TrimPrefix(part, `json:"`)
|
||||
value = strings.TrimSuffix(value, `"`)
|
||||
parts := strings.Split(value, ",")
|
||||
if len(parts) > 0 && parts[0] != "-" {
|
||||
name = parts[0]
|
||||
}
|
||||
for _, opt := range parts[1:] {
|
||||
if opt == "omitempty" {
|
||||
omitEmpty = true
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
}
|
||||
return "", false
|
||||
}
|
||||
|
||||
// collectReferencedTypes extracts custom type names from a Go type string.
|
||||
// It handles pointers, slices, and maps, collecting base type names.
|
||||
func collectReferencedTypes(goType string, refs map[string]bool) {
|
||||
// Strip pointer
|
||||
if strings.HasPrefix(goType, "*") {
|
||||
collectReferencedTypes(goType[1:], refs)
|
||||
return
|
||||
}
|
||||
// Strip slice
|
||||
if strings.HasPrefix(goType, "[]") {
|
||||
if goType != "[]byte" {
|
||||
collectReferencedTypes(goType[2:], refs)
|
||||
}
|
||||
return
|
||||
}
|
||||
// Handle map
|
||||
if strings.HasPrefix(goType, "map[") {
|
||||
rest := goType[4:] // Remove "map["
|
||||
depth := 1
|
||||
keyEnd := 0
|
||||
for i, r := range rest {
|
||||
if r == '[' {
|
||||
depth++
|
||||
} else if r == ']' {
|
||||
depth--
|
||||
if depth == 0 {
|
||||
keyEnd = i
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
keyType := rest[:keyEnd]
|
||||
valueType := rest[keyEnd+1:]
|
||||
collectReferencedTypes(keyType, refs)
|
||||
collectReferencedTypes(valueType, refs)
|
||||
return
|
||||
}
|
||||
|
||||
// Check if it's a custom type (starts with uppercase, not a builtin)
|
||||
if len(goType) > 0 && goType[0] >= 'A' && goType[0] <= 'Z' {
|
||||
switch goType {
|
||||
case "String", "Bool", "Int", "Int32", "Int64", "Float32", "Float64":
|
||||
// Not custom types (just capitalized for some reason)
|
||||
default:
|
||||
refs[goType] = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// toJSONName is imported from types.go via the same package
|
||||
|
||||
// getDocComment extracts the doc comment for a type spec.
|
||||
// Returns both the readable doc text and the raw comment text (which includes pragma-style comments).
|
||||
func getDocComment(genDecl *ast.GenDecl, typeSpec *ast.TypeSpec) (docText, rawText string) {
|
||||
|
||||
@ -5,6 +5,23 @@
|
||||
|
||||
use extism_pdk::*;
|
||||
use serde::{Deserialize, Serialize};
|
||||
{{- /* Generate struct definitions */ -}}
|
||||
{{- range .Service.Structs}}
|
||||
{{if .Doc}}
|
||||
{{rustDocComment .Doc}}
|
||||
{{else}}
|
||||
{{end}}#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct {{.Name}} {
|
||||
{{- range .Fields}}
|
||||
{{- if .NeedsDefault}}
|
||||
#[serde(default)]
|
||||
{{- end}}
|
||||
pub {{.RustName}}: {{fieldRustType .}},
|
||||
{{- end}}
|
||||
}
|
||||
{{- end}}
|
||||
{{- /* Generate request/response types */ -}}
|
||||
{{- range .Service.Methods}}
|
||||
{{- if .HasParams}}
|
||||
|
||||
@ -12,7 +29,7 @@ use serde::{Deserialize, Serialize};
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct {{requestType .}} {
|
||||
{{- range .Params}}
|
||||
{{.RustName}}: {{.RustType}},
|
||||
{{.RustName}}: {{rustType .}},
|
||||
{{- end}}
|
||||
}
|
||||
{{- end}}
|
||||
@ -22,7 +39,7 @@ struct {{requestType .}} {
|
||||
struct {{responseType .}} {
|
||||
{{- range .Returns}}
|
||||
#[serde(default)]
|
||||
{{.RustName}}: {{.RustType}},
|
||||
{{.RustName}}: {{rustType .}},
|
||||
{{- end}}
|
||||
#[serde(default)]
|
||||
error: Option<String>,
|
||||
@ -44,7 +61,7 @@ extern "ExtismHost" {
|
||||
///
|
||||
/// # Arguments
|
||||
{{- range .Params}}
|
||||
/// * `{{.RustName}}` - {{.RustType}} parameter.
|
||||
/// * `{{.RustName}}` - {{rustType .}} parameter.
|
||||
{{- end}}
|
||||
{{- end}}
|
||||
{{- if .HasReturns}}
|
||||
@ -59,7 +76,7 @@ extern "ExtismHost" {
|
||||
///
|
||||
/// # 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> {
|
||||
pub fn {{rustFunc .}}({{range $i, $p := .Params}}{{if $i}}, {{end}}{{$p.RustName}}: {{rustParamType $p}}{{end}}) -> Result<{{if eq (len .Returns) 0}}(){{else if eq (len .Returns) 1}}{{rustType (index .Returns 0)}}{{else}}({{range $i, $r := .Returns}}{{if $i}}, {{end}}{{rustType $r}}{{end}}){{end}}, Error> {
|
||||
let response = unsafe {
|
||||
{{- if .HasParams}}
|
||||
{{exportName .}}(Json({{requestType .}} {
|
||||
|
||||
@ -7,11 +7,28 @@ import (
|
||||
|
||||
// Service represents a parsed host service interface.
|
||||
type Service struct {
|
||||
Name string // Service name from annotation (e.g., "SubsonicAPI")
|
||||
Permission string // Manifest permission key (e.g., "subsonicapi")
|
||||
Interface string // Go interface name (e.g., "SubsonicAPIService")
|
||||
Methods []Method // Methods marked with //nd:hostfunc
|
||||
Doc string // Documentation comment for the service
|
||||
Name string // Service name from annotation (e.g., "SubsonicAPI")
|
||||
Permission string // Manifest permission key (e.g., "subsonicapi")
|
||||
Interface string // Go interface name (e.g., "SubsonicAPIService")
|
||||
Methods []Method // Methods marked with //nd:hostfunc
|
||||
Doc string // Documentation comment for the service
|
||||
Structs []StructDef // Structs used by this service
|
||||
}
|
||||
|
||||
// StructDef represents a Go struct type definition.
|
||||
type StructDef struct {
|
||||
Name string // Go struct name (e.g., "Library")
|
||||
Fields []FieldDef // Struct fields
|
||||
Doc string // Documentation comment
|
||||
}
|
||||
|
||||
// FieldDef represents a field within a struct.
|
||||
type FieldDef struct {
|
||||
Name string // Go field name (e.g., "TotalSongs")
|
||||
Type string // Go type (e.g., "int32", "*string", "[]User")
|
||||
JSONTag string // JSON tag value (e.g., "totalSongs,omitempty")
|
||||
OmitEmpty bool // Whether the field has omitempty tag
|
||||
Doc string // Field documentation
|
||||
}
|
||||
|
||||
// OutputFileName returns the generated file name for this service.
|
||||
@ -24,6 +41,15 @@ func (s Service) ExportPrefix() string {
|
||||
return strings.ToLower(s.Name)
|
||||
}
|
||||
|
||||
// KnownStructs returns a map of struct names defined in this service.
|
||||
func (s Service) KnownStructs() map[string]bool {
|
||||
result := make(map[string]bool)
|
||||
for _, st := range s.Structs {
|
||||
result[st.Name] = true
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// Method represents a host function method within a service.
|
||||
type Method struct {
|
||||
Name string // Go method name (e.g., "Call")
|
||||
@ -187,60 +213,7 @@ func (p Param) PythonName() string {
|
||||
|
||||
// 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<u8>"
|
||||
}
|
||||
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"
|
||||
}
|
||||
return ToRustTypeWithStructs(goType, nil)
|
||||
}
|
||||
|
||||
// RustParamType returns the Rust type for a function parameter (uses &str for strings).
|
||||
@ -303,11 +276,24 @@ func (p Param) RustType() string {
|
||||
return ToRustType(p.Type)
|
||||
}
|
||||
|
||||
// RustTypeWithStructs returns the Rust type using known struct names.
|
||||
func (p Param) RustTypeWithStructs(knownStructs map[string]bool) string {
|
||||
return ToRustTypeWithStructs(p.Type, knownStructs)
|
||||
}
|
||||
|
||||
// RustParamType returns the Rust type for this parameter when used as a function argument.
|
||||
func (p Param) RustParamType() string {
|
||||
return RustParamType(p.Type)
|
||||
}
|
||||
|
||||
// RustParamTypeWithStructs returns the Rust param type using known struct names.
|
||||
func (p Param) RustParamTypeWithStructs(knownStructs map[string]bool) string {
|
||||
if p.Type == "string" {
|
||||
return "&str"
|
||||
}
|
||||
return ToRustTypeWithStructs(p.Type, knownStructs)
|
||||
}
|
||||
|
||||
// RustName returns the snake_case Rust name for this parameter.
|
||||
func (p Param) RustName() string {
|
||||
return ToSnakeCase(p.Name)
|
||||
@ -317,3 +303,82 @@ func (p Param) RustName() string {
|
||||
func (p Param) NeedsToOwned() bool {
|
||||
return p.Type == "string"
|
||||
}
|
||||
|
||||
// RustType returns the Rust type for this field, using known struct names.
|
||||
func (f FieldDef) RustType(knownStructs map[string]bool) string {
|
||||
return ToRustTypeWithStructs(f.Type, knownStructs)
|
||||
}
|
||||
|
||||
// RustName returns the snake_case Rust name for this field.
|
||||
func (f FieldDef) RustName() string {
|
||||
return ToSnakeCase(f.Name)
|
||||
}
|
||||
|
||||
// NeedsDefault returns true if the field needs #[serde(default)] attribute.
|
||||
// This is true for fields with omitempty tag.
|
||||
func (f FieldDef) NeedsDefault() bool {
|
||||
return f.OmitEmpty
|
||||
}
|
||||
|
||||
// ToRustTypeWithStructs converts a Go type to its Rust equivalent,
|
||||
// using known struct names instead of serde_json::Value.
|
||||
func ToRustTypeWithStructs(goType string, knownStructs map[string]bool) string {
|
||||
// Handle pointer types
|
||||
if strings.HasPrefix(goType, "*") {
|
||||
inner := ToRustTypeWithStructs(goType[1:], knownStructs)
|
||||
return "Option<" + inner + ">"
|
||||
}
|
||||
// Handle slice types
|
||||
if strings.HasPrefix(goType, "[]") {
|
||||
if goType == "[]byte" {
|
||||
return "Vec<u8>"
|
||||
}
|
||||
inner := ToRustTypeWithStructs(goType[2:], knownStructs)
|
||||
return "Vec<" + inner + ">"
|
||||
}
|
||||
// Handle map types
|
||||
if strings.HasPrefix(goType, "map[") {
|
||||
// Extract key and value types from map[K]V
|
||||
rest := goType[4:] // Remove "map["
|
||||
depth := 1
|
||||
keyEnd := 0
|
||||
for i, r := range rest {
|
||||
if r == '[' {
|
||||
depth++
|
||||
} else if r == ']' {
|
||||
depth--
|
||||
if depth == 0 {
|
||||
keyEnd = i
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
keyType := rest[:keyEnd]
|
||||
valueType := rest[keyEnd+1:]
|
||||
return "std::collections::HashMap<" + ToRustTypeWithStructs(keyType, knownStructs) + ", " + ToRustTypeWithStructs(valueType, knownStructs) + ">"
|
||||
}
|
||||
|
||||
switch goType {
|
||||
case "string":
|
||||
return "String"
|
||||
case "int", "int32":
|
||||
return "i32"
|
||||
case "int64":
|
||||
return "i64"
|
||||
case "float32":
|
||||
return "f32"
|
||||
case "float64":
|
||||
return "f64"
|
||||
case "bool":
|
||||
return "bool"
|
||||
case "interface{}", "any":
|
||||
return "serde_json::Value"
|
||||
default:
|
||||
// Check if this is a known struct type
|
||||
if knownStructs != nil && knownStructs[goType] {
|
||||
return goType
|
||||
}
|
||||
// For unknown custom types, fall back to Value
|
||||
return "serde_json::Value"
|
||||
}
|
||||
}
|
||||
|
||||
@ -6,6 +6,19 @@
|
||||
use extism_pdk::*;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct User2 {
|
||||
pub id: String,
|
||||
pub name: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct Filter2 {
|
||||
pub active: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct ComprehensiveSimpleParamsRequest {
|
||||
@ -25,7 +38,7 @@ struct ComprehensiveSimpleParamsResponse {
|
||||
#[derive(Debug, Clone, Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct ComprehensiveStructParamRequest {
|
||||
user: serde_json::Value,
|
||||
user: User2,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize)]
|
||||
@ -39,7 +52,7 @@ struct ComprehensiveStructParamResponse {
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct ComprehensiveMixedParamsRequest {
|
||||
id: String,
|
||||
filter: serde_json::Value,
|
||||
filter: Filter2,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize)]
|
||||
@ -84,14 +97,14 @@ struct ComprehensiveNoParamsNoReturnsResponse {
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct ComprehensivePointerParamsRequest {
|
||||
id: Option<String>,
|
||||
user: Option<serde_json::Value>,
|
||||
user: Option<User2>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct ComprehensivePointerParamsResponse {
|
||||
#[serde(default)]
|
||||
result: Option<serde_json::Value>,
|
||||
result: Option<User2>,
|
||||
#[serde(default)]
|
||||
error: Option<String>,
|
||||
}
|
||||
@ -121,7 +134,7 @@ struct ComprehensiveMultipleReturnsRequest {
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct ComprehensiveMultipleReturnsResponse {
|
||||
#[serde(default)]
|
||||
results: Vec<serde_json::Value>,
|
||||
results: Vec<User2>,
|
||||
#[serde(default)]
|
||||
total: i32,
|
||||
#[serde(default)]
|
||||
@ -186,11 +199,11 @@ pub fn simple_params(name: &str, count: i32) -> Result<String, Error> {
|
||||
/// Calls the comprehensive_structparam host function.
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `user` - serde_json::Value parameter.
|
||||
/// * `user` - User2 parameter.
|
||||
///
|
||||
/// # Errors
|
||||
/// Returns an error if the host function call fails.
|
||||
pub fn struct_param(user: serde_json::Value) -> Result<(), Error> {
|
||||
pub fn struct_param(user: User2) -> Result<(), Error> {
|
||||
let response = unsafe {
|
||||
comprehensive_structparam(Json(ComprehensiveStructParamRequest {
|
||||
user: user,
|
||||
@ -208,14 +221,14 @@ pub fn struct_param(user: serde_json::Value) -> Result<(), Error> {
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `id` - String parameter.
|
||||
/// * `filter` - serde_json::Value parameter.
|
||||
/// * `filter` - Filter2 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<i32, Error> {
|
||||
pub fn mixed_params(id: &str, filter: Filter2) -> Result<i32, Error> {
|
||||
let response = unsafe {
|
||||
comprehensive_mixedparams(Json(ComprehensiveMixedParamsRequest {
|
||||
id: id.to_owned(),
|
||||
@ -290,14 +303,14 @@ pub fn no_params_no_returns() -> Result<(), Error> {
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `id` - Option<String> parameter.
|
||||
/// * `user` - Option<serde_json::Value> parameter.
|
||||
/// * `user` - Option<User2> parameter.
|
||||
///
|
||||
/// # Returns
|
||||
/// The result value.
|
||||
///
|
||||
/// # Errors
|
||||
/// Returns an error if the host function call fails.
|
||||
pub fn pointer_params(id: Option<String>, user: Option<serde_json::Value>) -> Result<Option<serde_json::Value>, Error> {
|
||||
pub fn pointer_params(id: Option<String>, user: Option<User2>) -> Result<Option<User2>, Error> {
|
||||
let response = unsafe {
|
||||
comprehensive_pointerparams(Json(ComprehensivePointerParamsRequest {
|
||||
id: id,
|
||||
@ -346,7 +359,7 @@ pub fn map_params(data: std::collections::HashMap<String, serde_json::Value>) ->
|
||||
///
|
||||
/// # Errors
|
||||
/// Returns an error if the host function call fails.
|
||||
pub fn multiple_returns(query: &str) -> Result<(Vec<serde_json::Value>, i32), Error> {
|
||||
pub fn multiple_returns(query: &str) -> Result<(Vec<User2>, i32), Error> {
|
||||
let response = unsafe {
|
||||
comprehensive_multiplereturns(Json(ComprehensiveMultipleReturnsRequest {
|
||||
query: query.to_owned(),
|
||||
|
||||
@ -6,11 +6,17 @@
|
||||
use extism_pdk::*;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct Filter {
|
||||
pub active: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct ListItemsRequest {
|
||||
name: String,
|
||||
filter: serde_json::Value,
|
||||
filter: Filter,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize)]
|
||||
@ -31,14 +37,14 @@ extern "ExtismHost" {
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `name` - String parameter.
|
||||
/// * `filter` - serde_json::Value parameter.
|
||||
/// * `filter` - Filter parameter.
|
||||
///
|
||||
/// # Returns
|
||||
/// The count value.
|
||||
///
|
||||
/// # Errors
|
||||
/// Returns an error if the host function call fails.
|
||||
pub fn items(name: &str, filter: serde_json::Value) -> Result<i32, Error> {
|
||||
pub fn items(name: &str, filter: Filter) -> Result<i32, Error> {
|
||||
let response = unsafe {
|
||||
list_items(Json(ListItemsRequest {
|
||||
name: name.to_owned(),
|
||||
|
||||
@ -6,6 +6,12 @@
|
||||
use extism_pdk::*;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct Result {
|
||||
pub id: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct SearchFindRequest {
|
||||
@ -16,7 +22,7 @@ struct SearchFindRequest {
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct SearchFindResponse {
|
||||
#[serde(default)]
|
||||
results: Vec<serde_json::Value>,
|
||||
results: Vec<Result>,
|
||||
#[serde(default)]
|
||||
total: i32,
|
||||
#[serde(default)]
|
||||
@ -38,7 +44,7 @@ extern "ExtismHost" {
|
||||
///
|
||||
/// # Errors
|
||||
/// Returns an error if the host function call fails.
|
||||
pub fn find(query: &str) -> Result<(Vec<serde_json::Value>, i32), Error> {
|
||||
pub fn find(query: &str) -> Result<(Vec<Result>, i32), Error> {
|
||||
let response = unsafe {
|
||||
search_find(Json(SearchFindRequest {
|
||||
query: query.to_owned(),
|
||||
|
||||
@ -6,10 +6,17 @@
|
||||
use extism_pdk::*;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct Item {
|
||||
pub id: String,
|
||||
pub name: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct StoreSaveRequest {
|
||||
item: serde_json::Value,
|
||||
item: Item,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize)]
|
||||
@ -29,14 +36,14 @@ extern "ExtismHost" {
|
||||
/// Calls the store_save host function.
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `item` - serde_json::Value parameter.
|
||||
/// * `item` - Item parameter.
|
||||
///
|
||||
/// # Returns
|
||||
/// The id value.
|
||||
///
|
||||
/// # Errors
|
||||
/// Returns an error if the host function call fails.
|
||||
pub fn save(item: serde_json::Value) -> Result<String, Error> {
|
||||
pub fn save(item: Item) -> Result<String, Error> {
|
||||
let response = unsafe {
|
||||
store_save(Json(StoreSaveRequest {
|
||||
item: item,
|
||||
|
||||
@ -6,18 +6,25 @@
|
||||
use extism_pdk::*;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct User {
|
||||
pub id: String,
|
||||
pub name: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct UsersGetRequest {
|
||||
id: Option<String>,
|
||||
filter: Option<serde_json::Value>,
|
||||
filter: Option<User>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct UsersGetResponse {
|
||||
#[serde(default)]
|
||||
result: Option<serde_json::Value>,
|
||||
result: Option<User>,
|
||||
#[serde(default)]
|
||||
error: Option<String>,
|
||||
}
|
||||
@ -31,14 +38,14 @@ extern "ExtismHost" {
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `id` - Option<String> parameter.
|
||||
/// * `filter` - Option<serde_json::Value> parameter.
|
||||
/// * `filter` - Option<User> parameter.
|
||||
///
|
||||
/// # Returns
|
||||
/// The result value.
|
||||
///
|
||||
/// # Errors
|
||||
/// Returns an error if the host function call fails.
|
||||
pub fn get(id: Option<String>, filter: Option<serde_json::Value>) -> Result<Option<serde_json::Value>, Error> {
|
||||
pub fn get(id: Option<String>, filter: Option<User>) -> Result<Option<User>, Error> {
|
||||
let response = unsafe {
|
||||
users_get(Json(UsersGetRequest {
|
||||
id: id,
|
||||
|
||||
@ -12,6 +12,12 @@ RUST_PLUGINS := $(patsubst %/Cargo.toml,%,$(wildcard */Cargo.toml))
|
||||
TINYGO := $(shell command -v tinygo 2> /dev/null)
|
||||
EXTISM_PY := $(shell command -v extism-py 2> /dev/null)
|
||||
|
||||
# Allow building plugins without .ndp extension (e.g., make minimal instead of make minimal.ndp)
|
||||
.PHONY: $(PLUGINS) $(PYTHON_PLUGINS) $(RUST_PLUGINS)
|
||||
$(PLUGINS): %: %.ndp
|
||||
$(PYTHON_PLUGINS): %: %.ndp
|
||||
$(RUST_PLUGINS): %: %.ndp
|
||||
|
||||
# Default target: show available plugins
|
||||
.DEFAULT_GOAL := help
|
||||
|
||||
@ -26,12 +32,12 @@ help:
|
||||
@$(foreach p,$(RUST_PLUGINS),echo " $(p)";)
|
||||
@echo ""
|
||||
@echo "Usage:"
|
||||
@echo " make <plugin>.ndp Build a specific plugin (e.g., make $(firstword $(PLUGINS)).ndp)"
|
||||
@echo " make <plugin> Build a specific plugin (e.g., make $(firstword $(PLUGINS)))"
|
||||
@echo " make all Build all plugins"
|
||||
@echo " make all-go Build all Go plugins"
|
||||
@echo " make all-python Build all Python plugins (requires extism-py)"
|
||||
@echo " make all-rust Build all Rust plugins (requires cargo)"
|
||||
@echo " make clean Remove all built plugins"
|
||||
@echo " make clean Remove all built plugins (.ndp and .wasm files)"
|
||||
|
||||
all: all-go all-python all-rust
|
||||
|
||||
@ -44,7 +50,7 @@ all-rust: $(RUST_PLUGINS:%=%.ndp)
|
||||
clean:
|
||||
rm -f $(PLUGINS:%=%.ndp) $(PYTHON_PLUGINS:%=%.ndp) $(RUST_PLUGINS:%=%.ndp)
|
||||
rm -f $(PLUGINS:%=%.wasm) $(PYTHON_PLUGINS:%=%.wasm) $(RUST_PLUGINS:%=%.wasm)
|
||||
$(foreach p,$(RUST_PLUGINS),cd $(p) && cargo clean 2>/dev/null || true;)
|
||||
@$(foreach p,$(RUST_PLUGINS),(cd $(p) && cargo clean 2>/dev/null) || true;)
|
||||
|
||||
# Build .ndp package from .wasm and manifest.json
|
||||
# Go plugins
|
||||
@ -53,9 +59,10 @@ clean:
|
||||
@cp $< plugin.wasm
|
||||
zip -j $@ $*/manifest.json plugin.wasm
|
||||
@rm -f plugin.wasm
|
||||
@rm -f $<
|
||||
|
||||
%.wasm: %/*.go %/go.mod
|
||||
# Use secondary expansion to properly track all Go source files
|
||||
.SECONDEXPANSION:
|
||||
$(PLUGINS:%=%.wasm): %.wasm: $$(shell find % -name '*.go' 2>/dev/null) %/go.mod
|
||||
ifdef TINYGO
|
||||
cd $* && tinygo build -target wasip1 -buildmode=c-shared -o ../$@ .
|
||||
else
|
||||
|
||||
@ -11,5 +11,6 @@ crate-type = ["cdylib"]
|
||||
|
||||
[dependencies]
|
||||
extism-pdk = "1.2"
|
||||
nd-host = { path = "../../host/rust" }
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
serde_json = "1.0"
|
||||
|
||||
@ -55,18 +55,6 @@ Configure the inspection interval in the Navidrome UI (Settings → Plugins →
|
||||
|--------|------------------------------------------|--------------|
|
||||
| `cron` | Cron expression for inspection interval | `@every 1m` |
|
||||
|
||||
### Cron Expression Examples
|
||||
|
||||
| Expression | Description |
|
||||
|------------|-------------|
|
||||
| `@every 1m` | Every minute (default) |
|
||||
| `@every 5m` | Every 5 minutes |
|
||||
| `@every 1h` | Every hour |
|
||||
| `@hourly` | Every hour at minute 0 |
|
||||
| `@daily` | Every day at midnight |
|
||||
| `0 */6 * * *` | Every 6 hours |
|
||||
| `0 9 * * *` | Daily at 9:00 AM |
|
||||
|
||||
## Permissions
|
||||
|
||||
This plugin requires:
|
||||
|
||||
@ -1,8 +1,8 @@
|
||||
//! Library Inspector Plugin for Navidrome
|
||||
//!
|
||||
//! This plugin demonstrates how to use the Library host service in Rust.
|
||||
//! It periodically logs details about all music libraries and finds the largest
|
||||
//! file in the root of each library directory.
|
||||
//! This plugin demonstrates how to use the nd-host library for accessing Navidrome
|
||||
//! host services in Rust. It periodically logs details about all music libraries
|
||||
//! and finds the largest file in the root of each library directory.
|
||||
//!
|
||||
//! ## Configuration
|
||||
//!
|
||||
@ -13,59 +13,14 @@
|
||||
//! ```
|
||||
|
||||
use extism_pdk::*;
|
||||
use nd_host::{library, scheduler};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::fs;
|
||||
|
||||
// ============================================================================
|
||||
// Library Types
|
||||
// ============================================================================
|
||||
|
||||
#[derive(Deserialize, Debug)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[allow(dead_code)]
|
||||
struct Library {
|
||||
id: i32,
|
||||
name: String,
|
||||
#[serde(default)]
|
||||
path: Option<String>,
|
||||
#[serde(default)]
|
||||
mount_point: Option<String>,
|
||||
last_scan_at: i64,
|
||||
total_songs: i32,
|
||||
total_albums: i32,
|
||||
total_artists: i32,
|
||||
total_size: i64,
|
||||
total_duration: f64,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct LibraryGetAllLibrariesResponse {
|
||||
result: Option<Vec<Library>>,
|
||||
#[serde(default)]
|
||||
error: Option<String>,
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Scheduler Types
|
||||
// ============================================================================
|
||||
|
||||
#[derive(Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct SchedulerScheduleRecurringRequest {
|
||||
cron_expression: String,
|
||||
payload: String,
|
||||
schedule_id: String,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct SchedulerScheduleRecurringResponse {
|
||||
#[serde(default)]
|
||||
new_schedule_id: Option<String>,
|
||||
#[serde(default)]
|
||||
error: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct SchedulerCallbackInput {
|
||||
@ -84,54 +39,10 @@ struct InitOutput {
|
||||
error: Option<String>,
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Host Function Imports
|
||||
// ============================================================================
|
||||
|
||||
#[host_fn]
|
||||
extern "ExtismHost" {
|
||||
fn library_getalllibraries(input: Json<serde_json::Value>) -> Json<LibraryGetAllLibrariesResponse>;
|
||||
fn scheduler_schedulerecurring(input: Json<SchedulerScheduleRecurringRequest>) -> Json<SchedulerScheduleRecurringResponse>;
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Helper Functions
|
||||
// ============================================================================
|
||||
|
||||
/// Get all libraries from Navidrome
|
||||
fn get_all_libraries() -> Result<Vec<Library>, String> {
|
||||
let response: Json<LibraryGetAllLibrariesResponse> = unsafe {
|
||||
library_getalllibraries(Json(serde_json::json!({})))
|
||||
.map_err(|e| format!("Failed to call library_getalllibraries: {:?}", e))?
|
||||
};
|
||||
|
||||
if let Some(err) = response.0.error {
|
||||
return Err(err);
|
||||
}
|
||||
|
||||
Ok(response.0.result.unwrap_or_default())
|
||||
}
|
||||
|
||||
/// Schedule a recurring task
|
||||
fn schedule_recurring(cron: &str, payload: &str, id: &str) -> Result<String, String> {
|
||||
let request = SchedulerScheduleRecurringRequest {
|
||||
cron_expression: cron.to_string(),
|
||||
payload: payload.to_string(),
|
||||
schedule_id: id.to_string(),
|
||||
};
|
||||
|
||||
let response: Json<SchedulerScheduleRecurringResponse> = unsafe {
|
||||
scheduler_schedulerecurring(Json(request))
|
||||
.map_err(|e| format!("Failed to schedule task: {:?}", e))?
|
||||
};
|
||||
|
||||
if let Some(err) = response.0.error {
|
||||
return Err(err);
|
||||
}
|
||||
|
||||
Ok(response.0.new_schedule_id.unwrap_or_default())
|
||||
}
|
||||
|
||||
/// Format bytes into human-readable size
|
||||
fn format_size(bytes: i64) -> String {
|
||||
const KB: i64 = 1024;
|
||||
@ -209,7 +120,7 @@ fn find_largest_file(mount_point: &str) -> Option<(String, u64)> {
|
||||
fn inspect_libraries() {
|
||||
info!("=== Library Inspection Started ===");
|
||||
|
||||
let libraries = match get_all_libraries() {
|
||||
let libraries = match library::get_all_libraries() {
|
||||
Ok(libs) => libs,
|
||||
Err(e) => {
|
||||
error!("Failed to get libraries: {}", e);
|
||||
@ -234,10 +145,10 @@ fn inspect_libraries() {
|
||||
info!(" Duration: {}", format_duration(lib.total_duration));
|
||||
|
||||
// If we have filesystem access, find the largest file
|
||||
if let Some(mount_point) = &lib.mount_point {
|
||||
info!(" Mount: {}", mount_point);
|
||||
if !lib.mount_point.is_empty() {
|
||||
info!(" Mount: {}", lib.mount_point);
|
||||
|
||||
match find_largest_file(mount_point) {
|
||||
match find_largest_file(&lib.mount_point) {
|
||||
Some((name, size)) => {
|
||||
info!(
|
||||
" Largest file in root: {} ({})",
|
||||
@ -274,8 +185,8 @@ pub fn nd_on_init() -> FnResult<Json<InitOutput>> {
|
||||
|
||||
info!("Scheduling library inspection with cron: {}", cron);
|
||||
|
||||
// Schedule the recurring task
|
||||
match schedule_recurring(&cron, "inspect", "library-inspect") {
|
||||
// Schedule the recurring task using nd-host scheduler
|
||||
match scheduler::schedule_recurring(&cron, "inspect", "library-inspect") {
|
||||
Ok(schedule_id) => {
|
||||
info!("Scheduled inspection task with ID: {}", schedule_id);
|
||||
}
|
||||
|
||||
@ -26,31 +26,56 @@ nd-host = { path = "../../host/rust" }
|
||||
Then import the services you need:
|
||||
|
||||
```rust
|
||||
use nd_host::{cache, scheduler, kvstore};
|
||||
use nd_host::{cache, scheduler, library};
|
||||
use nd_host::library::Library; // Import the typed struct
|
||||
|
||||
#[plugin_fn]
|
||||
pub fn my_callback(input: String) -> FnResult<String> {
|
||||
// Use the cache service
|
||||
cache::cache_set("my_key", b"my_value", 3600)?;
|
||||
cache::set("my_key", b"my_value", 3600)?;
|
||||
|
||||
// Schedule a recurring task
|
||||
scheduler::scheduler_schedule_recurring("@every 5m", "payload", "task_id")?;
|
||||
scheduler::schedule_recurring("@every 5m", "payload", "task_id")?;
|
||||
|
||||
// Access library data with typed structs
|
||||
let libraries: Vec<Library> = library::get_all_libraries()?;
|
||||
for lib in &libraries {
|
||||
info!("Library: {} with {} songs", lib.name, lib.total_songs);
|
||||
}
|
||||
|
||||
Ok("done".to_string())
|
||||
}
|
||||
```
|
||||
|
||||
## Typed Structs
|
||||
|
||||
Services that work with domain objects provide typed Rust structs instead of
|
||||
`serde_json::Value`. This enables compile-time type checking and IDE
|
||||
autocompletion.
|
||||
|
||||
For example, the `library` module provides a `Library` struct:
|
||||
|
||||
```rust
|
||||
use nd_host::library::Library;
|
||||
|
||||
let libs: Vec<Library> = library::get_all_libraries()?;
|
||||
println!("First library: {} ({} songs)", libs[0].name, libs[0].total_songs);
|
||||
```
|
||||
|
||||
All structs derive `Debug`, `Clone`, `Serialize`, and `Deserialize` for
|
||||
convenient use with logging and serialization.
|
||||
|
||||
## Available Services
|
||||
|
||||
| Module | Description |
|
||||
|--------|-------------|
|
||||
| `artwork` | Access album and artist artwork |
|
||||
| `cache` | Temporary key-value storage with TTL |
|
||||
| `kvstore` | Persistent key-value storage |
|
||||
| `library` | Access the music library (albums, artists, tracks) |
|
||||
| `scheduler` | Schedule one-time and recurring tasks |
|
||||
| `subsonicapi` | Make Subsonic API calls |
|
||||
| `websocket` | Send real-time messages to clients |
|
||||
| Module | Description |
|
||||
|---------------|----------------------------------------------------|
|
||||
| `artwork` | Access album and artist artwork |
|
||||
| `cache` | Temporary key-value storage with TTL |
|
||||
| `kvstore` | Persistent key-value storage |
|
||||
| `library` | Access the music library (albums, artists, tracks) |
|
||||
| `scheduler` | Schedule one-time and recurring tasks |
|
||||
| `subsonicapi` | Make Subsonic API calls |
|
||||
| `websocket` | Send real-time messages to clients |
|
||||
|
||||
## Building Plugins
|
||||
|
||||
|
||||
@ -6,6 +6,24 @@
|
||||
use extism_pdk::*;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
/// Library represents a music library with metadata.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct Library {
|
||||
pub id: i32,
|
||||
pub name: String,
|
||||
#[serde(default)]
|
||||
pub path: String,
|
||||
#[serde(default)]
|
||||
pub mount_point: String,
|
||||
pub last_scan_at: i64,
|
||||
pub total_songs: i32,
|
||||
pub total_albums: i32,
|
||||
pub total_artists: i32,
|
||||
pub total_size: i64,
|
||||
pub total_duration: f64,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct LibraryGetLibraryRequest {
|
||||
@ -16,7 +34,7 @@ struct LibraryGetLibraryRequest {
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct LibraryGetLibraryResponse {
|
||||
#[serde(default)]
|
||||
result: Option<serde_json::Value>,
|
||||
result: Option<Library>,
|
||||
#[serde(default)]
|
||||
error: Option<String>,
|
||||
}
|
||||
@ -25,7 +43,7 @@ struct LibraryGetLibraryResponse {
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct LibraryGetAllLibrariesResponse {
|
||||
#[serde(default)]
|
||||
result: Vec<serde_json::Value>,
|
||||
result: Vec<Library>,
|
||||
#[serde(default)]
|
||||
error: Option<String>,
|
||||
}
|
||||
@ -51,7 +69,7 @@ extern "ExtismHost" {
|
||||
///
|
||||
/// # Errors
|
||||
/// Returns an error if the host function call fails.
|
||||
pub fn get_library(id: i32) -> Result<Option<serde_json::Value>, Error> {
|
||||
pub fn get_library(id: i32) -> Result<Option<Library>, Error> {
|
||||
let response = unsafe {
|
||||
library_getlibrary(Json(LibraryGetLibraryRequest {
|
||||
id: id,
|
||||
@ -74,7 +92,7 @@ pub fn get_library(id: i32) -> Result<Option<serde_json::Value>, Error> {
|
||||
///
|
||||
/// # Errors
|
||||
/// Returns an error if the host function call fails.
|
||||
pub fn get_all_libraries() -> Result<Vec<serde_json::Value>, Error> {
|
||||
pub fn get_all_libraries() -> Result<Vec<Library>, Error> {
|
||||
let response = unsafe {
|
||||
library_getalllibraries(Json(serde_json::json!({})))?
|
||||
};
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user