diff --git a/plugins/README.md b/plugins/README.md index 10eb34fbc..7dca3a5f2 100644 --- a/plugins/README.md +++ b/plugins/README.md @@ -401,7 +401,7 @@ import "github.com/navidrome/navidrome/plugins/pdk/go/host" ### HTTP -Make HTTP requests to external services, with timeouts, redirect control, and protection against reaching private network addresses. This is the only supported way to make HTTP requests: Extism's built-in HTTP (`pdk.NewHTTPRequest`) is disabled. +Make HTTP requests to external services, with timeouts, redirect control, and protection against reaching private network addresses. This is the only supported way to make HTTP requests: Extism's built-in HTTP is disabled (`pdk.NewHTTPRequest` in Go, `http::request` in Rust, `extism.Http.request` in Python, `Http.request` in JS). **Manifest permission:** @@ -416,6 +416,8 @@ Make HTTP requests to external services, with timeouts, redirect control, and pr } ``` +**Private addresses:** the check runs on the resolved IP when connecting. A named host entry (`api.example.com`, `*.example.com`) never authorizes a loopback, private or link-local address on its own, even if its DNS points there. To reach a service on the local network, also list its IP or a CIDR (`192.168.1.10`, `10.0.0.0/8`), or use `"*"` when the user configures the address. Without `requiredHosts`, only public addresses are allowed. + **Host functions:** | Function | Parameters | Returns | @@ -700,6 +702,8 @@ Establish persistent WebSocket connections to external services. Your plugin mus } ``` +`requiredHosts` is mandatory here: leave it out and every connection is blocked. Unlike HTTP, there is no fallback to public addresses. Entries follow the same [private address rules](#http). + **Host functions:** | Function | Parameters | Description | @@ -1242,6 +1246,8 @@ extism-py plugin.wasm -o plugin.wasm *.py zip -j my-plugin.ndp manifest.json plugin.wasm ``` +There is no Python PDK, so call host services directly: import them from the `extism:host/user` namespace (e.g. `http_send`) with `@extism.import_fn`, and exchange JSON through Extism memory. Each function takes a JSON request and returns a JSON response with an `error` field on failure. For HTTP, send `{"request": {"method": "GET", "url": "..."}}` and read `result.statusCode` and `result.body` (base64). See [coverartarchive-py](examples/coverartarchive-py/) and [nowplaying-py](examples/nowplaying-py/). + ### Using XTP CLI (Scaffolding) Bootstrap a new plugin from a schema: diff --git a/plugins/cmd/ndpgen/internal/generator_test.go b/plugins/cmd/ndpgen/internal/generator_test.go index ea94c7fc6..555906489 100644 --- a/plugins/cmd/ndpgen/internal/generator_test.go +++ b/plugins/cmd/ndpgen/internal/generator_test.go @@ -1790,6 +1790,33 @@ var _ = Describe("Rust Generation", func() { Expect(codeStr).NotTo(ContainSubstring("return args.Get(0).(*HTTPRequest)")) }) }) + + Describe("Deprecated PDK functions", func() { + symbols := &PDKSymbols{ + Functions: []PDKFunc{ + { + Name: "NewHTTPRequest", + Doc: "NewHTTPRequest returns a new `HTTPRequest`.", + Params: []PDKParam{{Name: "method", Type: "HTTPMethod"}, {Name: "url", Type: "string"}}, + Returns: []PDKReturn{{Type: "*HTTPRequest"}}, + Deprecated: "Use host.HTTPSend instead.", + }, + }, + } + + DescribeTable("emits a Deprecated paragraph after the doc line", + func(generate func(*PDKSymbols) ([]byte, error)) { + code, err := generate(symbols) + Expect(err).NotTo(HaveOccurred()) + _, err = format.Source(code) + Expect(err).NotTo(HaveOccurred()) + Expect(string(code)).To(ContainSubstring( + "// NewHTTPRequest NewHTTPRequest returns a new `HTTPRequest`.\n//\n// Deprecated: Use host.HTTPSend instead.\nfunc NewHTTPRequest(")) + }, + Entry("WASM wrapper", GeneratePDKGo), + Entry("native stub", GeneratePDKGoStub), + ) + }) }) }) diff --git a/plugins/cmd/ndpgen/internal/pdk_parser.go b/plugins/cmd/ndpgen/internal/pdk_parser.go index 4756334dd..7607fe881 100644 --- a/plugins/cmd/ndpgen/internal/pdk_parser.go +++ b/plugins/cmd/ndpgen/internal/pdk_parser.go @@ -50,6 +50,12 @@ type PDKFunc struct { Params []PDKParam Returns []PDKReturn IsVariadic bool + Deprecated string // Rendered as a "Deprecated:" paragraph when set +} + +// deprecatedPDKFuncs marks extism functions that do not work inside Navidrome, with what to use instead. +var deprecatedPDKFuncs = map[string]string{ + "NewHTTPRequest": "Navidrome does not enable extism's http_request host function, so every request sent this way fails. Use host.HTTPSend instead.", } // PDKParam represents a function parameter. @@ -156,6 +162,7 @@ func ParseExtismPDK() (*PDKSymbols, error) { t.Methods = append(t.Methods, fn) } } else { + fn.Deprecated = deprecatedPDKFuncs[fn.Name] symbols.Functions = append(symbols.Functions, fn) } } diff --git a/plugins/cmd/ndpgen/internal/templates/client.rs.tmpl b/plugins/cmd/ndpgen/internal/templates/client.rs.tmpl index f8b786849..786a50db5 100644 --- a/plugins/cmd/ndpgen/internal/templates/client.rs.tmpl +++ b/plugins/cmd/ndpgen/internal/templates/client.rs.tmpl @@ -11,7 +11,7 @@ use serde::{Deserialize, Serialize}; {{if .Doc}} {{rustDocComment .Doc}} {{else}} -{{end}}#[derive(Debug, Clone, Serialize, Deserialize)] +{{end}}#[derive(Debug, Clone, Default, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct {{.Name}} { {{- range .Fields}} diff --git a/plugins/cmd/ndpgen/internal/templates/pdk.go.tmpl b/plugins/cmd/ndpgen/internal/templates/pdk.go.tmpl index ebaf88df0..117bd9232 100644 --- a/plugins/cmd/ndpgen/internal/templates/pdk.go.tmpl +++ b/plugins/cmd/ndpgen/internal/templates/pdk.go.tmpl @@ -40,6 +40,10 @@ const ( {{- if .Doc}} // {{.Name}} {{firstSentence .Doc}} {{- end}} +{{- if .Deprecated}} +// +// Deprecated: {{.Deprecated}} +{{- end}} func {{.Name}}({{paramList .Params}}){{returnList .Returns}} { {{- if .Returns}} return extism.{{.Name}}({{argList .Params}}) diff --git a/plugins/cmd/ndpgen/internal/templates/pdk_stub.go.tmpl b/plugins/cmd/ndpgen/internal/templates/pdk_stub.go.tmpl index 2bd73f107..27d06faa0 100644 --- a/plugins/cmd/ndpgen/internal/templates/pdk_stub.go.tmpl +++ b/plugins/cmd/ndpgen/internal/templates/pdk_stub.go.tmpl @@ -31,6 +31,10 @@ func ResetMock() { {{- if .Doc}} // {{.Name}} {{firstSentence .Doc}} {{- end}} +{{- if .Deprecated}} +// +// Deprecated: {{.Deprecated}} +{{- end}} func {{.Name}}({{paramList .Params}}){{returnList .Returns}} { {{- if .Returns}} args := PDKMock.Called({{argList .Params}}) diff --git a/plugins/cmd/ndpgen/testdata/comprehensive_client_expected.rs b/plugins/cmd/ndpgen/testdata/comprehensive_client_expected.rs index 08dae2901..1bd953640 100644 --- a/plugins/cmd/ndpgen/testdata/comprehensive_client_expected.rs +++ b/plugins/cmd/ndpgen/testdata/comprehensive_client_expected.rs @@ -29,14 +29,14 @@ mod base64_bytes { } } -#[derive(Debug, Clone, Serialize, Deserialize)] +#[derive(Debug, Clone, Default, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct User2 { pub id: String, pub name: String, } -#[derive(Debug, Clone, Serialize, Deserialize)] +#[derive(Debug, Clone, Default, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct Filter2 { pub active: bool, diff --git a/plugins/cmd/ndpgen/testdata/list_client_expected.rs b/plugins/cmd/ndpgen/testdata/list_client_expected.rs index 9b54f7544..5227cadf4 100644 --- a/plugins/cmd/ndpgen/testdata/list_client_expected.rs +++ b/plugins/cmd/ndpgen/testdata/list_client_expected.rs @@ -6,7 +6,7 @@ use extism_pdk::*; use serde::{Deserialize, Serialize}; -#[derive(Debug, Clone, Serialize, Deserialize)] +#[derive(Debug, Clone, Default, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct Filter { pub active: bool, diff --git a/plugins/cmd/ndpgen/testdata/search_client_expected.rs b/plugins/cmd/ndpgen/testdata/search_client_expected.rs index b0ab2505a..396a22b4e 100644 --- a/plugins/cmd/ndpgen/testdata/search_client_expected.rs +++ b/plugins/cmd/ndpgen/testdata/search_client_expected.rs @@ -6,7 +6,7 @@ use extism_pdk::*; use serde::{Deserialize, Serialize}; -#[derive(Debug, Clone, Serialize, Deserialize)] +#[derive(Debug, Clone, Default, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct Result { pub id: String, diff --git a/plugins/cmd/ndpgen/testdata/store_client_expected.rs b/plugins/cmd/ndpgen/testdata/store_client_expected.rs index 25d2af2e0..3b9d5ac17 100644 --- a/plugins/cmd/ndpgen/testdata/store_client_expected.rs +++ b/plugins/cmd/ndpgen/testdata/store_client_expected.rs @@ -6,7 +6,7 @@ use extism_pdk::*; use serde::{Deserialize, Serialize}; -#[derive(Debug, Clone, Serialize, Deserialize)] +#[derive(Debug, Clone, Default, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct Item { pub id: String, diff --git a/plugins/cmd/ndpgen/testdata/users_client_expected.rs b/plugins/cmd/ndpgen/testdata/users_client_expected.rs index 40daa9cfd..4bac43ce8 100644 --- a/plugins/cmd/ndpgen/testdata/users_client_expected.rs +++ b/plugins/cmd/ndpgen/testdata/users_client_expected.rs @@ -6,7 +6,7 @@ use extism_pdk::*; use serde::{Deserialize, Serialize}; -#[derive(Debug, Clone, Serialize, Deserialize)] +#[derive(Debug, Clone, Default, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct User { pub id: String, diff --git a/plugins/examples/coverartarchive-py/plugin/__init__.py b/plugins/examples/coverartarchive-py/plugin/__init__.py index e6f4c6a07..c4a0480b5 100644 --- a/plugins/examples/coverartarchive-py/plugin/__init__.py +++ b/plugins/examples/coverartarchive-py/plugin/__init__.py @@ -12,16 +12,18 @@ import json @extism.import_fn("extism:host/user", "http_send") -def http_send(req: dict) -> dict: ... +def _http_send(offset: int) -> int: ... def http_get(url): """GET url via Navidrome's HTTP host service. Returns (status_code, body_bytes).""" - resp = http_send({"request": {"method": "GET", "url": url}}) + request = json.dumps({"request": {"method": "GET", "url": url}}).encode("utf-8") + response_offset = _http_send(extism.memory.alloc(request).offset) + resp = json.loads(extism.memory.string(extism.memory.find(response_offset))) if resp.get("error"): raise Exception(f"HTTP request failed: {resp['error']}") - result = resp.get("result") or {} - return result.get("statusCode", 0), base64.b64decode(result.get("body") or "") + result = resp["result"] + return result["statusCode"], base64.b64decode(result.get("body", "")) @extism.plugin_fn diff --git a/plugins/examples/discord-rich-presence-rs/src/rpc.rs b/plugins/examples/discord-rich-presence-rs/src/rpc.rs index d90cb4341..fa72f7466 100644 --- a/plugins/examples/discord-rich-presence-rs/src/rpc.rs +++ b/plugins/examples/discord-rich-presence-rs/src/rpc.rs @@ -369,9 +369,8 @@ fn send_http( method: method.into(), url: url.into(), headers, - no_follow_redirects: false, body, - timeout_ms: 0, + ..Default::default() })? .ok_or_else(|| Error::msg("empty HTTP response")) } diff --git a/plugins/examples/webhook-rs/src/lib.rs b/plugins/examples/webhook-rs/src/lib.rs index 794cda59c..5f5d972a0 100644 --- a/plugins/examples/webhook-rs/src/lib.rs +++ b/plugins/examples/webhook-rs/src/lib.rs @@ -94,10 +94,7 @@ impl Scrobbler for WebhookPlugin { let http_req = HTTPRequest { method: "GET".into(), url: full_url, - headers: Default::default(), - no_follow_redirects: false, - body: Vec::new(), - timeout_ms: 0, + ..Default::default() }; match http::send(http_req) { Ok(res) => { diff --git a/plugins/host_httpclient.go b/plugins/host_httpclient.go index 209f80eb0..54875ae8a 100644 --- a/plugins/host_httpclient.go +++ b/plugins/host_httpclient.go @@ -159,7 +159,7 @@ func (s *httpServiceImpl) validateHost(ctx context.Context, hostStr string) erro hostname := extractHostname(hostStr) if len(s.requiredHosts) > 0 { - if !s.isHostAllowed(hostname) { + if !isHostInAllowlist(s.requiredHosts, hostname) { return fmt.Errorf("host %q is not allowed", hostStr) } return nil @@ -177,10 +177,6 @@ func (s *httpServiceImpl) dialControl(_, address string, _ syscall.RawConn) erro return checkPrivateDial(s.requiredHosts, address) } -func (s *httpServiceImpl) isHostAllowed(hostname string) bool { - return isHostInAllowlist(s.requiredHosts, hostname) -} - // extractHostname returns the hostname portion of a host string, stripping // any port number and IPv6 brackets. It handles IPv6 addresses correctly // (e.g. "[::1]:8080" → "::1", "[::1]" → "::1"). diff --git a/plugins/host_netguard.go b/plugins/host_netguard.go index 8400446ed..2244fed1f 100644 --- a/plugins/host_netguard.go +++ b/plugins/host_netguard.go @@ -41,8 +41,7 @@ func isHostInAllowlist(requiredHosts []string, hostname string) bool { return false } -// ipMatchesEntry reports whether a requiredHosts entry is a literal IP or CIDR -// that covers ip. Hostname and wildcard entries never match. +// ipMatchesEntry reports whether a requiredHosts entry is a literal IP or CIDR that covers ip. func ipMatchesEntry(entry string, ip net.IP) bool { if _, cidr, err := net.ParseCIDR(entry); err == nil { return cidr.Contains(ip) diff --git a/plugins/manifest-schema.json b/plugins/manifest-schema.json index dae0fd937..056a1caed 100644 --- a/plugins/manifest-schema.json +++ b/plugins/manifest-schema.json @@ -134,7 +134,7 @@ }, "requiredHosts": { "type": "array", - "description": "List of required host patterns for HTTP requests (e.g., 'api.example.com', '*.musicbrainz.org')", + "description": "List of required host patterns for HTTP requests (e.g., 'api.example.com', '*.musicbrainz.org'). A named host alone can't reach a private address; also list an IP, a CIDR (e.g., '10.0.0.0/8') or '*' for that", "items": { "type": "string" } @@ -174,7 +174,7 @@ }, "requiredHosts": { "type": "array", - "description": "List of required host patterns for WebSocket connections (e.g., 'api.example.com', '*.musicbrainz.org')", + "description": "List of required host patterns for WebSocket connections (e.g., 'api.example.com', '*.musicbrainz.org'). Required: with no entries every connection is blocked. A named host alone can't reach a private address; also list an IP, a CIDR (e.g., '10.0.0.0/8') or '*' for that", "items": { "type": "string" } diff --git a/plugins/manifest_gen.go b/plugins/manifest_gen.go index 6f5c01596..b5e9afd96 100644 --- a/plugins/manifest_gen.go +++ b/plugins/manifest_gen.go @@ -51,7 +51,8 @@ type HTTPPermission struct { Reason *string `json:"reason,omitempty" yaml:"reason,omitempty" mapstructure:"reason,omitempty"` // List of required host patterns for HTTP requests (e.g., 'api.example.com', - // '*.musicbrainz.org') + // '*.musicbrainz.org'). A named host alone can't reach a private address; also + // list an IP, a CIDR (e.g., '10.0.0.0/8') or '*' for that RequiredHosts []string `json:"requiredHosts,omitempty" yaml:"requiredHosts,omitempty" mapstructure:"requiredHosts,omitempty"` } @@ -264,6 +265,8 @@ type WebSocketPermission struct { Reason *string `json:"reason,omitempty" yaml:"reason,omitempty" mapstructure:"reason,omitempty"` // List of required host patterns for WebSocket connections (e.g., - // 'api.example.com', '*.musicbrainz.org') + // 'api.example.com', '*.musicbrainz.org'). Required: with no entries every + // connection is blocked. A named host alone can't reach a private address; also + // list an IP, a CIDR (e.g., '10.0.0.0/8') or '*' for that RequiredHosts []string `json:"requiredHosts,omitempty" yaml:"requiredHosts,omitempty" mapstructure:"requiredHosts,omitempty"` } diff --git a/plugins/pdk/go/pdk/example_test.go b/plugins/pdk/go/pdk/example_test.go index 5678bddd4..bf98eba2f 100644 --- a/plugins/pdk/go/pdk/example_test.go +++ b/plugins/pdk/go/pdk/example_test.go @@ -8,6 +8,7 @@ package pdk_test import ( "testing" + "github.com/navidrome/navidrome/plugins/pdk/go/host" "github.com/navidrome/navidrome/plugins/pdk/go/pdk" "github.com/stretchr/testify/mock" ) @@ -136,48 +137,39 @@ func TestProcessJSONRequest(t *testing.T) { } // ============================================================================= -// Examples using stub types (Memory, HTTPRequest, HTTPResponse) +// HTTP requests go through the host HTTP service (host.HTTPSend), not +// pdk.NewHTTPRequest, which Navidrome does not enable. // ============================================================================= // FetchData demonstrates a plugin function that makes an HTTP request. func FetchData(url string) ([]byte, error) { - // Create and configure the HTTP request - // Note: SetHeader and SetBody work directly on the stub - no mocking needed! - req := pdk.NewHTTPRequest(pdk.MethodGet, url) - req.SetHeader("Accept", "application/json") - req.SetHeader("User-Agent", "MyPlugin/1.0") - - // Send the request - this is mocked because it requires host interaction - resp := req.Send() - - // Check status - works directly on the stub - if resp.Status() != 200 { - return nil, nil + resp, err := host.HTTPSend(host.HTTPRequest{ + Method: "GET", + URL: url, + Headers: map[string]string{ + "Accept": "application/json", + "User-Agent": "MyPlugin/1.0", + }, + }) + if err != nil { + return nil, err } - // Return body - works directly on the stub - return resp.Body(), nil + if resp.StatusCode != 200 { + return nil, nil + } + return resp.Body, nil } func TestFetchData(t *testing.T) { - pdk.ResetMock() + host.HTTPMock.ExpectedCalls = nil - // Create a stub response with test data expectedBody := []byte(`{"result": "success"}`) - stubResponse := pdk.NewStubHTTPResponse(200, map[string]string{ - "Content-Type": "application/json", - }, expectedBody) + host.HTTPMock.On("Send", mock.MatchedBy(func(req host.HTTPRequest) bool { + return req.Method == "GET" && req.URL == "https://api.example.com/data" && + req.Headers["Accept"] == "application/json" + })).Return(&host.HTTPResponse{StatusCode: 200, Body: expectedBody}, nil) - // Mock NewHTTPRequest to return a real HTTPRequest struct - // The struct methods (SetHeader, SetBody) work without mocking - pdk.PDKMock.On("NewHTTPRequest", pdk.MethodGet, "https://api.example.com/data"). - Return(&pdk.HTTPRequest{}) - - // Mock Send to return our stub response - pdk.PDKMock.On("Send", mock.AnythingOfType("*pdk.HTTPRequest")). - Return(stubResponse) - - // Call the function body, err := FetchData("https://api.example.com/data") if err != nil { @@ -188,21 +180,15 @@ func TestFetchData(t *testing.T) { t.Errorf("expected body %q, got %q", expectedBody, body) } - pdk.PDKMock.AssertExpectations(t) + host.HTTPMock.AssertExpectations(t) } func TestFetchData_NonOKStatus(t *testing.T) { - pdk.ResetMock() + host.HTTPMock.ExpectedCalls = nil - // Create a stub response with 404 status - stubResponse := pdk.NewStubHTTPResponse(404, nil, []byte("Not Found")) + host.HTTPMock.On("Send", mock.Anything). + Return(&host.HTTPResponse{StatusCode: 404, Body: []byte("Not Found")}, nil) - pdk.PDKMock.On("NewHTTPRequest", pdk.MethodGet, "https://api.example.com/missing"). - Return(&pdk.HTTPRequest{}) - pdk.PDKMock.On("Send", mock.AnythingOfType("*pdk.HTTPRequest")). - Return(stubResponse) - - // Call the function body, err := FetchData("https://api.example.com/missing") if err != nil { @@ -214,7 +200,7 @@ func TestFetchData_NonOKStatus(t *testing.T) { t.Errorf("expected nil body for 404, got %q", body) } - pdk.PDKMock.AssertExpectations(t) + host.HTTPMock.AssertExpectations(t) } // ProcessMemoryData demonstrates working with Memory type. @@ -292,23 +278,27 @@ func TestHTTPMethodString(t *testing.T) { // PostJSON demonstrates a more complex HTTP request with body. func PostJSON(url string, data []byte) (int, error) { - req := pdk.NewHTTPRequest(pdk.MethodPost, url) - req.SetHeader("Content-Type", "application/json") - req.SetBody(data) // Works directly on stub - - resp := req.Send() // This is mocked - return int(resp.Status()), nil + resp, err := host.HTTPSend(host.HTTPRequest{ + Method: "POST", + URL: url, + Headers: map[string]string{"Content-Type": "application/json"}, + Body: data, + }) + if err != nil { + return 0, err + } + return int(resp.StatusCode), nil } func TestPostJSON(t *testing.T) { - pdk.ResetMock() + host.HTTPMock.ExpectedCalls = nil - stubResponse := pdk.NewStubHTTPResponse(201, nil, nil) - - pdk.PDKMock.On("NewHTTPRequest", pdk.MethodPost, "https://api.example.com/items"). - Return(&pdk.HTTPRequest{}) - pdk.PDKMock.On("Send", mock.AnythingOfType("*pdk.HTTPRequest")). - Return(stubResponse) + host.HTTPMock.On("Send", host.HTTPRequest{ + Method: "POST", + URL: "https://api.example.com/items", + Headers: map[string]string{"Content-Type": "application/json"}, + Body: []byte(`{"name":"test"}`), + }).Return(&host.HTTPResponse{StatusCode: 201}, nil) status, err := PostJSON("https://api.example.com/items", []byte(`{"name":"test"}`)) @@ -320,5 +310,5 @@ func TestPostJSON(t *testing.T) { t.Errorf("expected status 201, got %d", status) } - pdk.PDKMock.AssertExpectations(t) + host.HTTPMock.AssertExpectations(t) } diff --git a/plugins/pdk/go/pdk/pdk.go b/plugins/pdk/go/pdk/pdk.go index 35394d700..456a238f5 100644 --- a/plugins/pdk/go/pdk/pdk.go +++ b/plugins/pdk/go/pdk/pdk.go @@ -111,6 +111,8 @@ func LogMemory(level LogLevel, m Memory) { } // NewHTTPRequest NewHTTPRequest returns a new `HTTPRequest`. +// +// Deprecated: Navidrome does not enable extism's http_request host function, so every request sent this way fails. Use host.HTTPSend instead. func NewHTTPRequest(method HTTPMethod, url string) *HTTPRequest { return extism.NewHTTPRequest(method, url) } diff --git a/plugins/pdk/go/pdk/pdk_stub.go b/plugins/pdk/go/pdk/pdk_stub.go index 8f2707d56..1a685eecc 100644 --- a/plugins/pdk/go/pdk/pdk_stub.go +++ b/plugins/pdk/go/pdk/pdk_stub.go @@ -114,6 +114,8 @@ func LogMemory(level LogLevel, m Memory) { } // NewHTTPRequest NewHTTPRequest returns a new `HTTPRequest`. +// +// Deprecated: Navidrome does not enable extism's http_request host function, so every request sent this way fails. Use host.HTTPSend instead. func NewHTTPRequest(method HTTPMethod, url string) *HTTPRequest { args := PDKMock.Called(method, url) var r0 *HTTPRequest diff --git a/plugins/pdk/rust/nd-pdk-host/src/nd_host_http.rs b/plugins/pdk/rust/nd-pdk-host/src/nd_host_http.rs index 1c44cd2f3..f1607112c 100644 --- a/plugins/pdk/rust/nd-pdk-host/src/nd_host_http.rs +++ b/plugins/pdk/rust/nd-pdk-host/src/nd_host_http.rs @@ -30,7 +30,7 @@ mod base64_bytes { } /// HTTPRequest represents an outbound HTTP request from a plugin. -#[derive(Debug, Clone, Serialize, Deserialize)] +#[derive(Debug, Clone, Default, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct HTTPRequest { pub method: String, @@ -47,7 +47,7 @@ pub struct HTTPRequest { } /// HTTPResponse represents the response from an outbound HTTP request. -#[derive(Debug, Clone, Serialize, Deserialize)] +#[derive(Debug, Clone, Default, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct HTTPResponse { pub status_code: i32, diff --git a/plugins/pdk/rust/nd-pdk-host/src/nd_host_library.rs b/plugins/pdk/rust/nd-pdk-host/src/nd_host_library.rs index b4b9b3fb0..966c1607f 100644 --- a/plugins/pdk/rust/nd-pdk-host/src/nd_host_library.rs +++ b/plugins/pdk/rust/nd-pdk-host/src/nd_host_library.rs @@ -7,7 +7,7 @@ use extism_pdk::*; use serde::{Deserialize, Serialize}; /// Library represents a music library with metadata. -#[derive(Debug, Clone, Serialize, Deserialize)] +#[derive(Debug, Clone, Default, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct Library { pub id: i32, diff --git a/plugins/pdk/rust/nd-pdk-host/src/nd_host_matcher.rs b/plugins/pdk/rust/nd-pdk-host/src/nd_host_matcher.rs index be2819257..7fdf88b7f 100644 --- a/plugins/pdk/rust/nd-pdk-host/src/nd_host_matcher.rs +++ b/plugins/pdk/rust/nd-pdk-host/src/nd_host_matcher.rs @@ -7,7 +7,7 @@ use extism_pdk::*; use serde::{Deserialize, Serialize}; /// MatchOptions carries optional parameters for a match request. -#[derive(Debug, Clone, Serialize, Deserialize)] +#[derive(Debug, Clone, Default, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct MatchOptions { #[serde(default)] diff --git a/plugins/pdk/rust/nd-pdk-host/src/nd_host_scrobbleretriever.rs b/plugins/pdk/rust/nd-pdk-host/src/nd_host_scrobbleretriever.rs index fe80d98f6..19010924a 100644 --- a/plugins/pdk/rust/nd-pdk-host/src/nd_host_scrobbleretriever.rs +++ b/plugins/pdk/rust/nd-pdk-host/src/nd_host_scrobbleretriever.rs @@ -7,7 +7,7 @@ use extism_pdk::*; use serde::{Deserialize, Serialize}; /// ScrobbleCountOptions carries optional parameters for counting user scrobbles -#[derive(Debug, Clone, Serialize, Deserialize)] +#[derive(Debug, Clone, Default, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct ScrobbleCountOptions { #[serde(default)] @@ -17,7 +17,7 @@ pub struct ScrobbleCountOptions { } /// ScrobbleOptions carries optional parameters for retrieving user scrobbles -#[derive(Debug, Clone, Serialize, Deserialize)] +#[derive(Debug, Clone, Default, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct ScrobbleOptions { #[serde(default)] @@ -31,7 +31,7 @@ pub struct ScrobbleOptions { } /// ScrobbleRef represents one instance of a scrobble (instance id, file id, submission time) -#[derive(Debug, Clone, Serialize, Deserialize)] +#[derive(Debug, Clone, Default, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct ScrobbleRef { pub id: i64, diff --git a/plugins/pdk/rust/nd-pdk-host/src/nd_host_task.rs b/plugins/pdk/rust/nd-pdk-host/src/nd_host_task.rs index 4f43e165c..ac82fdd08 100644 --- a/plugins/pdk/rust/nd-pdk-host/src/nd_host_task.rs +++ b/plugins/pdk/rust/nd-pdk-host/src/nd_host_task.rs @@ -30,7 +30,7 @@ mod base64_bytes { } /// QueueConfig holds configuration for a task queue. -#[derive(Debug, Clone, Serialize, Deserialize)] +#[derive(Debug, Clone, Default, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct QueueConfig { pub concurrency: i32, @@ -41,7 +41,7 @@ pub struct QueueConfig { } /// TaskInfo holds the current state of a task. -#[derive(Debug, Clone, Serialize, Deserialize)] +#[derive(Debug, Clone, Default, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct TaskInfo { pub status: String, diff --git a/plugins/pdk/rust/nd-pdk-host/src/nd_host_users.rs b/plugins/pdk/rust/nd-pdk-host/src/nd_host_users.rs index faa795bb9..b31b39fb4 100644 --- a/plugins/pdk/rust/nd-pdk-host/src/nd_host_users.rs +++ b/plugins/pdk/rust/nd-pdk-host/src/nd_host_users.rs @@ -8,7 +8,7 @@ use serde::{Deserialize, Serialize}; /// User represents a Navidrome user with minimal information exposed to plugins. /// Sensitive fields like password, email, and internal IDs are intentionally excluded. -#[derive(Debug, Clone, Serialize, Deserialize)] +#[derive(Debug, Clone, Default, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct User { pub user_name: String,