Files
navidrome/plugins/cmd/ndpgen/internal/templates/capability.rs.tmpl
T
Deluan Quintão bd8032b327 fix(plugins): add base64 handling for []byte and remove raw=true (#5121)
* fix(plugins): add base64 handling for []byte and remove raw=true

Go's json.Marshal automatically base64-encodes []byte fields, but Rust's
serde_json serializes Vec<u8> as a JSON array and Python's json.dumps
raises TypeError on bytes. This fixes both directions of plugin
communication by adding proper base64 encoding/decoding in generated
client code.

For Rust templates (client and capability): adds a base64_bytes serde
helper module with #[serde(with = "base64_bytes")] on all Vec<u8> fields,
and adds base64 as a dependency. For Python templates: wraps bytes params
with base64.b64encode() and responses with base64.b64decode().

Also removes the raw=true binary framing protocol from all templates,
the parser, and the Method type. The raw mechanism added complexity that
is no longer needed once []byte works properly over JSON.

* fix(plugins): update production code and tests for base64 migration

Remove raw=true annotation from SubsonicAPI.CallRaw, delete all raw
test fixtures, remove raw-related test cases from parser, generator, and
integration tests, and add new test cases validating base64 handling
for Rust and Python templates.

* fix(plugins): update golden files and regenerate production code

Update golden test fixtures for codec and comprehensive services to
include base64 handling for []byte fields. Regenerate all production
PDK code (Go, Rust, Python) and host wrappers to use standard JSON
with base64-encoded byte fields instead of binary framing protocol.

* refactor: remove base64 helper duplication from rust template

Signed-off-by: Deluan <deluan@navidrome.org>

* fix(plugins): add base64 dependency to capabilities' Cargo.toml

Signed-off-by: Deluan <deluan@navidrome.org>

---------

Signed-off-by: Deluan <deluan@navidrome.org>
2026-02-27 19:00:19 -05:00

203 lines
7.2 KiB
Cheetah

// Code generated by ndpgen. DO NOT EDIT.
//
// This file contains export wrappers for the {{.Capability.Interface}} capability.
// It is intended for use in Navidrome plugins built with extism-pdk.
{{if .Capability.Structs}}
use serde::{Deserialize, Serialize};
{{- if hasHashMap .Capability}}
use std::collections::HashMap;
{{- end}}
{{- if .Capability.HasByteFields}}{{template "base64_bytes_module" .}}{{- end}}
// Helper functions for skip_serializing_if with numeric types
#[allow(dead_code)]
fn is_zero_i32(value: &i32) -> bool { *value == 0 }
#[allow(dead_code)]
fn is_zero_u32(value: &u32) -> bool { *value == 0 }
#[allow(dead_code)]
fn is_zero_i64(value: &i64) -> bool { *value == 0 }
#[allow(dead_code)]
fn is_zero_u64(value: &u64) -> bool { *value == 0 }
#[allow(dead_code)]
fn is_zero_f32(value: &f32) -> bool { *value == 0.0 }
#[allow(dead_code)]
fn is_zero_f64(value: &f64) -> bool { *value == 0.0 }
{{- end}}
{{- /* Generate type alias definitions */ -}}
{{- range .Capability.TypeAliases}}
{{- if .Doc}}
{{rustDocComment .Doc}}
{{- end}}
pub type {{.Name}} = {{rustTypeAlias .Type}};
{{- end}}
{{- /* Generate const definitions */ -}}
{{- range .Capability.Consts}}
{{- if .Values}}
{{- $type := .Type}}
{{- range $i, $v := .Values}}
{{- if $v.Doc}}
{{rustDocComment $v.Doc}}
{{- end}}
{{- /* Use the type alias name if a named type is provided, otherwise use &'static str */ -}}
{{- if $type}}
pub const {{rustConstName $v.Name}}: {{$type}} = {{$v.Value}};
{{- else}}
pub const {{rustConstName $v.Name}}: &'static str = {{$v.Value}};
{{- end}}
{{- end}}
{{- end}}
{{- end}}
{{- /* Generate struct definitions */ -}}
{{- range .Capability.Structs}}
{{- if .Doc}}
{{rustDocComment .Doc}}
{{- else}}
/// {{.Name}} represents the {{.Name}} data structure.
{{- end}}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct {{.Name}} {
{{- range .Fields}}
{{- if .Doc}}
{{rustDocComment .Doc | indent 4}}
{{- end}}
{{- if .OmitEmpty}}
#[serde(default, skip_serializing_if = "{{skipSerializingFunc .Type}}")]
{{- else}}
#[serde(default)]
{{- end}}
{{- if .IsByteSlice}}
#[serde(with = "base64_bytes")]
{{- end}}
pub {{rustFieldName .Name}}: {{fieldRustType .}},
{{- end}}
}
{{- end}}
/// Error represents an error from a capability method.
#[derive(Debug)]
pub struct Error {
pub message: String,
}
impl std::fmt::Display for Error {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.message)
}
}
impl std::error::Error for Error {}
impl Error {
pub fn new(message: impl Into<String>) -> Self {
Self { message: message.into() }
}
}
{{- /* Generate main interface based on required flag */ -}}
{{if .Capability.Required}}
/// {{agentName .Capability}} requires all methods to be implemented.
{{- if .Capability.Doc}}
{{rustDocComment .Capability.Doc}}
{{- end}}
pub trait {{agentName .Capability}} {
{{- range .Capability.Methods}}
/// {{.Name}}{{if .Doc}} - {{.Doc}}{{end}}
{{- if and .HasInput .HasOutput}}
fn {{rustMethodName .Name}}(&self, req: {{rustOutputType .Input.Type}}) -> Result<{{rustOutputType .Output.Type}}, Error>;
{{- else if .HasInput}}
fn {{rustMethodName .Name}}(&self, req: {{rustOutputType .Input.Type}}) -> Result<(), Error>;
{{- else if .HasOutput}}
fn {{rustMethodName .Name}}(&self) -> Result<{{rustOutputType .Output.Type}}, Error>;
{{- else}}
fn {{rustMethodName .Name}}(&self) -> Result<(), Error>;
{{- end}}
{{- end}}
}
/// Register all exports for the {{agentName .Capability}} capability.
/// This macro generates the WASM export functions for all trait methods.
#[macro_export]
macro_rules! register_{{snakeCase .Package}} {
($plugin_type:ty) => {
{{- range .Capability.Methods}}
#[extism_pdk::plugin_fn]
pub fn {{.ExportName}}(
{{- if .HasInput}}
req: extism_pdk::Json<$crate::{{snakeCase $.Package}}::{{rustOutputType .Input.Type}}>
{{- end}}
) -> extism_pdk::FnResult<{{if .HasOutput}}extism_pdk::Json<{{if isPrimitiveRust .Output.Type}}{{rustOutputType .Output.Type}}{{else}}$crate::{{snakeCase $.Package}}::{{rustOutputType .Output.Type}}{{end}}>{{else}}(){{end}}> {
let plugin = <$plugin_type>::default();
{{- if and .HasInput .HasOutput}}
let result = $crate::{{snakeCase $.Package}}::{{agentName $.Capability}}::{{rustMethodName .Name}}(&plugin, req.into_inner())?;
Ok(extism_pdk::Json(result))
{{- else if .HasInput}}
$crate::{{snakeCase $.Package}}::{{agentName $.Capability}}::{{rustMethodName .Name}}(&plugin, req.into_inner())?;
Ok(())
{{- else if .HasOutput}}
let result = $crate::{{snakeCase $.Package}}::{{agentName $.Capability}}::{{rustMethodName .Name}}(&plugin)?;
Ok(extism_pdk::Json(result))
{{- else}}
$crate::{{snakeCase $.Package}}::{{agentName $.Capability}}::{{rustMethodName .Name}}(&plugin)?;
Ok(())
{{- end}}
}
{{- end}}
};
}
{{- else}}
{{- /* Generate optional provider interfaces for non-required capabilities */ -}}
{{- range .Capability.Methods}}
/// {{providerInterface .}} provides the {{.Name}} function.
pub trait {{providerInterface .}} {
{{- if and .HasInput .HasOutput}}
fn {{rustMethodName .Name}}(&self, req: {{rustOutputType .Input.Type}}) -> Result<{{rustOutputType .Output.Type}}, Error>;
{{- else if .HasInput}}
fn {{rustMethodName .Name}}(&self, req: {{rustOutputType .Input.Type}}) -> Result<(), Error>;
{{- else if .HasOutput}}
fn {{rustMethodName .Name}}(&self) -> Result<{{rustOutputType .Output.Type}}, Error>;
{{- else}}
fn {{rustMethodName .Name}}(&self) -> Result<(), Error>;
{{- end}}
}
/// Register the {{rustMethodName .Name}} export.
/// This macro generates the WASM export function for this method.
#[macro_export]
macro_rules! {{registerMacroName .Name}} {
($plugin_type:ty) => {
#[extism_pdk::plugin_fn]
pub fn {{.ExportName}}(
{{- if .HasInput}}
req: extism_pdk::Json<$crate::{{snakeCase $.Package}}::{{rustOutputType .Input.Type}}>
{{- end}}
) -> extism_pdk::FnResult<{{if .HasOutput}}extism_pdk::Json<{{if isPrimitiveRust .Output.Type}}{{rustOutputType .Output.Type}}{{else}}$crate::{{snakeCase $.Package}}::{{rustOutputType .Output.Type}}{{end}}>{{else}}(){{end}}> {
let plugin = <$plugin_type>::default();
{{- if and .HasInput .HasOutput}}
let result = $crate::{{snakeCase $.Package}}::{{providerInterface .}}::{{rustMethodName .Name}}(&plugin, req.into_inner())?;
Ok(extism_pdk::Json(result))
{{- else if .HasInput}}
$crate::{{snakeCase $.Package}}::{{providerInterface .}}::{{rustMethodName .Name}}(&plugin, req.into_inner())?;
Ok(())
{{- else if .HasOutput}}
let result = $crate::{{snakeCase $.Package}}::{{providerInterface .}}::{{rustMethodName .Name}}(&plugin)?;
Ok(extism_pdk::Json(result))
{{- else}}
$crate::{{snakeCase $.Package}}::{{providerInterface .}}::{{rustMethodName .Name}}(&plugin)?;
Ok(())
{{- end}}
}
};
}
{{- end}}
{{- end}}