mirror of
https://github.com/navidrome/navidrome.git
synced 2026-04-03 06:41:01 +00:00
feat: implement ConfigService for plugin configuration management
Signed-off-by: Deluan <deluan@navidrome.org>
This commit is contained in:
parent
4b66560f50
commit
b90ecb9754
@ -29,6 +29,7 @@ The plugin system is built on **[Extism](https://extism.org/)**, a cross-languag
|
||||
- [Library](#library)
|
||||
- [Artwork](#artwork)
|
||||
- [SubsonicAPI](#subsonicapi)
|
||||
- [Config](#config)
|
||||
- [Configuration](#configuration)
|
||||
- [Building Plugins](#building-plugins)
|
||||
- [Examples](#examples)
|
||||
@ -670,6 +671,44 @@ Call Navidrome's Subsonic API internally (no network round-trip).
|
||||
response, err := SubsonicAPICall("getAlbumList2?type=random&size=10&u=username")
|
||||
```
|
||||
|
||||
### Config
|
||||
|
||||
Access plugin configuration values programmatically. Unlike `pdk.GetConfig()` which only retrieves individual values, this service can list all available configuration keys—useful for discovering dynamic configuration (e.g., user-to-token mappings).
|
||||
|
||||
> **Note:** This service is always available and does not require a manifest permission.
|
||||
|
||||
**Host functions:**
|
||||
|
||||
| Function | Parameters | Returns |
|
||||
|-----------------|------------|-----------------------------|
|
||||
| `config_get` | `key` | `value, exists` |
|
||||
| `config_getint` | `key` | `value, exists` |
|
||||
| `config_list` | `prefix` | Array of matching key names |
|
||||
|
||||
**Usage (with generated SDK):**
|
||||
|
||||
```go
|
||||
import "github.com/navidrome/navidrome/plugins/pdk/go/host"
|
||||
|
||||
// Get a string configuration value
|
||||
value, exists := host.ConfigGet("api_key")
|
||||
if exists {
|
||||
// Use the value
|
||||
}
|
||||
|
||||
// Get an integer configuration value
|
||||
count, exists := host.ConfigGetInt("max_retries")
|
||||
|
||||
// List all keys with a prefix (useful for user-specific config)
|
||||
keys := host.ConfigList("user:")
|
||||
for _, key := range keys {
|
||||
// key might be "user:john", "user:jane", etc.
|
||||
}
|
||||
|
||||
// List all configuration keys
|
||||
allKeys := host.ConfigList("")
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Configuration
|
||||
|
||||
44
plugins/host/config.go
Normal file
44
plugins/host/config.go
Normal file
@ -0,0 +1,44 @@
|
||||
package host
|
||||
|
||||
import "context"
|
||||
|
||||
// ConfigService provides access to plugin configuration values.
|
||||
//
|
||||
// This service allows plugins to retrieve configuration values and enumerate
|
||||
// available configuration keys. Unlike the built-in pdk.GetConfig(key) which
|
||||
// only retrieves individual values, this service provides methods to list all
|
||||
// available keys, making it useful for plugins that need to discover dynamic
|
||||
// configuration (e.g., user-to-token mappings).
|
||||
//
|
||||
// This service is always available and does not require a permission in the manifest.
|
||||
//
|
||||
//nd:hostservice name=Config
|
||||
type ConfigService interface {
|
||||
// Get retrieves a configuration value as a string.
|
||||
//
|
||||
// Parameters:
|
||||
// - key: The configuration key
|
||||
//
|
||||
// Returns the value and whether the key exists.
|
||||
//nd:hostfunc
|
||||
Get(ctx context.Context, key string) (value string, exists bool)
|
||||
|
||||
// GetInt retrieves a configuration value as an integer.
|
||||
//
|
||||
// Parameters:
|
||||
// - key: The configuration key
|
||||
//
|
||||
// Returns the value and whether the key exists. If the key exists but the
|
||||
// value cannot be parsed as an integer, exists will be false.
|
||||
//nd:hostfunc
|
||||
GetInt(ctx context.Context, key string) (value int64, exists bool)
|
||||
|
||||
// List returns configuration keys matching the given prefix.
|
||||
//
|
||||
// Parameters:
|
||||
// - prefix: Key prefix to filter by. If empty, returns all keys.
|
||||
//
|
||||
// Returns a sorted slice of matching configuration keys.
|
||||
//nd:hostfunc
|
||||
List(ctx context.Context, prefix string) (keys []string)
|
||||
}
|
||||
169
plugins/host/config_gen.go
Normal file
169
plugins/host/config_gen.go
Normal file
@ -0,0 +1,169 @@
|
||||
// Code generated by ndpgen. DO NOT EDIT.
|
||||
|
||||
package host
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
|
||||
extism "github.com/extism/go-sdk"
|
||||
)
|
||||
|
||||
// ConfigGetRequest is the request type for Config.Get.
|
||||
type ConfigGetRequest struct {
|
||||
Key string `json:"key"`
|
||||
}
|
||||
|
||||
// ConfigGetResponse is the response type for Config.Get.
|
||||
type ConfigGetResponse struct {
|
||||
Value string `json:"value,omitempty"`
|
||||
Exists bool `json:"exists,omitempty"`
|
||||
}
|
||||
|
||||
// ConfigGetIntRequest is the request type for Config.GetInt.
|
||||
type ConfigGetIntRequest struct {
|
||||
Key string `json:"key"`
|
||||
}
|
||||
|
||||
// ConfigGetIntResponse is the response type for Config.GetInt.
|
||||
type ConfigGetIntResponse struct {
|
||||
Value int64 `json:"value,omitempty"`
|
||||
Exists bool `json:"exists,omitempty"`
|
||||
}
|
||||
|
||||
// ConfigListRequest is the request type for Config.List.
|
||||
type ConfigListRequest struct {
|
||||
Prefix string `json:"prefix"`
|
||||
}
|
||||
|
||||
// ConfigListResponse is the response type for Config.List.
|
||||
type ConfigListResponse struct {
|
||||
Keys []string `json:"keys,omitempty"`
|
||||
}
|
||||
|
||||
// RegisterConfigHostFunctions registers Config service host functions.
|
||||
// The returned host functions should be added to the plugin's configuration.
|
||||
func RegisterConfigHostFunctions(service ConfigService) []extism.HostFunction {
|
||||
return []extism.HostFunction{
|
||||
newConfigGetHostFunction(service),
|
||||
newConfigGetIntHostFunction(service),
|
||||
newConfigListHostFunction(service),
|
||||
}
|
||||
}
|
||||
|
||||
func newConfigGetHostFunction(service ConfigService) extism.HostFunction {
|
||||
return extism.NewHostFunctionWithStack(
|
||||
"config_get",
|
||||
func(ctx context.Context, p *extism.CurrentPlugin, stack []uint64) {
|
||||
// Read JSON request from plugin memory
|
||||
reqBytes, err := p.ReadBytes(stack[0])
|
||||
if err != nil {
|
||||
configWriteError(p, stack, err)
|
||||
return
|
||||
}
|
||||
var req ConfigGetRequest
|
||||
if err := json.Unmarshal(reqBytes, &req); err != nil {
|
||||
configWriteError(p, stack, err)
|
||||
return
|
||||
}
|
||||
|
||||
// Call the service method
|
||||
value, exists := service.Get(ctx, req.Key)
|
||||
|
||||
// Write JSON response to plugin memory
|
||||
resp := ConfigGetResponse{
|
||||
Value: value,
|
||||
Exists: exists,
|
||||
}
|
||||
configWriteResponse(p, stack, resp)
|
||||
},
|
||||
[]extism.ValueType{extism.ValueTypePTR},
|
||||
[]extism.ValueType{extism.ValueTypePTR},
|
||||
)
|
||||
}
|
||||
|
||||
func newConfigGetIntHostFunction(service ConfigService) extism.HostFunction {
|
||||
return extism.NewHostFunctionWithStack(
|
||||
"config_getint",
|
||||
func(ctx context.Context, p *extism.CurrentPlugin, stack []uint64) {
|
||||
// Read JSON request from plugin memory
|
||||
reqBytes, err := p.ReadBytes(stack[0])
|
||||
if err != nil {
|
||||
configWriteError(p, stack, err)
|
||||
return
|
||||
}
|
||||
var req ConfigGetIntRequest
|
||||
if err := json.Unmarshal(reqBytes, &req); err != nil {
|
||||
configWriteError(p, stack, err)
|
||||
return
|
||||
}
|
||||
|
||||
// Call the service method
|
||||
value, exists := service.GetInt(ctx, req.Key)
|
||||
|
||||
// Write JSON response to plugin memory
|
||||
resp := ConfigGetIntResponse{
|
||||
Value: value,
|
||||
Exists: exists,
|
||||
}
|
||||
configWriteResponse(p, stack, resp)
|
||||
},
|
||||
[]extism.ValueType{extism.ValueTypePTR},
|
||||
[]extism.ValueType{extism.ValueTypePTR},
|
||||
)
|
||||
}
|
||||
|
||||
func newConfigListHostFunction(service ConfigService) extism.HostFunction {
|
||||
return extism.NewHostFunctionWithStack(
|
||||
"config_list",
|
||||
func(ctx context.Context, p *extism.CurrentPlugin, stack []uint64) {
|
||||
// Read JSON request from plugin memory
|
||||
reqBytes, err := p.ReadBytes(stack[0])
|
||||
if err != nil {
|
||||
configWriteError(p, stack, err)
|
||||
return
|
||||
}
|
||||
var req ConfigListRequest
|
||||
if err := json.Unmarshal(reqBytes, &req); err != nil {
|
||||
configWriteError(p, stack, err)
|
||||
return
|
||||
}
|
||||
|
||||
// Call the service method
|
||||
keys := service.List(ctx, req.Prefix)
|
||||
|
||||
// Write JSON response to plugin memory
|
||||
resp := ConfigListResponse{
|
||||
Keys: keys,
|
||||
}
|
||||
configWriteResponse(p, stack, resp)
|
||||
},
|
||||
[]extism.ValueType{extism.ValueTypePTR},
|
||||
[]extism.ValueType{extism.ValueTypePTR},
|
||||
)
|
||||
}
|
||||
|
||||
// configWriteResponse writes a JSON response to plugin memory.
|
||||
func configWriteResponse(p *extism.CurrentPlugin, stack []uint64, resp any) {
|
||||
respBytes, err := json.Marshal(resp)
|
||||
if err != nil {
|
||||
configWriteError(p, stack, err)
|
||||
return
|
||||
}
|
||||
respPtr, err := p.WriteBytes(respBytes)
|
||||
if err != nil {
|
||||
stack[0] = 0
|
||||
return
|
||||
}
|
||||
stack[0] = respPtr
|
||||
}
|
||||
|
||||
// configWriteError writes an error response to plugin memory.
|
||||
func configWriteError(p *extism.CurrentPlugin, stack []uint64, err error) {
|
||||
errResp := struct {
|
||||
Error string `json:"error"`
|
||||
}{Error: err.Error()}
|
||||
respBytes, _ := json.Marshal(errResp)
|
||||
respPtr, _ := p.WriteBytes(respBytes)
|
||||
stack[0] = respPtr
|
||||
}
|
||||
69
plugins/host_config.go
Normal file
69
plugins/host_config.go
Normal file
@ -0,0 +1,69 @@
|
||||
package plugins
|
||||
|
||||
import (
|
||||
"context"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/navidrome/navidrome/log"
|
||||
"github.com/navidrome/navidrome/plugins/host"
|
||||
)
|
||||
|
||||
// configServiceImpl implements the host.ConfigService interface.
|
||||
// It provides access to plugin configuration values set in the Navidrome config file.
|
||||
type configServiceImpl struct {
|
||||
pluginName string
|
||||
config map[string]string
|
||||
}
|
||||
|
||||
// newConfigService creates a new configServiceImpl instance.
|
||||
func newConfigService(pluginName string, config map[string]string) *configServiceImpl {
|
||||
if config == nil {
|
||||
config = make(map[string]string)
|
||||
}
|
||||
return &configServiceImpl{
|
||||
pluginName: pluginName,
|
||||
config: config,
|
||||
}
|
||||
}
|
||||
|
||||
// Get retrieves a configuration value as a string.
|
||||
func (s *configServiceImpl) Get(ctx context.Context, key string) (string, bool) {
|
||||
value, exists := s.config[key]
|
||||
log.Trace(ctx, "Config.Get", "plugin", s.pluginName, "key", key, "exists", exists)
|
||||
return value, exists
|
||||
}
|
||||
|
||||
// GetInt retrieves a configuration value as an integer.
|
||||
func (s *configServiceImpl) GetInt(ctx context.Context, key string) (int64, bool) {
|
||||
value, exists := s.config[key]
|
||||
if !exists {
|
||||
log.Trace(ctx, "Config.GetInt", "plugin", s.pluginName, "key", key, "exists", false)
|
||||
return 0, false
|
||||
}
|
||||
|
||||
intValue, err := strconv.ParseInt(value, 10, 64)
|
||||
if err != nil {
|
||||
log.Trace(ctx, "Config.GetInt parse error", "plugin", s.pluginName, "key", key, "value", value, "error", err)
|
||||
return 0, false
|
||||
}
|
||||
|
||||
log.Trace(ctx, "Config.GetInt", "plugin", s.pluginName, "key", key, "value", intValue)
|
||||
return intValue, true
|
||||
}
|
||||
|
||||
// List returns configuration keys matching the given prefix.
|
||||
func (s *configServiceImpl) List(ctx context.Context, prefix string) []string {
|
||||
keys := make([]string, 0, len(s.config))
|
||||
for k := range s.config {
|
||||
if prefix == "" || strings.HasPrefix(k, prefix) {
|
||||
keys = append(keys, k)
|
||||
}
|
||||
}
|
||||
sort.Strings(keys)
|
||||
log.Trace(ctx, "Config.List", "plugin", s.pluginName, "prefix", prefix, "keyCount", len(keys))
|
||||
return keys
|
||||
}
|
||||
|
||||
var _ host.ConfigService = (*configServiceImpl)(nil)
|
||||
312
plugins/host_config_test.go
Normal file
312
plugins/host_config_test.go
Normal file
@ -0,0 +1,312 @@
|
||||
//go:build !windows
|
||||
|
||||
package plugins
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/navidrome/navidrome/conf"
|
||||
"github.com/navidrome/navidrome/conf/configtest"
|
||||
"github.com/navidrome/navidrome/model"
|
||||
"github.com/navidrome/navidrome/tests"
|
||||
. "github.com/onsi/ginkgo/v2"
|
||||
. "github.com/onsi/gomega"
|
||||
)
|
||||
|
||||
var _ = Describe("ConfigService", func() {
|
||||
var service *configServiceImpl
|
||||
var ctx context.Context
|
||||
|
||||
BeforeEach(func() {
|
||||
ctx = context.Background()
|
||||
})
|
||||
|
||||
Describe("newConfigService", func() {
|
||||
It("creates service with provided config", func() {
|
||||
config := map[string]string{"key1": "value1", "key2": "value2"}
|
||||
service = newConfigService("test_plugin", config)
|
||||
Expect(service.pluginName).To(Equal("test_plugin"))
|
||||
Expect(service.config).To(Equal(config))
|
||||
})
|
||||
|
||||
It("creates service with empty config when nil", func() {
|
||||
service = newConfigService("test_plugin", nil)
|
||||
Expect(service.config).ToNot(BeNil())
|
||||
Expect(service.config).To(BeEmpty())
|
||||
})
|
||||
})
|
||||
|
||||
Describe("Get", func() {
|
||||
BeforeEach(func() {
|
||||
service = newConfigService("test_plugin", map[string]string{
|
||||
"api_key": "secret123",
|
||||
"debug_mode": "true",
|
||||
"max_items": "100",
|
||||
})
|
||||
})
|
||||
|
||||
It("returns value for existing key", func() {
|
||||
value, exists := service.Get(ctx, "api_key")
|
||||
Expect(exists).To(BeTrue())
|
||||
Expect(value).To(Equal("secret123"))
|
||||
})
|
||||
|
||||
It("returns not exists for missing key", func() {
|
||||
value, exists := service.Get(ctx, "missing_key")
|
||||
Expect(exists).To(BeFalse())
|
||||
Expect(value).To(Equal(""))
|
||||
})
|
||||
})
|
||||
|
||||
Describe("GetInt", func() {
|
||||
BeforeEach(func() {
|
||||
service = newConfigService("test_plugin", map[string]string{
|
||||
"max_items": "100",
|
||||
"timeout": "30",
|
||||
"negative": "-50",
|
||||
"not_a_number": "abc",
|
||||
"float": "3.14",
|
||||
})
|
||||
})
|
||||
|
||||
It("returns integer for valid numeric value", func() {
|
||||
value, exists := service.GetInt(ctx, "max_items")
|
||||
Expect(exists).To(BeTrue())
|
||||
Expect(value).To(Equal(int64(100)))
|
||||
})
|
||||
|
||||
It("returns negative integer", func() {
|
||||
value, exists := service.GetInt(ctx, "negative")
|
||||
Expect(exists).To(BeTrue())
|
||||
Expect(value).To(Equal(int64(-50)))
|
||||
})
|
||||
|
||||
It("returns not exists for non-numeric value", func() {
|
||||
value, exists := service.GetInt(ctx, "not_a_number")
|
||||
Expect(exists).To(BeFalse())
|
||||
Expect(value).To(Equal(int64(0)))
|
||||
})
|
||||
|
||||
It("returns not exists for float value", func() {
|
||||
value, exists := service.GetInt(ctx, "float")
|
||||
Expect(exists).To(BeFalse())
|
||||
Expect(value).To(Equal(int64(0)))
|
||||
})
|
||||
|
||||
It("returns not exists for missing key", func() {
|
||||
value, exists := service.GetInt(ctx, "missing_key")
|
||||
Expect(exists).To(BeFalse())
|
||||
Expect(value).To(Equal(int64(0)))
|
||||
})
|
||||
})
|
||||
|
||||
Describe("List", func() {
|
||||
BeforeEach(func() {
|
||||
service = newConfigService("test_plugin", map[string]string{
|
||||
"zebra": "z",
|
||||
"apple": "a",
|
||||
"banana": "b",
|
||||
"user_alice": "token1",
|
||||
"user_bob": "token2",
|
||||
"user_charlie": "token3",
|
||||
})
|
||||
})
|
||||
|
||||
It("returns all keys in sorted order when prefix is empty", func() {
|
||||
keys := service.List(ctx, "")
|
||||
Expect(keys).To(Equal([]string{"apple", "banana", "user_alice", "user_bob", "user_charlie", "zebra"}))
|
||||
})
|
||||
|
||||
It("returns only keys matching prefix", func() {
|
||||
keys := service.List(ctx, "user_")
|
||||
Expect(keys).To(Equal([]string{"user_alice", "user_bob", "user_charlie"}))
|
||||
})
|
||||
|
||||
It("returns empty slice when no keys match prefix", func() {
|
||||
keys := service.List(ctx, "nonexistent_")
|
||||
Expect(keys).To(BeEmpty())
|
||||
})
|
||||
|
||||
It("returns empty slice for empty config", func() {
|
||||
service = newConfigService("test_plugin", nil)
|
||||
keys := service.List(ctx, "")
|
||||
Expect(keys).To(BeEmpty())
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
var _ = Describe("ConfigService Integration", Ordered, func() {
|
||||
var (
|
||||
manager *Manager
|
||||
tmpDir string
|
||||
)
|
||||
|
||||
BeforeAll(func() {
|
||||
var err error
|
||||
tmpDir, err = os.MkdirTemp("", "config-test-*")
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
|
||||
// Copy the test-config plugin
|
||||
srcPath := filepath.Join(testdataDir, "test-config"+PackageExtension)
|
||||
destPath := filepath.Join(tmpDir, "test-config"+PackageExtension)
|
||||
data, err := os.ReadFile(srcPath)
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
err = os.WriteFile(destPath, data, 0600)
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
|
||||
// Compute SHA256 for the plugin
|
||||
hash := sha256.Sum256(data)
|
||||
hashHex := hex.EncodeToString(hash[:])
|
||||
|
||||
// Setup config
|
||||
DeferCleanup(configtest.SetupConfig())
|
||||
conf.Server.Plugins.Enabled = true
|
||||
conf.Server.Plugins.Folder = tmpDir
|
||||
conf.Server.Plugins.AutoReload = false
|
||||
conf.Server.CacheFolder = filepath.Join(tmpDir, "cache")
|
||||
|
||||
// Setup mock DataStore with pre-enabled plugin and config
|
||||
mockPluginRepo := tests.CreateMockPluginRepo()
|
||||
mockPluginRepo.Permitted = true
|
||||
mockPluginRepo.SetData(model.Plugins{{
|
||||
ID: "test-config",
|
||||
Path: destPath,
|
||||
SHA256: hashHex,
|
||||
Enabled: true,
|
||||
Config: `{"api_key":"test_secret","max_retries":"5","timeout":"30"}`,
|
||||
}})
|
||||
dataStore := &tests.MockDataStore{MockedPlugin: mockPluginRepo}
|
||||
|
||||
// Create and start manager
|
||||
manager = &Manager{
|
||||
plugins: make(map[string]*plugin),
|
||||
ds: dataStore,
|
||||
subsonicRouter: http.NotFoundHandler(),
|
||||
}
|
||||
err = manager.Start(GinkgoT().Context())
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
|
||||
DeferCleanup(func() {
|
||||
_ = manager.Stop()
|
||||
_ = os.RemoveAll(tmpDir)
|
||||
})
|
||||
})
|
||||
|
||||
Describe("Plugin Loading", func() {
|
||||
It("should load plugin without config permission", func() {
|
||||
manager.mu.RLock()
|
||||
p, ok := manager.plugins["test-config"]
|
||||
manager.mu.RUnlock()
|
||||
Expect(ok).To(BeTrue())
|
||||
// Config service doesn't require permission, so Permissions can be nil
|
||||
// Just verify the plugin loaded
|
||||
Expect(p.manifest.Name).To(Equal("Test Config Plugin"))
|
||||
})
|
||||
})
|
||||
|
||||
Describe("Config Operations via Plugin", func() {
|
||||
type testConfigInput struct {
|
||||
Operation string `json:"operation"`
|
||||
Key string `json:"key,omitempty"`
|
||||
Prefix string `json:"prefix,omitempty"`
|
||||
}
|
||||
type testConfigOutput struct {
|
||||
StringVal string `json:"string_val,omitempty"`
|
||||
IntVal int64 `json:"int_val,omitempty"`
|
||||
Keys []string `json:"keys,omitempty"`
|
||||
Exists bool `json:"exists,omitempty"`
|
||||
Error *string `json:"error,omitempty"`
|
||||
}
|
||||
|
||||
// Helper to call test plugin's exported function
|
||||
callTestConfig := func(ctx context.Context, input testConfigInput) (*testConfigOutput, error) {
|
||||
manager.mu.RLock()
|
||||
p := manager.plugins["test-config"]
|
||||
manager.mu.RUnlock()
|
||||
|
||||
instance, err := p.instance(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer instance.Close(ctx)
|
||||
|
||||
inputBytes, _ := json.Marshal(input)
|
||||
_, outputBytes, err := instance.Call("nd_test_config", inputBytes)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var output testConfigOutput
|
||||
if err := json.Unmarshal(outputBytes, &output); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if output.Error != nil {
|
||||
return nil, errors.New(*output.Error)
|
||||
}
|
||||
return &output, nil
|
||||
}
|
||||
|
||||
It("should get string value", func() {
|
||||
output, err := callTestConfig(GinkgoT().Context(), testConfigInput{
|
||||
Operation: "get",
|
||||
Key: "api_key",
|
||||
})
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(output.StringVal).To(Equal("test_secret"))
|
||||
Expect(output.Exists).To(BeTrue())
|
||||
})
|
||||
|
||||
It("should return not exists for missing key", func() {
|
||||
output, err := callTestConfig(GinkgoT().Context(), testConfigInput{
|
||||
Operation: "get",
|
||||
Key: "nonexistent",
|
||||
})
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(output.Exists).To(BeFalse())
|
||||
})
|
||||
|
||||
It("should get integer value", func() {
|
||||
output, err := callTestConfig(GinkgoT().Context(), testConfigInput{
|
||||
Operation: "get_int",
|
||||
Key: "max_retries",
|
||||
})
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(output.IntVal).To(Equal(int64(5)))
|
||||
Expect(output.Exists).To(BeTrue())
|
||||
})
|
||||
|
||||
It("should return not exists for non-integer value", func() {
|
||||
output, err := callTestConfig(GinkgoT().Context(), testConfigInput{
|
||||
Operation: "get_int",
|
||||
Key: "api_key", // This is a string, not an integer
|
||||
})
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(output.Exists).To(BeFalse())
|
||||
})
|
||||
|
||||
It("should list all config keys with empty prefix", func() {
|
||||
output, err := callTestConfig(GinkgoT().Context(), testConfigInput{
|
||||
Operation: "list",
|
||||
Prefix: "",
|
||||
})
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(output.Keys).To(ConsistOf("api_key", "max_retries", "timeout"))
|
||||
})
|
||||
|
||||
It("should list config keys with prefix filter", func() {
|
||||
output, err := callTestConfig(GinkgoT().Context(), testConfigInput{
|
||||
Operation: "list",
|
||||
Prefix: "max",
|
||||
})
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(output.Keys).To(ConsistOf("max_retries"))
|
||||
})
|
||||
})
|
||||
})
|
||||
@ -22,6 +22,7 @@ type serviceContext struct {
|
||||
pluginName string
|
||||
manager *Manager
|
||||
permissions *Permissions
|
||||
config map[string]string
|
||||
}
|
||||
|
||||
// hostServiceEntry defines a host service for table-driven registration.
|
||||
@ -34,6 +35,14 @@ type hostServiceEntry struct {
|
||||
// hostServices defines all available host services.
|
||||
// Adding a new host service only requires adding an entry here.
|
||||
var hostServices = []hostServiceEntry{
|
||||
{
|
||||
name: "Config",
|
||||
hasPermission: func(p *Permissions) bool { return true }, // Always available, no permission required
|
||||
create: func(ctx *serviceContext) ([]extism.HostFunction, io.Closer) {
|
||||
service := newConfigService(ctx.pluginName, ctx.config)
|
||||
return host.RegisterConfigHostFunctions(service), nil
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "SubsonicAPI",
|
||||
hasPermission: func(p *Permissions) bool { return p != nil && p.Subsonicapi != nil },
|
||||
@ -258,6 +267,7 @@ func (m *Manager) loadPluginWithConfig(name, ndpPath, configJSON string) error {
|
||||
pluginName: name,
|
||||
manager: m,
|
||||
permissions: pkg.Manifest.Permissions,
|
||||
config: pluginConfig,
|
||||
}
|
||||
for _, entry := range hostServices {
|
||||
if entry.hasPermission(pkg.Manifest.Permissions) {
|
||||
|
||||
@ -37,6 +37,7 @@ The following host services are available:
|
||||
|
||||
- Artwork: provides artwork public URL generation capabilities for plugins.
|
||||
- Cache: provides in-memory TTL-based caching capabilities for plugins.
|
||||
- Config: provides access to plugin configuration values.
|
||||
- KVStore: provides persistent key-value storage for plugins.
|
||||
- Library: provides access to music library metadata for plugins.
|
||||
- Scheduler: provides task scheduling capabilities for plugins.
|
||||
|
||||
161
plugins/pdk/go/host/nd_host_config.go
Normal file
161
plugins/pdk/go/host/nd_host_config.go
Normal file
@ -0,0 +1,161 @@
|
||||
// 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 host
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
|
||||
"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_getint is the host function provided by Navidrome.
|
||||
//
|
||||
//go:wasmimport extism:host/user config_getint
|
||||
func config_getint(uint64) uint64
|
||||
|
||||
// config_list is the host function provided by Navidrome.
|
||||
//
|
||||
//go:wasmimport extism:host/user config_list
|
||||
func config_list(uint64) uint64
|
||||
|
||||
type configGetRequest struct {
|
||||
Key string `json:"key"`
|
||||
}
|
||||
|
||||
type configGetResponse struct {
|
||||
Value string `json:"value,omitempty"`
|
||||
Exists bool `json:"exists,omitempty"`
|
||||
}
|
||||
|
||||
type configGetIntRequest struct {
|
||||
Key string `json:"key"`
|
||||
}
|
||||
|
||||
type configGetIntResponse struct {
|
||||
Value int64 `json:"value,omitempty"`
|
||||
Exists bool `json:"exists,omitempty"`
|
||||
}
|
||||
|
||||
type configListRequest struct {
|
||||
Prefix string `json:"prefix"`
|
||||
}
|
||||
|
||||
type configListResponse struct {
|
||||
Keys []string `json:"keys,omitempty"`
|
||||
}
|
||||
|
||||
// ConfigGet calls the config_get host function.
|
||||
// Get retrieves a configuration value as a string.
|
||||
//
|
||||
// Parameters:
|
||||
// - key: The configuration key
|
||||
//
|
||||
// Returns the value and whether the key exists.
|
||||
func ConfigGet(key string) (string, bool) {
|
||||
// Marshal request to JSON
|
||||
req := configGetRequest{
|
||||
Key: key,
|
||||
}
|
||||
reqBytes, err := json.Marshal(req)
|
||||
if err != nil {
|
||||
return "", false
|
||||
}
|
||||
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
|
||||
}
|
||||
|
||||
return response.Value, response.Exists
|
||||
}
|
||||
|
||||
// ConfigGetInt calls the config_getint host function.
|
||||
// GetInt retrieves a configuration value as an integer.
|
||||
//
|
||||
// Parameters:
|
||||
// - key: The configuration key
|
||||
//
|
||||
// Returns the value and whether the key exists. If the key exists but the
|
||||
// value cannot be parsed as an integer, exists will be false.
|
||||
func ConfigGetInt(key string) (int64, bool) {
|
||||
// Marshal request to JSON
|
||||
req := configGetIntRequest{
|
||||
Key: key,
|
||||
}
|
||||
reqBytes, err := json.Marshal(req)
|
||||
if err != nil {
|
||||
return 0, false
|
||||
}
|
||||
reqMem := pdk.AllocateBytes(reqBytes)
|
||||
defer reqMem.Free()
|
||||
|
||||
// Call the host function
|
||||
responsePtr := config_getint(reqMem.Offset())
|
||||
|
||||
// Read the response from memory
|
||||
responseMem := pdk.FindMemory(responsePtr)
|
||||
responseBytes := responseMem.ReadBytes()
|
||||
|
||||
// Parse the response
|
||||
var response configGetIntResponse
|
||||
if err := json.Unmarshal(responseBytes, &response); err != nil {
|
||||
return 0, false
|
||||
}
|
||||
|
||||
return response.Value, response.Exists
|
||||
}
|
||||
|
||||
// ConfigList calls the config_list host function.
|
||||
// List returns configuration keys matching the given prefix.
|
||||
//
|
||||
// Parameters:
|
||||
// - prefix: Key prefix to filter by. If empty, returns all keys.
|
||||
//
|
||||
// Returns a sorted slice of matching configuration keys.
|
||||
func ConfigList(prefix string) []string {
|
||||
// Marshal request to JSON
|
||||
req := configListRequest{
|
||||
Prefix: prefix,
|
||||
}
|
||||
reqBytes, err := json.Marshal(req)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
reqMem := pdk.AllocateBytes(reqBytes)
|
||||
defer reqMem.Free()
|
||||
|
||||
// Call the host function
|
||||
responsePtr := config_list(reqMem.Offset())
|
||||
|
||||
// Read the response from memory
|
||||
responseMem := pdk.FindMemory(responsePtr)
|
||||
responseBytes := responseMem.ReadBytes()
|
||||
|
||||
// Parse the response
|
||||
var response configListResponse
|
||||
if err := json.Unmarshal(responseBytes, &response); err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
return response.Keys
|
||||
}
|
||||
72
plugins/pdk/go/host/nd_host_config_stub.go
Normal file
72
plugins/pdk/go/host/nd_host_config_stub.go
Normal file
@ -0,0 +1,72 @@
|
||||
// Code generated by ndpgen. DO NOT EDIT.
|
||||
//
|
||||
// This file contains mock implementations for non-WASM builds.
|
||||
// These mocks allow IDE support, compilation, and unit testing on non-WASM platforms.
|
||||
// Plugin authors can use the exported mock instances to set expectations in tests.
|
||||
//
|
||||
//go:build !wasip1
|
||||
|
||||
package host
|
||||
|
||||
import "github.com/stretchr/testify/mock"
|
||||
|
||||
// mockConfigService is the mock implementation for testing.
|
||||
type mockConfigService struct {
|
||||
mock.Mock
|
||||
}
|
||||
|
||||
// ConfigMock is the auto-instantiated mock instance for testing.
|
||||
// Use this to set expectations: host.ConfigMock.On("MethodName", args...).Return(values...)
|
||||
var ConfigMock = &mockConfigService{}
|
||||
|
||||
// Get is the mock method for ConfigGet.
|
||||
func (m *mockConfigService) Get(key string) (string, bool) {
|
||||
args := m.Called(key)
|
||||
return args.String(0), args.Bool(1)
|
||||
}
|
||||
|
||||
// ConfigGet delegates to the mock instance.
|
||||
// Get retrieves a configuration value as a string.
|
||||
//
|
||||
// Parameters:
|
||||
// - key: The configuration key
|
||||
//
|
||||
// Returns the value and whether the key exists.
|
||||
func ConfigGet(key string) (string, bool) {
|
||||
return ConfigMock.Get(key)
|
||||
}
|
||||
|
||||
// GetInt is the mock method for ConfigGetInt.
|
||||
func (m *mockConfigService) GetInt(key string) (int64, bool) {
|
||||
args := m.Called(key)
|
||||
return args.Get(0).(int64), args.Bool(1)
|
||||
}
|
||||
|
||||
// ConfigGetInt delegates to the mock instance.
|
||||
// GetInt retrieves a configuration value as an integer.
|
||||
//
|
||||
// Parameters:
|
||||
// - key: The configuration key
|
||||
//
|
||||
// Returns the value and whether the key exists. If the key exists but the
|
||||
// value cannot be parsed as an integer, exists will be false.
|
||||
func ConfigGetInt(key string) (int64, bool) {
|
||||
return ConfigMock.GetInt(key)
|
||||
}
|
||||
|
||||
// List is the mock method for ConfigList.
|
||||
func (m *mockConfigService) List(prefix string) []string {
|
||||
args := m.Called(prefix)
|
||||
return args.Get(0).([]string)
|
||||
}
|
||||
|
||||
// ConfigList delegates to the mock instance.
|
||||
// List returns configuration keys matching the given prefix.
|
||||
//
|
||||
// Parameters:
|
||||
// - prefix: Key prefix to filter by. If empty, returns all keys.
|
||||
//
|
||||
// Returns a sorted slice of matching configuration keys.
|
||||
func ConfigList(prefix string) []string {
|
||||
return ConfigMock.List(prefix)
|
||||
}
|
||||
145
plugins/pdk/python/host/nd_host_config.py
Normal file
145
plugins/pdk/python/host/nd_host_config.py
Normal file
@ -0,0 +1,145 @@
|
||||
# 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_getint")
|
||||
def _config_getint(offset: int) -> int:
|
||||
"""Raw host function - do not call directly."""
|
||||
...
|
||||
|
||||
|
||||
@extism.import_fn("extism:host/user", "config_list")
|
||||
def _config_list(offset: int) -> int:
|
||||
"""Raw host function - do not call directly."""
|
||||
...
|
||||
|
||||
|
||||
@dataclass
|
||||
class ConfigGetResult:
|
||||
"""Result type for config_get."""
|
||||
value: str
|
||||
exists: bool
|
||||
|
||||
|
||||
@dataclass
|
||||
class ConfigGetIntResult:
|
||||
"""Result type for config_get_int."""
|
||||
value: int
|
||||
exists: bool
|
||||
|
||||
|
||||
def config_get(key: str) -> ConfigGetResult:
|
||||
"""Get retrieves a configuration value as a string.
|
||||
|
||||
Parameters:
|
||||
- key: The configuration key
|
||||
|
||||
Returns the value and whether the key exists.
|
||||
|
||||
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))
|
||||
|
||||
return ConfigGetResult(
|
||||
value=response.get("value", ""),
|
||||
exists=response.get("exists", False),
|
||||
)
|
||||
|
||||
|
||||
def config_get_int(key: str) -> ConfigGetIntResult:
|
||||
"""GetInt retrieves a configuration value as an integer.
|
||||
|
||||
Parameters:
|
||||
- key: The configuration key
|
||||
|
||||
Returns the value and whether the key exists. If the key exists but the
|
||||
value cannot be parsed as an integer, exists will be false.
|
||||
|
||||
Args:
|
||||
key: str parameter.
|
||||
|
||||
Returns:
|
||||
ConfigGetIntResult 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_getint(request_mem.offset)
|
||||
response_mem = extism.memory.find(response_offset)
|
||||
response = json.loads(extism.memory.string(response_mem))
|
||||
|
||||
return ConfigGetIntResult(
|
||||
value=response.get("value", 0),
|
||||
exists=response.get("exists", False),
|
||||
)
|
||||
|
||||
|
||||
def config_list(prefix: str) -> Any:
|
||||
"""List returns configuration keys matching the given prefix.
|
||||
|
||||
Parameters:
|
||||
- prefix: Key prefix to filter by. If empty, returns all keys.
|
||||
|
||||
Returns a sorted slice of matching configuration keys.
|
||||
|
||||
Args:
|
||||
prefix: str parameter.
|
||||
|
||||
Returns:
|
||||
Any: The result value.
|
||||
|
||||
Raises:
|
||||
HostFunctionError: If the host function returns an error.
|
||||
"""
|
||||
request = {
|
||||
"prefix": prefix,
|
||||
}
|
||||
request_bytes = json.dumps(request).encode("utf-8")
|
||||
request_mem = extism.memory.alloc(request_bytes)
|
||||
response_offset = _config_list(request_mem.offset)
|
||||
response_mem = extism.memory.find(response_offset)
|
||||
response = json.loads(extism.memory.string(response_mem))
|
||||
|
||||
return response.get("keys", None)
|
||||
@ -34,6 +34,7 @@
|
||||
//!
|
||||
//! - [`artwork`] - provides artwork public URL generation capabilities for plugins.
|
||||
//! - [`cache`] - provides in-memory TTL-based caching capabilities for plugins.
|
||||
//! - [`config`] - provides access to plugin configuration values.
|
||||
//! - [`kvstore`] - provides persistent key-value storage for plugins.
|
||||
//! - [`library`] - provides access to music library metadata for plugins.
|
||||
//! - [`scheduler`] - provides task scheduling capabilities for plugins.
|
||||
@ -54,6 +55,13 @@ pub mod cache {
|
||||
pub use super::nd_host_cache::*;
|
||||
}
|
||||
|
||||
#[doc(hidden)]
|
||||
mod nd_host_config;
|
||||
/// provides access to plugin configuration values.
|
||||
pub mod config {
|
||||
pub use super::nd_host_config::*;
|
||||
}
|
||||
|
||||
#[doc(hidden)]
|
||||
mod nd_host_kvstore;
|
||||
/// provides persistent key-value storage for plugins.
|
||||
|
||||
133
plugins/pdk/rust/nd-pdk-host/src/nd_host_config.rs
Normal file
133
plugins/pdk/rust/nd-pdk-host/src/nd_host_config.rs
Normal file
@ -0,0 +1,133 @@
|
||||
// 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,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct ConfigGetIntRequest {
|
||||
key: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct ConfigGetIntResponse {
|
||||
#[serde(default)]
|
||||
value: i64,
|
||||
#[serde(default)]
|
||||
exists: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct ConfigListRequest {
|
||||
prefix: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct ConfigListResponse {
|
||||
#[serde(default)]
|
||||
keys: Vec<String>,
|
||||
}
|
||||
|
||||
#[host_fn]
|
||||
extern "ExtismHost" {
|
||||
fn config_get(input: Json<ConfigGetRequest>) -> Json<ConfigGetResponse>;
|
||||
fn config_getint(input: Json<ConfigGetIntRequest>) -> Json<ConfigGetIntResponse>;
|
||||
fn config_list(input: Json<ConfigListRequest>) -> Json<ConfigListResponse>;
|
||||
}
|
||||
|
||||
/// Get retrieves a configuration value as a string.
|
||||
///
|
||||
/// Parameters:
|
||||
/// - key: The configuration key
|
||||
///
|
||||
/// Returns the value and whether the key exists.
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `key` - String parameter.
|
||||
///
|
||||
/// # Returns
|
||||
/// A tuple of (value, exists).
|
||||
///
|
||||
/// # Errors
|
||||
/// Returns an error if the host function call fails.
|
||||
pub fn get(key: &str) -> Result<(String, bool), Error> {
|
||||
let response = unsafe {
|
||||
config_get(Json(ConfigGetRequest {
|
||||
key: key.to_owned(),
|
||||
}))?
|
||||
};
|
||||
|
||||
Ok((response.0.value, response.0.exists))
|
||||
}
|
||||
|
||||
/// GetInt retrieves a configuration value as an integer.
|
||||
///
|
||||
/// Parameters:
|
||||
/// - key: The configuration key
|
||||
///
|
||||
/// Returns the value and whether the key exists. If the key exists but the
|
||||
/// value cannot be parsed as an integer, exists will be false.
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `key` - String parameter.
|
||||
///
|
||||
/// # Returns
|
||||
/// A tuple of (value, exists).
|
||||
///
|
||||
/// # Errors
|
||||
/// Returns an error if the host function call fails.
|
||||
pub fn get_int(key: &str) -> Result<(i64, bool), Error> {
|
||||
let response = unsafe {
|
||||
config_getint(Json(ConfigGetIntRequest {
|
||||
key: key.to_owned(),
|
||||
}))?
|
||||
};
|
||||
|
||||
Ok((response.0.value, response.0.exists))
|
||||
}
|
||||
|
||||
/// List returns configuration keys matching the given prefix.
|
||||
///
|
||||
/// Parameters:
|
||||
/// - prefix: Key prefix to filter by. If empty, returns all keys.
|
||||
///
|
||||
/// Returns a sorted slice of matching configuration keys.
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `prefix` - String parameter.
|
||||
///
|
||||
/// # Returns
|
||||
/// The keys value.
|
||||
///
|
||||
/// # Errors
|
||||
/// Returns an error if the host function call fails.
|
||||
pub fn list(prefix: &str) -> Result<Vec<String>, Error> {
|
||||
let response = unsafe {
|
||||
config_list(Json(ConfigListRequest {
|
||||
prefix: prefix.to_owned(),
|
||||
}))?
|
||||
};
|
||||
|
||||
Ok(response.0.keys)
|
||||
}
|
||||
16
plugins/testdata/test-config/go.mod
vendored
Normal file
16
plugins/testdata/test-config/go.mod
vendored
Normal file
@ -0,0 +1,16 @@
|
||||
module test-config
|
||||
|
||||
go 1.25
|
||||
|
||||
require github.com/navidrome/navidrome/plugins/pdk/go v0.0.0
|
||||
|
||||
require (
|
||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||
github.com/extism/go-pdk v1.1.3 // indirect
|
||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||
github.com/stretchr/objx v0.5.2 // indirect
|
||||
github.com/stretchr/testify v1.11.1 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
)
|
||||
|
||||
replace github.com/navidrome/navidrome/plugins/pdk/go => ../../pdk/go
|
||||
14
plugins/testdata/test-config/go.sum
vendored
Normal file
14
plugins/testdata/test-config/go.sum
vendored
Normal file
@ -0,0 +1,14 @@
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/extism/go-pdk v1.1.3 h1:hfViMPWrqjN6u67cIYRALZTZLk/enSPpNKa+rZ9X2SQ=
|
||||
github.com/extism/go-pdk v1.1.3/go.mod h1:Gz+LIU/YCKnKXhgge8yo5Yu1F/lbv7KtKFkiCSzW/P4=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY=
|
||||
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
|
||||
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
|
||||
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
60
plugins/testdata/test-config/main.go
vendored
Normal file
60
plugins/testdata/test-config/main.go
vendored
Normal file
@ -0,0 +1,60 @@
|
||||
// Test Config plugin for Navidrome plugin system integration tests.
|
||||
// Build with: tinygo build -o ../test-config.wasm -target wasip1 -buildmode=c-shared .
|
||||
package main
|
||||
|
||||
import (
|
||||
"github.com/navidrome/navidrome/plugins/pdk/go/host"
|
||||
"github.com/navidrome/navidrome/plugins/pdk/go/pdk"
|
||||
)
|
||||
|
||||
// TestConfigInput is the input for nd_test_config callback.
|
||||
type TestConfigInput struct {
|
||||
Operation string `json:"operation"` // "get", "get_int", "list"
|
||||
Key string `json:"key"` // For get/get_int operations
|
||||
Prefix string `json:"prefix"` // For list operation
|
||||
}
|
||||
|
||||
// TestConfigOutput is the output from nd_test_config callback.
|
||||
type TestConfigOutput struct {
|
||||
StringVal string `json:"string_val,omitempty"`
|
||||
IntVal int64 `json:"int_val,omitempty"`
|
||||
Keys []string `json:"keys,omitempty"`
|
||||
Exists bool `json:"exists,omitempty"`
|
||||
Error *string `json:"error,omitempty"`
|
||||
}
|
||||
|
||||
// nd_test_config is the test callback that tests the config host functions.
|
||||
//
|
||||
//go:wasmexport nd_test_config
|
||||
func ndTestConfig() int32 {
|
||||
var input TestConfigInput
|
||||
if err := pdk.InputJSON(&input); err != nil {
|
||||
errStr := err.Error()
|
||||
pdk.OutputJSON(TestConfigOutput{Error: &errStr})
|
||||
return 0
|
||||
}
|
||||
|
||||
switch input.Operation {
|
||||
case "get":
|
||||
value, exists := host.ConfigGet(input.Key)
|
||||
pdk.OutputJSON(TestConfigOutput{StringVal: value, Exists: exists})
|
||||
return 0
|
||||
|
||||
case "get_int":
|
||||
value, exists := host.ConfigGetInt(input.Key)
|
||||
pdk.OutputJSON(TestConfigOutput{IntVal: value, Exists: exists})
|
||||
return 0
|
||||
|
||||
case "list":
|
||||
keys := host.ConfigList(input.Prefix)
|
||||
pdk.OutputJSON(TestConfigOutput{Keys: keys})
|
||||
return 0
|
||||
|
||||
default:
|
||||
errStr := "unknown operation: " + input.Operation
|
||||
pdk.OutputJSON(TestConfigOutput{Error: &errStr})
|
||||
return 0
|
||||
}
|
||||
}
|
||||
|
||||
func main() {}
|
||||
6
plugins/testdata/test-config/manifest.json
vendored
Normal file
6
plugins/testdata/test-config/manifest.json
vendored
Normal file
@ -0,0 +1,6 @@
|
||||
{
|
||||
"name": "Test Config Plugin",
|
||||
"author": "Navidrome Test",
|
||||
"version": "1.0.0",
|
||||
"description": "A test plugin for config service integration testing"
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user