package plugins
import (
"encoding/json"
"errors"
"fmt"
"strings"
"github.com/santhosh-tekuri/jsonschema/v6"
)
type ConfigValidationError struct {
Field string `json:"field"`
Message string `json:"message"`
}
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, "; ")
}
func ValidateConfig(manifest *Manifest, configJSON string) error {
if !manifest.HasConfigSchema() {
return fmt.Errorf("plugin has no configurable options")
}
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),
}},
}
}
}
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)
}
if err := schema.Validate(configData); err != nil {
return convertValidationError(err)
}
return nil
}
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}
}
func collectErrors(err *jsonschema.ValidationError, errors *[]ConfigValidationError) {
if len(err.Causes) > 0 {
for _, cause := range err.Causes {
collectErrors(cause, errors)
}
return
}
field := ""
if len(err.InstanceLocation) > 0 {
field = strings.Join(err.InstanceLocation, "/")
}
*errors = append(*errors, ConfigValidationError{
Field: field,
Message: err.Error(),
})
}
func (m *Manifest) HasConfigSchema() bool {
return m.Config != nil && m.Config.Schema != nil
}