mirror of
https://github.com/navidrome/navidrome.git
synced 2026-02-07 05:21:22 -05:00
* feat(plugins): add JSONForms schema for plugin configuration Signed-off-by: Deluan <deluan@navidrome.org> * feat: enhance error handling by formatting validation errors with field names Signed-off-by: Deluan <deluan@navidrome.org> * feat: enforce required fields in config validation and improve error handling Signed-off-by: Deluan <deluan@navidrome.org> * format JS code Signed-off-by: Deluan <deluan@navidrome.org> * feat: add config schema validation and enhance manifest structure Signed-off-by: Deluan <deluan@navidrome.org> * feat: refactor plugin config parsing and add unit tests Signed-off-by: Deluan <deluan@navidrome.org> * feat: add config validation error message in Portuguese * feat: enhance AlwaysExpandedArrayLayout with description support and improve array control testing Signed-off-by: Deluan <deluan@navidrome.org> * feat: update Discord Rust plugin configuration to use JSONForm for user tokens and enhance schema validation Signed-off-by: Deluan <deluan@navidrome.org> * fix: resolve React Hooks linting issues in plugin UI components * Apply suggestions from code review Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com> * format code Signed-off-by: Deluan <deluan@navidrome.org> * feat: migrate schema validation to use santhosh-tekuri/jsonschema and improve error formatting Signed-off-by: Deluan <deluan@navidrome.org> * address PR comments Signed-off-by: Deluan <deluan@navidrome.org> * fix flaky test Signed-off-by: Deluan <deluan@navidrome.org> * feat: enhance array layout and configuration handling with AJV defaults Signed-off-by: Deluan <deluan@navidrome.org> * feat: implement custom tester to exclude enum arrays from AlwaysExpandedArrayLayout Signed-off-by: Deluan <deluan@navidrome.org> * feat: add error boundary for schema rendering and improve error messages Signed-off-by: Deluan <deluan@navidrome.org> * feat: refine non-enum array control logic by utilizing JSONForms schema resolution Signed-off-by: Deluan <deluan@navidrome.org> * feat: add error styling to ToggleEnabledSwitch for disabled state Signed-off-by: Deluan <deluan@navidrome.org> * feat: adjust label positioning and styling in SchemaConfigEditor for improved layout Signed-off-by: Deluan <deluan@navidrome.org> * feat: implement outlined input controls renderers to replace custom fragile CSS Signed-off-by: Deluan <deluan@navidrome.org> * feat: remove margin from last form control inside array items for better spacing Signed-off-by: Deluan <deluan@navidrome.org> * feat: enhance AJV error handling to transform required errors for field-level validation Signed-off-by: Deluan <deluan@navidrome.org> * feat: set default value for User Tokens in manifest.json to improve user experience Signed-off-by: Deluan <deluan@navidrome.org> * format Signed-off-by: Deluan <deluan@navidrome.org> * feat: add margin to outlined input controls for improved spacing Signed-off-by: Deluan <deluan@navidrome.org> * feat: remove redundant margin rule for last form control in array items Signed-off-by: Deluan <deluan@navidrome.org> * feat: adjust font size of label elements in SchemaConfigEditor for improved readability Signed-off-by: Deluan <deluan@navidrome.org> --------- Signed-off-by: Deluan <deluan@navidrome.org> Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com>
130 lines
3.4 KiB
Go
130 lines
3.4 KiB
Go
package plugins
|
|
|
|
import (
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"strings"
|
|
|
|
"github.com/santhosh-tekuri/jsonschema/v6"
|
|
)
|
|
|
|
// ConfigValidationError represents a validation error with field path and message.
|
|
type ConfigValidationError struct {
|
|
Field string `json:"field"`
|
|
Message string `json:"message"`
|
|
}
|
|
|
|
// ConfigValidationErrors is a collection of validation errors.
|
|
type ConfigValidationErrors struct {
|
|
Errors []ConfigValidationError `json:"errors"`
|
|
}
|
|
|
|
func (e *ConfigValidationErrors) Error() string {
|
|
if len(e.Errors) == 0 {
|
|
return "validation failed"
|
|
}
|
|
var msgs []string
|
|
for _, err := range e.Errors {
|
|
if err.Field != "" {
|
|
msgs = append(msgs, fmt.Sprintf("%s: %s", err.Field, err.Message))
|
|
} else {
|
|
msgs = append(msgs, err.Message)
|
|
}
|
|
}
|
|
return strings.Join(msgs, "; ")
|
|
}
|
|
|
|
// ValidateConfig validates a config JSON string against a plugin's config schema.
|
|
// If the manifest has no config schema, it returns an error indicating the plugin
|
|
// has no configurable options.
|
|
// Returns nil if validation passes, ConfigValidationErrors if validation fails.
|
|
func ValidateConfig(manifest *Manifest, configJSON string) error {
|
|
// If no config schema defined, plugin has no configurable options
|
|
if !manifest.HasConfigSchema() {
|
|
return fmt.Errorf("plugin has no configurable options")
|
|
}
|
|
|
|
// Parse the config JSON (empty string treated as empty object)
|
|
var configData any
|
|
if configJSON == "" {
|
|
configData = map[string]any{}
|
|
} else {
|
|
if err := json.Unmarshal([]byte(configJSON), &configData); err != nil {
|
|
return &ConfigValidationErrors{
|
|
Errors: []ConfigValidationError{{
|
|
Message: fmt.Sprintf("invalid JSON: %v", err),
|
|
}},
|
|
}
|
|
}
|
|
}
|
|
|
|
// Compile the schema
|
|
compiler := jsonschema.NewCompiler()
|
|
if err := compiler.AddResource("schema.json", manifest.Config.Schema); err != nil {
|
|
return fmt.Errorf("adding schema resource: %w", err)
|
|
}
|
|
|
|
schema, err := compiler.Compile("schema.json")
|
|
if err != nil {
|
|
return fmt.Errorf("compiling schema: %w", err)
|
|
}
|
|
|
|
// Validate config against schema
|
|
if err := schema.Validate(configData); err != nil {
|
|
return convertValidationError(err)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// convertValidationError converts jsonschema validation errors to our format.
|
|
func convertValidationError(err error) *ConfigValidationErrors {
|
|
var validationErr *jsonschema.ValidationError
|
|
if !errors.As(err, &validationErr) {
|
|
return &ConfigValidationErrors{
|
|
Errors: []ConfigValidationError{{
|
|
Message: err.Error(),
|
|
}},
|
|
}
|
|
}
|
|
|
|
var configErrors []ConfigValidationError
|
|
collectErrors(validationErr, &configErrors)
|
|
|
|
if len(configErrors) == 0 {
|
|
configErrors = append(configErrors, ConfigValidationError{
|
|
Message: validationErr.Error(),
|
|
})
|
|
}
|
|
|
|
return &ConfigValidationErrors{Errors: configErrors}
|
|
}
|
|
|
|
// collectErrors recursively collects validation errors from the error tree.
|
|
func collectErrors(err *jsonschema.ValidationError, errors *[]ConfigValidationError) {
|
|
// If there are child errors, collect from them
|
|
if len(err.Causes) > 0 {
|
|
for _, cause := range err.Causes {
|
|
collectErrors(cause, errors)
|
|
}
|
|
return
|
|
}
|
|
|
|
// Leaf error - add it
|
|
field := ""
|
|
if len(err.InstanceLocation) > 0 {
|
|
field = strings.Join(err.InstanceLocation, "/")
|
|
}
|
|
|
|
*errors = append(*errors, ConfigValidationError{
|
|
Field: field,
|
|
Message: err.Error(),
|
|
})
|
|
}
|
|
|
|
// HasConfigSchema returns true if the manifest defines a config schema.
|
|
func (m *Manifest) HasConfigSchema() bool {
|
|
return m.Config != nil && m.Config.Schema != nil
|
|
}
|