feat: renderer
This commit is contained in:
7
validator/go.mod
Normal file
7
validator/go.mod
Normal file
@@ -0,0 +1,7 @@
|
||||
module git.max-richter.dev/max/marka/validator
|
||||
|
||||
go 1.24.5
|
||||
|
||||
require github.com/santhosh-tekuri/jsonschema/v6 v6.0.2
|
||||
|
||||
require golang.org/x/text v0.14.0 // indirect
|
4
validator/go.sum
Normal file
4
validator/go.sum
Normal file
@@ -0,0 +1,4 @@
|
||||
github.com/santhosh-tekuri/jsonschema/v6 v6.0.2 h1:KRzFb2m7YtdldCEkzs6KqmJw4nqEVZGK7IN2kJkjTuQ=
|
||||
github.com/santhosh-tekuri/jsonschema/v6 v6.0.2/go.mod h1:JXeL+ps8p7/KNMjDQk3TCwPpBy0wYklyWTfbkIzdIFU=
|
||||
golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
|
||||
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
93
validator/validator.go
Normal file
93
validator/validator.go
Normal file
@@ -0,0 +1,93 @@
|
||||
// Package validator provides a validator for the marka data.
|
||||
package validator
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"git.max-richter.dev/max/marka/registry"
|
||||
"github.com/santhosh-tekuri/jsonschema/v6"
|
||||
)
|
||||
|
||||
var (
|
||||
loadOnce sync.Once
|
||||
compiler *jsonschema.Compiler
|
||||
compileErr error
|
||||
schemaCache sync.Map
|
||||
)
|
||||
|
||||
// ValidateSchema validates instance against the Schema.org JSON Schema named `schemaName`.
|
||||
// Examples: ValidateSchema(inst, "Recipe"), ValidateSchema(inst, "schema:Recipe").
|
||||
func ValidateSchema(instance any, schemaName string) error {
|
||||
if err := ensureCompiler(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
ref := normalizeRef(schemaName)
|
||||
|
||||
if v, ok := schemaCache.Load(ref); ok {
|
||||
if err := v.(*jsonschema.Schema).Validate(instance); err != nil {
|
||||
return fmt.Errorf("validation failed: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
sch, err := compiler.Compile(ref)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to compile schema %q: %w", ref, err)
|
||||
}
|
||||
schemaCache.Store(ref, sch)
|
||||
|
||||
if err := sch.Validate(instance); err != nil {
|
||||
return fmt.Errorf("validation failed: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func ensureCompiler() error {
|
||||
loadOnce.Do(func() {
|
||||
c := jsonschema.NewCompiler()
|
||||
|
||||
rawSchemas, err := registry.GetSchemas()
|
||||
if err != nil {
|
||||
compileErr = fmt.Errorf("read schema directory: %w", err)
|
||||
return
|
||||
}
|
||||
|
||||
for _, rawSchema := range rawSchemas {
|
||||
js, err := jsonschema.UnmarshalJSON(bytes.NewReader(rawSchema))
|
||||
if err != nil {
|
||||
compileErr = err
|
||||
return
|
||||
}
|
||||
|
||||
id := extractID(rawSchema)
|
||||
if err := c.AddResource(id, js); err != nil {
|
||||
compileErr = fmt.Errorf("add resource %s: %w", id, err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
compiler = c
|
||||
})
|
||||
return compileErr
|
||||
}
|
||||
|
||||
func extractID(raw []byte) string {
|
||||
var tmp struct {
|
||||
ID string `json:"$id"`
|
||||
}
|
||||
_ = json.Unmarshal(raw, &tmp)
|
||||
return strings.TrimSpace(tmp.ID)
|
||||
}
|
||||
|
||||
func normalizeRef(name string) string {
|
||||
n := strings.TrimSpace(name)
|
||||
if strings.HasPrefix(n, "schema:") || strings.HasPrefix(n, "http://") || strings.HasPrefix(n, "https://") {
|
||||
return n
|
||||
}
|
||||
return "schema:" + n
|
||||
}
|
38
validator/validator_test.go
Normal file
38
validator/validator_test.go
Normal file
@@ -0,0 +1,38 @@
|
||||
package validator_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"git.max-richter.dev/max/marka/validator"
|
||||
)
|
||||
|
||||
func TestValidateRecipe_InvalidType(t *testing.T) {
|
||||
recipe := map[string]any{
|
||||
"@type": "Recipe",
|
||||
"recipeYield": 4,
|
||||
"recipeIngredient": []string{
|
||||
"500 g flour",
|
||||
"300 ml water",
|
||||
},
|
||||
"recipeInstructions": "Mix and bake.",
|
||||
}
|
||||
|
||||
if err := validator.ValidateSchema(recipe, "schema:Recipe"); err == nil {
|
||||
t.Fatalf("expected validation error for invalid recipe, got nil")
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidateRecipe_Valid(t *testing.T) {
|
||||
recipe := map[string]any{
|
||||
"@type": "Recipe",
|
||||
"name": "Simple Bread",
|
||||
"cookTime": "PT30M",
|
||||
"recipeIngredient": []any{"500 g flour", "300 ml water", "10 g salt", "3 g yeast"},
|
||||
"recipeInstructions": "Mix ingredients, let dough rise, bake at 220°C for 35 minutes.",
|
||||
"recipeYield": "1 loaf",
|
||||
}
|
||||
|
||||
if err := validator.ValidateSchema(recipe, "schema:Recipe"); err != nil {
|
||||
t.Fatalf("expected valid recipe, got error: %v", err)
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user