Files
navidrome/plugins/cmd/ndpgen/internal/templates/client.py.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

112 lines
3.2 KiB
Cheetah

# Code generated by ndpgen. 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-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
{{- if .Service.HasByteFields}}
import base64
{{- end}}
class HostFunctionError(Exception):
"""Raised when a host function returns an error."""
pass
{{- /* Generate raw host function imports */ -}}
{{range .Service.Methods}}
@extism.import_fn("extism:host/user", "{{exportName .}}")
def _{{exportName .}}(offset: int) -> int:
"""Raw host function - do not call directly."""
...
{{- end}}
{{- /* Generate dataclasses for multi-value returns */ -}}
{{range .Service.Methods}}
{{- if .NeedsResultClass}}
@dataclass
class {{pythonResultType .}}:
"""Result type for {{pythonFunc .}}."""
{{- range .Returns}}
{{.PythonName}}: {{.PythonType}}
{{- end}}
{{- end}}
{{- end}}
{{- /* Generate wrapper functions */ -}}
{{range .Service.Methods}}
def {{pythonFunc .}}({{range $i, $p := .Params}}{{if $i}}, {{end}}{{$p.PythonName}}: {{$p.PythonType}}{{end}}){{if .NeedsResultClass}} -> {{pythonResultType .}}{{else if .HasReturns}} -> {{(index .Returns 0).PythonType}}{{else}} -> None{{end}}:
"""{{if .Doc}}{{.Doc}}{{else}}Call the {{exportName .}} host function.{{end}}
{{- if .HasParams}}
Args:
{{- range .Params}}
{{.PythonName}}: {{.PythonType}} parameter.
{{- end}}
{{- end}}
{{- if .HasReturns}}
Returns:
{{- if .NeedsResultClass}}
{{pythonResultType .}} containing{{range .Returns}} {{.PythonName}},{{end}}.
{{- else}}
{{(index .Returns 0).PythonType}}: The result value.
{{- end}}
{{- end}}
Raises:
HostFunctionError: If the host function returns an error.
"""
{{- if .HasParams}}
request = {
{{- range .Params}}
{{- if .IsByteSlice}}
"{{.JSONName}}": base64.b64encode({{.PythonName}}).decode("ascii"),
{{- else}}
"{{.JSONName}}": {{.PythonName}},
{{- end}}
{{- end}}
}
request_bytes = json.dumps(request).encode("utf-8")
{{- else}}
request_bytes = b"{}"
{{- end}}
request_mem = extism.memory.alloc(request_bytes)
response_offset = _{{exportName .}}(request_mem.offset)
response_mem = extism.memory.find(response_offset)
response = json.loads(extism.memory.string(response_mem))
{{if .HasError}}
if response.get("error"):
raise HostFunctionError(response["error"])
{{end}}
{{- if .NeedsResultClass}}
return {{pythonResultType .}}(
{{- range .Returns}}
{{- if .IsByteSlice}}
{{.PythonName}}=base64.b64decode(response.get("{{.JSONName}}", "")),
{{- else}}
{{.PythonName}}=response.get("{{.JSONName}}"{{pythonDefault .}}),
{{- end}}
{{- end}}
)
{{- else if .HasReturns}}
{{- if (index .Returns 0).IsByteSlice}}
return base64.b64decode(response.get("{{(index .Returns 0).JSONName}}", ""))
{{- else}}
return response.get("{{(index .Returns 0).JSONName}}"{{pythonDefault (index .Returns 0)}})
{{- end}}
{{- end}}
{{- end}}