package registry import ( "bytes" "embed" "encoding/json" "fmt" "path/filepath" "strings" "sync" "github.com/santhosh-tekuri/jsonschema/v6" ) //go:embed schema-org/*.json var schemasFS embed.FS var ( loadOnce sync.Once compiler *jsonschema.Compiler compileErr error schemaCache sync.Map // map[string]*jsonschema.Schema ) // 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) // Fast-path: reuse compiled schema if we have it. 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 } // Compile on first use, then cache. 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 } // --- internals --- func ensureCompiler() error { loadOnce.Do(func() { c := jsonschema.NewCompiler() // Load all embedded schemas and register them under their $id (e.g., "schema:Recipe"). entries, err := schemasFS.ReadDir("schema-org") if err != nil { compileErr = fmt.Errorf("read schema directory: %w", err) return } for _, e := range entries { if e.IsDir() || !strings.HasSuffix(e.Name(), ".json") { continue } raw, err := schemasFS.ReadFile(filepath.ToSlash("schema-org/" + e.Name())) if err != nil { compileErr = fmt.Errorf("read %s: %w", e.Name(), err) return } // Unmarshal once for the compiler, but use $id from the raw JSON as the resource name. js, err := jsonschema.UnmarshalJSON(bytes.NewReader(raw)) if err != nil { compileErr = fmt.Errorf("unmarshal %s: %w", e.Name(), err) return } id := extractID(raw) if id == "" { // Fallbacks if $id is missing; Schema.org dumps typically use "schema:". base := strings.TrimSuffix(e.Name(), ".json") id = "schema:" + base } 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) // Accept "Recipe" or "schema:Recipe" transparently. if strings.HasPrefix(n, "schema:") || strings.HasPrefix(n, "http://") || strings.HasPrefix(n, "https://") { return n } return "schema:" + n }