mirror of
https://github.com/navidrome/navidrome.git
synced 2026-08-31 07:30:32 +00:00
feat: implement IsOptionPattern method for better return type handling in Rust PDK generation
Signed-off-by: Deluan <deluan@navidrome.org>
This commit is contained in:
parent
f4477fffd4
commit
38c121c78c
@ -279,6 +279,9 @@ type ServiceB interface {
|
||||
|
||||
Entry("bytes",
|
||||
"codec_service.go.txt", "codec_client_expected.go.txt", "codec_client_expected.py", "codec_client_expected.rs"),
|
||||
|
||||
Entry("option pattern (value, exists bool)",
|
||||
"config_service.go.txt", "config_client_expected.go.txt", "config_client_expected.py", "config_client_expected.rs"),
|
||||
)
|
||||
|
||||
It("generates compilable client code for comprehensive service", func() {
|
||||
|
||||
@ -315,6 +315,109 @@ var _ = Describe("Generator", func() {
|
||||
})
|
||||
})
|
||||
|
||||
Describe("Method.IsOptionPattern", func() {
|
||||
It("should return true for (value, exists bool) pattern", func() {
|
||||
m := Method{
|
||||
Returns: []Param{
|
||||
{Name: "value", Type: "string"},
|
||||
{Name: "exists", Type: "bool"},
|
||||
},
|
||||
}
|
||||
Expect(m.IsOptionPattern()).To(BeTrue())
|
||||
})
|
||||
|
||||
It("should return true for (value, ok bool) pattern", func() {
|
||||
m := Method{
|
||||
Returns: []Param{
|
||||
{Name: "value", Type: "int64"},
|
||||
{Name: "ok", Type: "bool"},
|
||||
},
|
||||
}
|
||||
Expect(m.IsOptionPattern()).To(BeTrue())
|
||||
})
|
||||
|
||||
It("should return true for (value, found bool) pattern", func() {
|
||||
m := Method{
|
||||
Returns: []Param{
|
||||
{Name: "data", Type: "[]byte"},
|
||||
{Name: "found", Type: "bool"},
|
||||
},
|
||||
}
|
||||
Expect(m.IsOptionPattern()).To(BeTrue())
|
||||
})
|
||||
|
||||
It("should be case insensitive for bool name", func() {
|
||||
m := Method{
|
||||
Returns: []Param{
|
||||
{Name: "value", Type: "string"},
|
||||
{Name: "EXISTS", Type: "bool"},
|
||||
},
|
||||
}
|
||||
Expect(m.IsOptionPattern()).To(BeTrue())
|
||||
})
|
||||
|
||||
It("should return false for single return", func() {
|
||||
m := Method{
|
||||
Returns: []Param{
|
||||
{Name: "value", Type: "string"},
|
||||
},
|
||||
}
|
||||
Expect(m.IsOptionPattern()).To(BeFalse())
|
||||
})
|
||||
|
||||
It("should return false for more than two returns", func() {
|
||||
m := Method{
|
||||
Returns: []Param{
|
||||
{Name: "value", Type: "string"},
|
||||
{Name: "count", Type: "int"},
|
||||
{Name: "exists", Type: "bool"},
|
||||
},
|
||||
}
|
||||
Expect(m.IsOptionPattern()).To(BeFalse())
|
||||
})
|
||||
|
||||
It("should return false when second return is not bool", func() {
|
||||
m := Method{
|
||||
Returns: []Param{
|
||||
{Name: "value", Type: "string"},
|
||||
{Name: "count", Type: "int"},
|
||||
},
|
||||
}
|
||||
Expect(m.IsOptionPattern()).To(BeFalse())
|
||||
})
|
||||
|
||||
It("should return false when bool is not named exists/ok/found", func() {
|
||||
m := Method{
|
||||
Returns: []Param{
|
||||
{Name: "value", Type: "string"},
|
||||
{Name: "success", Type: "bool"},
|
||||
},
|
||||
}
|
||||
Expect(m.IsOptionPattern()).To(BeFalse())
|
||||
})
|
||||
|
||||
It("should return false for Has() pattern where first return is bool", func() {
|
||||
// Has(key) -> (exists bool) should NOT be treated as Option pattern
|
||||
m := Method{
|
||||
Returns: []Param{
|
||||
{Name: "exists", Type: "bool"},
|
||||
},
|
||||
}
|
||||
Expect(m.IsOptionPattern()).To(BeFalse())
|
||||
})
|
||||
|
||||
It("should return false when first return is bool (preserves Has-like methods)", func() {
|
||||
// Even with two returns, if first is bool, don't convert to Option<bool>
|
||||
m := Method{
|
||||
Returns: []Param{
|
||||
{Name: "result", Type: "bool"},
|
||||
{Name: "exists", Type: "bool"},
|
||||
},
|
||||
}
|
||||
Expect(m.IsOptionPattern()).To(BeFalse())
|
||||
})
|
||||
})
|
||||
|
||||
Describe("Python type and name helpers", func() {
|
||||
Describe("ToPythonType", func() {
|
||||
It("should map Go types to Python types", func() {
|
||||
@ -1328,6 +1431,95 @@ var _ = Describe("Rust Generation", func() {
|
||||
Expect(codeStr).To(ContainSubstring("FnResult<extism_pdk::Json<f64>>"))
|
||||
})
|
||||
})
|
||||
|
||||
Describe("GenerateClientRust", func() {
|
||||
It("should generate Option<T> for (value, exists bool) pattern", func() {
|
||||
svc := Service{
|
||||
Name: "Config",
|
||||
Permission: "config",
|
||||
Interface: "ConfigService",
|
||||
Methods: []Method{
|
||||
{
|
||||
Name: "Get",
|
||||
Params: []Param{
|
||||
{Name: "key", Type: "string", JSONName: "key"},
|
||||
},
|
||||
Returns: []Param{
|
||||
{Name: "value", Type: "string", JSONName: "value"},
|
||||
{Name: "exists", Type: "bool", JSONName: "exists"},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
code, err := GenerateClientRust(svc)
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
|
||||
codeStr := string(code)
|
||||
|
||||
// Should generate Option<String> return type, not (String, bool)
|
||||
Expect(codeStr).To(ContainSubstring("Result<Option<String>, Error>"))
|
||||
Expect(codeStr).NotTo(ContainSubstring("Result<(String, bool), Error>"))
|
||||
|
||||
// Should generate Some/None logic
|
||||
Expect(codeStr).To(ContainSubstring("Ok(Some("))
|
||||
Expect(codeStr).To(ContainSubstring("Ok(None)"))
|
||||
})
|
||||
|
||||
It("should generate tuple for non-option multi-return", func() {
|
||||
svc := Service{
|
||||
Name: "Test",
|
||||
Permission: "test",
|
||||
Interface: "TestService",
|
||||
Methods: []Method{
|
||||
{
|
||||
Name: "GetStats",
|
||||
Returns: []Param{
|
||||
{Name: "count", Type: "int64", JSONName: "count"},
|
||||
{Name: "size", Type: "int64", JSONName: "size"},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
code, err := GenerateClientRust(svc)
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
|
||||
codeStr := string(code)
|
||||
|
||||
// Should generate tuple return type
|
||||
Expect(codeStr).To(ContainSubstring("Result<(i64, i64), Error>"))
|
||||
Expect(codeStr).NotTo(ContainSubstring("Option<"))
|
||||
})
|
||||
|
||||
It("should NOT generate Option for Has() pattern where first return is bool", func() {
|
||||
svc := Service{
|
||||
Name: "Cache",
|
||||
Permission: "cache",
|
||||
Interface: "CacheService",
|
||||
Methods: []Method{
|
||||
{
|
||||
Name: "Has",
|
||||
Params: []Param{
|
||||
{Name: "key", Type: "string", JSONName: "key"},
|
||||
},
|
||||
Returns: []Param{
|
||||
{Name: "exists", Type: "bool", JSONName: "exists"},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
code, err := GenerateClientRust(svc)
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
|
||||
codeStr := string(code)
|
||||
|
||||
// Should generate simple bool return, not Option
|
||||
Expect(codeStr).To(ContainSubstring("Result<bool, Error>"))
|
||||
Expect(codeStr).NotTo(ContainSubstring("Option<bool>"))
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
func writeFile(path, content string) error {
|
||||
|
||||
@ -69,7 +69,9 @@ extern "ExtismHost" {
|
||||
{{- if .HasReturns}}
|
||||
///
|
||||
/// # Returns
|
||||
{{- if eq (len .Returns) 1}}
|
||||
{{- if .IsOptionPattern}}
|
||||
/// `Some({{(index .Returns 0).RustName}})` if found, `None` otherwise.
|
||||
{{- else 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}}).
|
||||
@ -78,6 +80,31 @@ extern "ExtismHost" {
|
||||
///
|
||||
/// # Errors
|
||||
/// Returns an error if the host function call fails.
|
||||
{{- if .IsOptionPattern}}
|
||||
pub fn {{rustFunc .}}({{range $i, $p := .Params}}{{if $i}}, {{end}}{{$p.RustName}}: {{rustParamType $p}}{{end}}) -> Result<Option<{{rustType (index .Returns 0)}}>, 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 .HasError}}
|
||||
if let Some(err) = response.0.error {
|
||||
return Err(Error::msg(err));
|
||||
}
|
||||
{{end}}
|
||||
if response.0.{{(index .Returns 1).RustName}} {
|
||||
Ok(Some(response.0.{{(index .Returns 0).RustName}}))
|
||||
} else {
|
||||
Ok(None)
|
||||
}
|
||||
}
|
||||
{{- else}}
|
||||
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}}
|
||||
@ -104,3 +131,4 @@ pub fn {{rustFunc .}}({{range $i, $p := .Params}}{{if $i}}, {{end}}{{$p.RustName
|
||||
{{- end}}
|
||||
}
|
||||
{{- end}}
|
||||
{{- end}}
|
||||
|
||||
@ -246,6 +246,25 @@ func (m Method) IsMultiReturn() bool {
|
||||
return len(m.Returns) > 1
|
||||
}
|
||||
|
||||
// IsOptionPattern returns true if the method returns (value, bool) where the bool
|
||||
// indicates existence (named "exists", "ok", or "found"). This pattern is used to
|
||||
// generate Option<T> in Rust instead of a tuple.
|
||||
func (m Method) IsOptionPattern() bool {
|
||||
if len(m.Returns) != 2 {
|
||||
return false
|
||||
}
|
||||
if m.Returns[1].Type != "bool" {
|
||||
return false
|
||||
}
|
||||
// Only treat as option pattern if the first return has a meaningful value type
|
||||
// (not just a bool check like Has())
|
||||
if m.Returns[0].Type == "bool" {
|
||||
return false
|
||||
}
|
||||
name := strings.ToLower(m.Returns[1].Name)
|
||||
return name == "exists" || name == "ok" || name == "found"
|
||||
}
|
||||
|
||||
// ReturnSignature returns the Go return type signature for the wrapper function.
|
||||
// For error-only: "error"
|
||||
// For single return with error: "(Type, error)"
|
||||
|
||||
156
plugins/cmd/ndpgen/testdata/config_client_expected.go.txt
vendored
Normal file
156
plugins/cmd/ndpgen/testdata/config_client_expected.go.txt
vendored
Normal file
@ -0,0 +1,156 @@
|
||||
// Code generated by ndpgen. DO NOT EDIT.
|
||||
//
|
||||
// This file contains client wrappers for the Config host service.
|
||||
// It is intended for use in Navidrome plugins built with TinyGo.
|
||||
//
|
||||
//go:build wasip1
|
||||
|
||||
package ndhost
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
|
||||
"github.com/navidrome/navidrome/plugins/pdk/go/pdk"
|
||||
)
|
||||
|
||||
// config_get is the host function provided by Navidrome.
|
||||
//
|
||||
//go:wasmimport extism:host/user config_get
|
||||
func config_get(uint64) uint64
|
||||
|
||||
// config_set is the host function provided by Navidrome.
|
||||
//
|
||||
//go:wasmimport extism:host/user config_set
|
||||
func config_set(uint64) uint64
|
||||
|
||||
// config_has is the host function provided by Navidrome.
|
||||
//
|
||||
//go:wasmimport extism:host/user config_has
|
||||
func config_has(uint64) uint64
|
||||
|
||||
type configGetRequest struct {
|
||||
Key string `json:"key"`
|
||||
}
|
||||
|
||||
type configGetResponse struct {
|
||||
Value string `json:"value,omitempty"`
|
||||
Exists bool `json:"exists,omitempty"`
|
||||
Error string `json:"error,omitempty"`
|
||||
}
|
||||
|
||||
type configSetRequest struct {
|
||||
Key string `json:"key"`
|
||||
Value string `json:"value"`
|
||||
}
|
||||
|
||||
type configHasRequest struct {
|
||||
Key string `json:"key"`
|
||||
}
|
||||
|
||||
type configHasResponse struct {
|
||||
Exists bool `json:"exists,omitempty"`
|
||||
Error string `json:"error,omitempty"`
|
||||
}
|
||||
|
||||
// ConfigGet calls the config_get host function.
|
||||
func ConfigGet(key string) (string, bool, error) {
|
||||
// Marshal request to JSON
|
||||
req := configGetRequest{
|
||||
Key: key,
|
||||
}
|
||||
reqBytes, err := json.Marshal(req)
|
||||
if err != nil {
|
||||
return "", false, err
|
||||
}
|
||||
reqMem := pdk.AllocateBytes(reqBytes)
|
||||
defer reqMem.Free()
|
||||
|
||||
// Call the host function
|
||||
responsePtr := config_get(reqMem.Offset())
|
||||
|
||||
// Read the response from memory
|
||||
responseMem := pdk.FindMemory(responsePtr)
|
||||
responseBytes := responseMem.ReadBytes()
|
||||
|
||||
// Parse the response
|
||||
var response configGetResponse
|
||||
if err := json.Unmarshal(responseBytes, &response); err != nil {
|
||||
return "", false, err
|
||||
}
|
||||
|
||||
// Convert Error field to Go error
|
||||
if response.Error != "" {
|
||||
return "", false, errors.New(response.Error)
|
||||
}
|
||||
|
||||
return response.Value, response.Exists, nil
|
||||
}
|
||||
|
||||
// ConfigSet calls the config_set host function.
|
||||
func ConfigSet(key string, value string) error {
|
||||
// Marshal request to JSON
|
||||
req := configSetRequest{
|
||||
Key: key,
|
||||
Value: value,
|
||||
}
|
||||
reqBytes, err := json.Marshal(req)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
reqMem := pdk.AllocateBytes(reqBytes)
|
||||
defer reqMem.Free()
|
||||
|
||||
// Call the host function
|
||||
responsePtr := config_set(reqMem.Offset())
|
||||
|
||||
// Read the response from memory
|
||||
responseMem := pdk.FindMemory(responsePtr)
|
||||
responseBytes := responseMem.ReadBytes()
|
||||
|
||||
// Parse error-only response
|
||||
var response struct {
|
||||
Error string `json:"error,omitempty"`
|
||||
}
|
||||
if err := json.Unmarshal(responseBytes, &response); err != nil {
|
||||
return err
|
||||
}
|
||||
if response.Error != "" {
|
||||
return errors.New(response.Error)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// ConfigHas calls the config_has host function.
|
||||
func ConfigHas(key string) (bool, error) {
|
||||
// Marshal request to JSON
|
||||
req := configHasRequest{
|
||||
Key: key,
|
||||
}
|
||||
reqBytes, err := json.Marshal(req)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
reqMem := pdk.AllocateBytes(reqBytes)
|
||||
defer reqMem.Free()
|
||||
|
||||
// Call the host function
|
||||
responsePtr := config_has(reqMem.Offset())
|
||||
|
||||
// Read the response from memory
|
||||
responseMem := pdk.FindMemory(responsePtr)
|
||||
responseBytes := responseMem.ReadBytes()
|
||||
|
||||
// Parse the response
|
||||
var response configHasResponse
|
||||
if err := json.Unmarshal(responseBytes, &response); err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
// Convert Error field to Go error
|
||||
if response.Error != "" {
|
||||
return false, errors.New(response.Error)
|
||||
}
|
||||
|
||||
return response.Exists, nil
|
||||
}
|
||||
126
plugins/cmd/ndpgen/testdata/config_client_expected.py
vendored
Normal file
126
plugins/cmd/ndpgen/testdata/config_client_expected.py
vendored
Normal file
@ -0,0 +1,126 @@
|
||||
# Code generated by ndpgen. DO NOT EDIT.
|
||||
#
|
||||
# This file contains client wrappers for the Config 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", "config_get")
|
||||
def _config_get(offset: int) -> int:
|
||||
"""Raw host function - do not call directly."""
|
||||
...
|
||||
|
||||
|
||||
@extism.import_fn("extism:host/user", "config_set")
|
||||
def _config_set(offset: int) -> int:
|
||||
"""Raw host function - do not call directly."""
|
||||
...
|
||||
|
||||
|
||||
@extism.import_fn("extism:host/user", "config_has")
|
||||
def _config_has(offset: int) -> int:
|
||||
"""Raw host function - do not call directly."""
|
||||
...
|
||||
|
||||
|
||||
@dataclass
|
||||
class ConfigGetResult:
|
||||
"""Result type for config_get."""
|
||||
value: str
|
||||
exists: bool
|
||||
|
||||
|
||||
def config_get(key: str) -> ConfigGetResult:
|
||||
"""Call the config_get host function.
|
||||
|
||||
Args:
|
||||
key: str parameter.
|
||||
|
||||
Returns:
|
||||
ConfigGetResult containing value, exists,.
|
||||
|
||||
Raises:
|
||||
HostFunctionError: If the host function returns an error.
|
||||
"""
|
||||
request = {
|
||||
"key": key,
|
||||
}
|
||||
request_bytes = json.dumps(request).encode("utf-8")
|
||||
request_mem = extism.memory.alloc(request_bytes)
|
||||
response_offset = _config_get(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 ConfigGetResult(
|
||||
value=response.get("value", ""),
|
||||
exists=response.get("exists", False),
|
||||
)
|
||||
|
||||
|
||||
def config_set(key: str, value: str) -> None:
|
||||
"""Call the config_set host function.
|
||||
|
||||
Args:
|
||||
key: str parameter.
|
||||
value: str parameter.
|
||||
|
||||
Raises:
|
||||
HostFunctionError: If the host function returns an error.
|
||||
"""
|
||||
request = {
|
||||
"key": key,
|
||||
"value": value,
|
||||
}
|
||||
request_bytes = json.dumps(request).encode("utf-8")
|
||||
request_mem = extism.memory.alloc(request_bytes)
|
||||
response_offset = _config_set(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 config_has(key: str) -> bool:
|
||||
"""Call the config_has host function.
|
||||
|
||||
Args:
|
||||
key: str parameter.
|
||||
|
||||
Returns:
|
||||
bool: The result value.
|
||||
|
||||
Raises:
|
||||
HostFunctionError: If the host function returns an error.
|
||||
"""
|
||||
request = {
|
||||
"key": key,
|
||||
}
|
||||
request_bytes = json.dumps(request).encode("utf-8")
|
||||
request_mem = extism.memory.alloc(request_bytes)
|
||||
response_offset = _config_has(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("exists", False)
|
||||
135
plugins/cmd/ndpgen/testdata/config_client_expected.rs
vendored
Normal file
135
plugins/cmd/ndpgen/testdata/config_client_expected.rs
vendored
Normal file
@ -0,0 +1,135 @@
|
||||
// Code generated by ndpgen. DO NOT EDIT.
|
||||
//
|
||||
// This file contains client wrappers for the Config 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 ConfigGetRequest {
|
||||
key: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct ConfigGetResponse {
|
||||
#[serde(default)]
|
||||
value: String,
|
||||
#[serde(default)]
|
||||
exists: bool,
|
||||
#[serde(default)]
|
||||
error: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct ConfigSetRequest {
|
||||
key: String,
|
||||
value: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct ConfigSetResponse {
|
||||
#[serde(default)]
|
||||
error: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct ConfigHasRequest {
|
||||
key: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct ConfigHasResponse {
|
||||
#[serde(default)]
|
||||
exists: bool,
|
||||
#[serde(default)]
|
||||
error: Option<String>,
|
||||
}
|
||||
|
||||
#[host_fn]
|
||||
extern "ExtismHost" {
|
||||
fn config_get(input: Json<ConfigGetRequest>) -> Json<ConfigGetResponse>;
|
||||
fn config_set(input: Json<ConfigSetRequest>) -> Json<ConfigSetResponse>;
|
||||
fn config_has(input: Json<ConfigHasRequest>) -> Json<ConfigHasResponse>;
|
||||
}
|
||||
|
||||
/// Calls the config_get host function.
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `key` - String parameter.
|
||||
///
|
||||
/// # Returns
|
||||
/// `Some(value)` if found, `None` otherwise.
|
||||
///
|
||||
/// # Errors
|
||||
/// Returns an error if the host function call fails.
|
||||
pub fn get(key: &str) -> Result<Option<String>, Error> {
|
||||
let response = unsafe {
|
||||
config_get(Json(ConfigGetRequest {
|
||||
key: key.to_owned(),
|
||||
}))?
|
||||
};
|
||||
|
||||
if let Some(err) = response.0.error {
|
||||
return Err(Error::msg(err));
|
||||
}
|
||||
|
||||
if response.0.exists {
|
||||
Ok(Some(response.0.value))
|
||||
} else {
|
||||
Ok(None)
|
||||
}
|
||||
}
|
||||
|
||||
/// Calls the config_set host function.
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `key` - String parameter.
|
||||
/// * `value` - String parameter.
|
||||
///
|
||||
/// # Errors
|
||||
/// Returns an error if the host function call fails.
|
||||
pub fn set(key: &str, value: &str) -> Result<(), Error> {
|
||||
let response = unsafe {
|
||||
config_set(Json(ConfigSetRequest {
|
||||
key: key.to_owned(),
|
||||
value: value.to_owned(),
|
||||
}))?
|
||||
};
|
||||
|
||||
if let Some(err) = response.0.error {
|
||||
return Err(Error::msg(err));
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Calls the config_has host function.
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `key` - String parameter.
|
||||
///
|
||||
/// # Returns
|
||||
/// The exists value.
|
||||
///
|
||||
/// # Errors
|
||||
/// Returns an error if the host function call fails.
|
||||
pub fn has(key: &str) -> Result<bool, Error> {
|
||||
let response = unsafe {
|
||||
config_has(Json(ConfigHasRequest {
|
||||
key: key.to_owned(),
|
||||
}))?
|
||||
};
|
||||
|
||||
if let Some(err) = response.0.error {
|
||||
return Err(Error::msg(err));
|
||||
}
|
||||
|
||||
Ok(response.0.exists)
|
||||
}
|
||||
15
plugins/cmd/ndpgen/testdata/config_service.go.txt
vendored
Normal file
15
plugins/cmd/ndpgen/testdata/config_service.go.txt
vendored
Normal file
@ -0,0 +1,15 @@
|
||||
package testpkg
|
||||
|
||||
import "context"
|
||||
|
||||
//nd:hostservice name=Config permission=config
|
||||
type ConfigService interface {
|
||||
//nd:hostfunc
|
||||
Get(ctx context.Context, key string) (value string, exists bool, err error)
|
||||
|
||||
//nd:hostfunc
|
||||
Set(ctx context.Context, key string, value string) error
|
||||
|
||||
//nd:hostfunc
|
||||
Has(ctx context.Context, key string) (exists bool, err error)
|
||||
}
|
||||
@ -65,10 +65,9 @@ struct DiscordPlugin;
|
||||
// ============================================================================
|
||||
|
||||
fn get_config() -> Result<(String, std::collections::HashMap<String, String>), Error> {
|
||||
let (client_id, exists) = config::get(CLIENT_ID_KEY)?;
|
||||
if !exists || client_id.is_empty() {
|
||||
return Err(Error::msg("missing clientid in configuration"));
|
||||
}
|
||||
let client_id = config::get(CLIENT_ID_KEY)?
|
||||
.filter(|s| !s.is_empty())
|
||||
.ok_or_else(|| Error::msg("missing clientid in configuration"))?;
|
||||
|
||||
// Get all user keys with the "user." prefix
|
||||
let user_keys = config::keys(USER_KEY_PREFIX)?;
|
||||
@ -76,8 +75,7 @@ fn get_config() -> Result<(String, std::collections::HashMap<String, String>), E
|
||||
let mut users = std::collections::HashMap::new();
|
||||
for key in user_keys {
|
||||
let username = key.strip_prefix(USER_KEY_PREFIX).unwrap_or(&key);
|
||||
let (token, token_exists) = config::get(&key)?;
|
||||
if token_exists && !token.is_empty() {
|
||||
if let Some(token) = config::get(&key)?.filter(|s| !s.is_empty()) {
|
||||
users.insert(username.to_string(), token);
|
||||
}
|
||||
}
|
||||
|
||||
@ -129,8 +129,8 @@ pub fn cleanup_connection(username: &str) {
|
||||
|
||||
// Try to close the WebSocket connection
|
||||
let conn_key = connection_key(username);
|
||||
if let Ok((conn_id, exists)) = cache::get_string(&conn_key) {
|
||||
if exists && !conn_id.is_empty() {
|
||||
if let Ok(Some(conn_id)) = cache::get_string(&conn_key) {
|
||||
if !conn_id.is_empty() {
|
||||
if let Err(e) = websocket::close_connection(&conn_id, 1000, "Reconnecting") {
|
||||
trace!("Failed to close WebSocket for user {}: {:?}", username, e);
|
||||
}
|
||||
@ -264,24 +264,22 @@ pub fn handle_clear_activity_callback(username: &str) -> Result<(), Error> {
|
||||
info!("Clearing activity for user {}", username);
|
||||
|
||||
let conn_key = connection_key(username);
|
||||
if let Ok((conn_id, exists)) = cache::get_string(&conn_key) {
|
||||
if exists && !conn_id.is_empty() {
|
||||
// Send empty presence to clear activity
|
||||
let msg = GatewayMessage {
|
||||
op: PRESENCE_OP_CODE,
|
||||
d: PresencePayload {
|
||||
activities: vec![],
|
||||
since: 0,
|
||||
status: "dnd".to_string(),
|
||||
afk: false,
|
||||
},
|
||||
};
|
||||
if let Some(conn_id) = cache::get_string(&conn_key)?.filter(|s| !s.is_empty()) {
|
||||
// Send empty presence to clear activity
|
||||
let msg = GatewayMessage {
|
||||
op: PRESENCE_OP_CODE,
|
||||
d: PresencePayload {
|
||||
activities: vec![],
|
||||
since: 0,
|
||||
status: "dnd".to_string(),
|
||||
afk: false,
|
||||
},
|
||||
};
|
||||
|
||||
let json = serde_json::to_string(&msg)
|
||||
.map_err(|e| Error::msg(format!("Failed to serialize message: {}", e)))?;
|
||||
let json = serde_json::to_string(&msg)
|
||||
.map_err(|e| Error::msg(format!("Failed to serialize message: {}", e)))?;
|
||||
|
||||
websocket::send_text(&conn_id, &json)?;
|
||||
}
|
||||
websocket::send_text(&conn_id, &json)?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
@ -298,15 +296,13 @@ pub fn disconnect(username: &str) -> Result<(), Error> {
|
||||
|
||||
// Close the WebSocket connection
|
||||
let conn_key = connection_key(username);
|
||||
if let Ok((conn_id, exists)) = cache::get_string(&conn_key) {
|
||||
if exists && !conn_id.is_empty() {
|
||||
if let Err(e) = websocket::close_connection(&conn_id, 1000, "Navidrome disconnect") {
|
||||
warn!("Failed to close WebSocket connection: {:?}", e);
|
||||
}
|
||||
// Clean up reverse mapping
|
||||
let reverse_key = format!("discord.reverse.{}", conn_id);
|
||||
let _ = cache::remove(&reverse_key);
|
||||
if let Some(conn_id) = cache::get_string(&conn_key)?.filter(|s| !s.is_empty()) {
|
||||
if let Err(e) = websocket::close_connection(&conn_id, 1000, "Navidrome disconnect") {
|
||||
warn!("Failed to close WebSocket connection: {:?}", e);
|
||||
}
|
||||
// Clean up reverse mapping
|
||||
let reverse_key = format!("discord.reverse.{}", conn_id);
|
||||
let _ = cache::remove(&reverse_key);
|
||||
}
|
||||
|
||||
// Clean up cache entries
|
||||
@ -324,10 +320,9 @@ pub fn send_activity(
|
||||
mut activity: Activity,
|
||||
) -> Result<(), Error> {
|
||||
let conn_key = connection_key(username);
|
||||
let (conn_id, exists) = cache::get_string(&conn_key)?;
|
||||
if !exists || conn_id.is_empty() {
|
||||
return Err(Error::msg("Not connected to Discord"));
|
||||
}
|
||||
let conn_id = cache::get_string(&conn_key)?
|
||||
.filter(|s| !s.is_empty())
|
||||
.ok_or_else(|| Error::msg("Not connected to Discord"))?;
|
||||
|
||||
// Process image URL
|
||||
activity.assets.large_image = process_image(&activity.assets.large_image, client_id, token)?;
|
||||
@ -361,12 +356,7 @@ fn find_username_for_connection(connection_id: &str) -> Result<Option<String>, E
|
||||
// The connection ID is stored as cache value, so we need to scan for it
|
||||
// Since we can't iterate cache, we'll use a workaround with a reverse mapping
|
||||
let reverse_key = format!("discord.reverse.{}", connection_id);
|
||||
if let Ok((username, exists)) = cache::get_string(&reverse_key) {
|
||||
if exists && !username.is_empty() {
|
||||
return Ok(Some(username));
|
||||
}
|
||||
}
|
||||
Ok(None)
|
||||
Ok(cache::get_string(&reverse_key)?.filter(|s| !s.is_empty()))
|
||||
}
|
||||
|
||||
fn get_discord_gateway() -> Result<String, Error> {
|
||||
@ -394,16 +384,14 @@ fn identify(username: &str) -> Result<(), Error> {
|
||||
info!("Identifying with Discord for user {}", username);
|
||||
|
||||
let conn_key = connection_key(username);
|
||||
let (conn_id, exists) = cache::get_string(&conn_key)?;
|
||||
if !exists || conn_id.is_empty() {
|
||||
return Err(Error::msg("No connection found"));
|
||||
}
|
||||
let conn_id = cache::get_string(&conn_key)?
|
||||
.filter(|s| !s.is_empty())
|
||||
.ok_or_else(|| Error::msg("No connection found"))?;
|
||||
|
||||
let token_k = token_key(username);
|
||||
let (token, exists) = cache::get_string(&token_k)?;
|
||||
if !exists || token.is_empty() {
|
||||
return Err(Error::msg("No token found"));
|
||||
}
|
||||
let token = cache::get_string(&token_k)?
|
||||
.filter(|s| !s.is_empty())
|
||||
.ok_or_else(|| Error::msg("No token found"))?;
|
||||
|
||||
// Store reverse mapping for connection -> username
|
||||
let reverse_key = format!("discord.reverse.{}", conn_id);
|
||||
@ -440,19 +428,14 @@ fn identify(username: &str) -> Result<(), Error> {
|
||||
|
||||
fn send_heartbeat(username: &str) -> Result<(), Error> {
|
||||
let conn_key = connection_key(username);
|
||||
let (conn_id, exists) = cache::get_string(&conn_key)?;
|
||||
if !exists || conn_id.is_empty() {
|
||||
return Err(Error::msg("No connection found"));
|
||||
}
|
||||
let conn_id = cache::get_string(&conn_key)?
|
||||
.filter(|s| !s.is_empty())
|
||||
.ok_or_else(|| Error::msg("No connection found"))?;
|
||||
|
||||
// Get sequence number
|
||||
let seq_key = sequence_key(username);
|
||||
let (seq_str, exists) = cache::get_string(&seq_key)?;
|
||||
let seq: Option<i64> = if exists && !seq_str.is_empty() {
|
||||
seq_str.parse().ok()
|
||||
} else {
|
||||
None
|
||||
};
|
||||
let seq: Option<i64> = cache::get_string(&seq_key)?
|
||||
.and_then(|s| s.parse().ok());
|
||||
|
||||
// Send heartbeat
|
||||
let msg = GatewayMessage {
|
||||
@ -493,10 +476,8 @@ fn process_image_inner(
|
||||
|
||||
// Check cache
|
||||
let cache_key = format!("discord.image.{:x}", md5_hash(url));
|
||||
if let Ok((cached, exists)) = cache::get_string(&cache_key) {
|
||||
if exists && !cached.is_empty() {
|
||||
return Ok(cached);
|
||||
}
|
||||
if let Some(cached) = cache::get_string(&cache_key)?.filter(|s| !s.is_empty()) {
|
||||
return Ok(cached);
|
||||
}
|
||||
|
||||
// Process via Discord API
|
||||
|
||||
@ -220,11 +220,11 @@ pub fn set_string(key: &str, value: &str, ttl_seconds: i64) -> Result<(), Error>
|
||||
/// * `key` - String parameter.
|
||||
///
|
||||
/// # Returns
|
||||
/// A tuple of (value, exists).
|
||||
/// `Some(value)` if found, `None` otherwise.
|
||||
///
|
||||
/// # Errors
|
||||
/// Returns an error if the host function call fails.
|
||||
pub fn get_string(key: &str) -> Result<(String, bool), Error> {
|
||||
pub fn get_string(key: &str) -> Result<Option<String>, Error> {
|
||||
let response = unsafe {
|
||||
cache_getstring(Json(CacheGetStringRequest {
|
||||
key: key.to_owned(),
|
||||
@ -235,7 +235,11 @@ pub fn get_string(key: &str) -> Result<(String, bool), Error> {
|
||||
return Err(Error::msg(err));
|
||||
}
|
||||
|
||||
Ok((response.0.value, response.0.exists))
|
||||
if response.0.exists {
|
||||
Ok(Some(response.0.value))
|
||||
} else {
|
||||
Ok(None)
|
||||
}
|
||||
}
|
||||
|
||||
/// SetInt stores an integer value in the cache.
|
||||
@ -282,11 +286,11 @@ pub fn set_int(key: &str, value: i64, ttl_seconds: i64) -> Result<(), Error> {
|
||||
/// * `key` - String parameter.
|
||||
///
|
||||
/// # Returns
|
||||
/// A tuple of (value, exists).
|
||||
/// `Some(value)` if found, `None` otherwise.
|
||||
///
|
||||
/// # Errors
|
||||
/// Returns an error if the host function call fails.
|
||||
pub fn get_int(key: &str) -> Result<(i64, bool), Error> {
|
||||
pub fn get_int(key: &str) -> Result<Option<i64>, Error> {
|
||||
let response = unsafe {
|
||||
cache_getint(Json(CacheGetIntRequest {
|
||||
key: key.to_owned(),
|
||||
@ -297,7 +301,11 @@ pub fn get_int(key: &str) -> Result<(i64, bool), Error> {
|
||||
return Err(Error::msg(err));
|
||||
}
|
||||
|
||||
Ok((response.0.value, response.0.exists))
|
||||
if response.0.exists {
|
||||
Ok(Some(response.0.value))
|
||||
} else {
|
||||
Ok(None)
|
||||
}
|
||||
}
|
||||
|
||||
/// SetFloat stores a float value in the cache.
|
||||
@ -344,11 +352,11 @@ pub fn set_float(key: &str, value: f64, ttl_seconds: i64) -> Result<(), Error> {
|
||||
/// * `key` - String parameter.
|
||||
///
|
||||
/// # Returns
|
||||
/// A tuple of (value, exists).
|
||||
/// `Some(value)` if found, `None` otherwise.
|
||||
///
|
||||
/// # Errors
|
||||
/// Returns an error if the host function call fails.
|
||||
pub fn get_float(key: &str) -> Result<(f64, bool), Error> {
|
||||
pub fn get_float(key: &str) -> Result<Option<f64>, Error> {
|
||||
let response = unsafe {
|
||||
cache_getfloat(Json(CacheGetFloatRequest {
|
||||
key: key.to_owned(),
|
||||
@ -359,7 +367,11 @@ pub fn get_float(key: &str) -> Result<(f64, bool), Error> {
|
||||
return Err(Error::msg(err));
|
||||
}
|
||||
|
||||
Ok((response.0.value, response.0.exists))
|
||||
if response.0.exists {
|
||||
Ok(Some(response.0.value))
|
||||
} else {
|
||||
Ok(None)
|
||||
}
|
||||
}
|
||||
|
||||
/// SetBytes stores a byte slice in the cache.
|
||||
@ -406,11 +418,11 @@ pub fn set_bytes(key: &str, value: Vec<u8>, ttl_seconds: i64) -> Result<(), Erro
|
||||
/// * `key` - String parameter.
|
||||
///
|
||||
/// # Returns
|
||||
/// A tuple of (value, exists).
|
||||
/// `Some(value)` if found, `None` otherwise.
|
||||
///
|
||||
/// # Errors
|
||||
/// Returns an error if the host function call fails.
|
||||
pub fn get_bytes(key: &str) -> Result<(Vec<u8>, bool), Error> {
|
||||
pub fn get_bytes(key: &str) -> Result<Option<Vec<u8>>, Error> {
|
||||
let response = unsafe {
|
||||
cache_getbytes(Json(CacheGetBytesRequest {
|
||||
key: key.to_owned(),
|
||||
@ -421,7 +433,11 @@ pub fn get_bytes(key: &str) -> Result<(Vec<u8>, bool), Error> {
|
||||
return Err(Error::msg(err));
|
||||
}
|
||||
|
||||
Ok((response.0.value, response.0.exists))
|
||||
if response.0.exists {
|
||||
Ok(Some(response.0.value))
|
||||
} else {
|
||||
Ok(None)
|
||||
}
|
||||
}
|
||||
|
||||
/// Has checks if a key exists in the cache.
|
||||
|
||||
@ -67,18 +67,22 @@ extern "ExtismHost" {
|
||||
/// * `key` - String parameter.
|
||||
///
|
||||
/// # Returns
|
||||
/// A tuple of (value, exists).
|
||||
/// `Some(value)` if found, `None` otherwise.
|
||||
///
|
||||
/// # Errors
|
||||
/// Returns an error if the host function call fails.
|
||||
pub fn get(key: &str) -> Result<(String, bool), Error> {
|
||||
pub fn get(key: &str) -> Result<Option<String>, Error> {
|
||||
let response = unsafe {
|
||||
config_get(Json(ConfigGetRequest {
|
||||
key: key.to_owned(),
|
||||
}))?
|
||||
};
|
||||
|
||||
Ok((response.0.value, response.0.exists))
|
||||
if response.0.exists {
|
||||
Ok(Some(response.0.value))
|
||||
} else {
|
||||
Ok(None)
|
||||
}
|
||||
}
|
||||
|
||||
/// GetInt retrieves a configuration value as an integer.
|
||||
@ -93,18 +97,22 @@ pub fn get(key: &str) -> Result<(String, bool), Error> {
|
||||
/// * `key` - String parameter.
|
||||
///
|
||||
/// # Returns
|
||||
/// A tuple of (value, exists).
|
||||
/// `Some(value)` if found, `None` otherwise.
|
||||
///
|
||||
/// # Errors
|
||||
/// Returns an error if the host function call fails.
|
||||
pub fn get_int(key: &str) -> Result<(i64, bool), Error> {
|
||||
pub fn get_int(key: &str) -> Result<Option<i64>, Error> {
|
||||
let response = unsafe {
|
||||
config_getint(Json(ConfigGetIntRequest {
|
||||
key: key.to_owned(),
|
||||
}))?
|
||||
};
|
||||
|
||||
Ok((response.0.value, response.0.exists))
|
||||
if response.0.exists {
|
||||
Ok(Some(response.0.value))
|
||||
} else {
|
||||
Ok(None)
|
||||
}
|
||||
}
|
||||
|
||||
/// Keys returns configuration keys matching the given prefix.
|
||||
|
||||
@ -139,11 +139,11 @@ pub fn set(key: &str, value: Vec<u8>) -> Result<(), Error> {
|
||||
/// * `key` - String parameter.
|
||||
///
|
||||
/// # Returns
|
||||
/// A tuple of (value, exists).
|
||||
/// `Some(value)` if found, `None` otherwise.
|
||||
///
|
||||
/// # Errors
|
||||
/// Returns an error if the host function call fails.
|
||||
pub fn get(key: &str) -> Result<(Vec<u8>, bool), Error> {
|
||||
pub fn get(key: &str) -> Result<Option<Vec<u8>>, Error> {
|
||||
let response = unsafe {
|
||||
kvstore_get(Json(KVStoreGetRequest {
|
||||
key: key.to_owned(),
|
||||
@ -154,7 +154,11 @@ pub fn get(key: &str) -> Result<(Vec<u8>, bool), Error> {
|
||||
return Err(Error::msg(err));
|
||||
}
|
||||
|
||||
Ok((response.0.value, response.0.exists))
|
||||
if response.0.exists {
|
||||
Ok(Some(response.0.value))
|
||||
} else {
|
||||
Ok(None)
|
||||
}
|
||||
}
|
||||
|
||||
/// Delete removes a value from storage.
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user