-
Notifications
You must be signed in to change notification settings - Fork 0
/
validator.go
152 lines (125 loc) · 4.03 KB
/
validator.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
package validator
import (
"embed"
"encoding/json"
"errors"
"fmt"
"github.com/santhosh-tekuri/jsonschema/v5"
)
// DataType represents the type of data being validated e.g. resource or message
type DataType string
const (
TypeResource DataType = "resource" // TypeResource represents tbdex resource
TypeMessage DataType = "message" // TypeMessage represents tbdex message
definitionsSchema = "definitions.json"
schemaDir = "json-schemas/"
schemaHost = "https://tbdex.dev/"
schemaExtension = ".schema.json"
)
//go:embed json-schemas
var embeddedSchemas embed.FS
var schemaCompiler *jsonschema.Compiler
var schemaMap map[string]*jsonschema.Schema = make(map[string]*jsonschema.Schema)
// init does the following:
// - initializes the schema compiler
// - loads the shared definitions schema
// - loads the resource and message schemas
func init() {
schemaCompiler = jsonschema.NewCompiler()
schemaCompiler.Draft = jsonschema.Draft7
definitions, err := embeddedSchemas.Open(schemaDir + definitionsSchema)
if err != nil {
panic(err)
}
err = schemaCompiler.AddResource(schemaHost+definitionsSchema, definitions)
if err != nil {
panic(err)
}
for _, schemaName := range []DataType{TypeResource, TypeMessage} {
_, err = loadSchema(string(schemaName))
if err != nil {
panic(err)
}
}
}
type validateOptions struct {
kind string
}
// ValidateOption is a function that sets an option for the Validate function
type ValidateOption func(*validateOptions)
// WithKind sets the kind option for the Validate function
func WithKind(kind string) ValidateOption {
return func(o *validateOptions) {
o.kind = kind
}
}
// Validate validates the input provided in two phases:
// 1. Validate the general structure of the resource or message based on the Type.
// 2. Validate the specific structure of the resource or message based on the Kind.
//
// A Kind can be optionally specified in order to fail early if the input's Kind does match
// what was provided. This is useful when the Kind is known ahead of time. If the Kind is not
// specified, validation will proceed to phase 2 using metadata.kind.
//
// # Note
//
// Kind-specific schemas are lazily loaded the first time they are needed and then
// cached for future use.
func Validate(dataType DataType, input []byte, opts ...ValidateOption) error {
var options validateOptions
for _, o := range opts {
o(&options)
}
var v any
err := json.Unmarshal(input, &v)
if err != nil {
return fmt.Errorf("failed to JSON unmarshal input: %w", err)
}
typeSchema := schemaMap[string(dataType)]
err = typeSchema.Validate(v)
if err != nil {
return fmt.Errorf("failed to validate input: %w", err)
}
entity, ok := v.(map[string]any)
if !ok {
return fmt.Errorf("expected input to be an object: %w", err)
}
metadata, _ := entity["metadata"].(map[string]any)
kind, _ := metadata["kind"].(string)
if options.kind != "" && kind != options.kind {
return errors.New("kind mismatch")
}
kindSchema, err := loadSchema(kind)
if err != nil {
return fmt.Errorf("failed to validate input: %w", err)
}
err = kindSchema.Validate(entity["data"])
if err != nil {
return fmt.Errorf("failed to validate input: %w", err)
}
return nil
}
// loadSchema loads the schema with the given name. If the schema has already been loaded,
// it is returned from the cache.
func loadSchema(schemaName string) (*jsonschema.Schema, error) {
schema, ok := schemaMap[schemaName]
if ok {
return schema, nil
}
schemaPath := schemaDir + schemaName + schemaExtension
schemaFile, err := embeddedSchemas.Open(schemaPath)
if err != nil {
return nil, fmt.Errorf("failed to load schema file: %w", err)
}
schemaURL := schemaHost + schemaPath
err = schemaCompiler.AddResource(schemaURL, schemaFile)
if err != nil {
return nil, fmt.Errorf("failed to add schema as resource: %w", err)
}
schema, err = schemaCompiler.Compile(schemaURL)
if err != nil {
return nil, fmt.Errorf("failed to compile schema: %w", err)
}
schemaMap[schemaName] = schema
return schema, nil
}