feat: renderer

This commit is contained in:
2025-08-19 17:20:24 +02:00
parent 6db87db325
commit 210b31aef8
38 changed files with 727 additions and 299 deletions

7
validator/go.mod Normal file
View 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
View 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
View 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
}

View 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)
}
}