1package lexicon
2
3import (
4 "fmt"
5 "reflect"
6)
7
8// Boolean flags tweaking how Lexicon validation rules are interpreted.
9type ValidateFlags int
10
11const (
12 // Flag which allows legacy "blob" data to pass validation.
13 AllowLegacyBlob = 1 << iota
14 // Flag which loosens "datetime" string syntax validation. String must still be an ISO datetime, but might be missing timezone (for example)
15 AllowLenientDatetime
16 // Flag which requires validation of nested data in open unions. By default nested union types are only validated optimistically (if the type is known in catatalog) for unlisted types. This flag will result in a validation error if the Lexicon can't be resolved from the catalog.
17 StrictRecursiveValidation
18)
19
20// Combination of argument flags for less formal validation. Recommended for, eg, working with old/legacy data from 2023.
21var LenientMode ValidateFlags = AllowLegacyBlob | AllowLenientDatetime
22
23// Represents a Lexicon schema definition
24type Schema struct {
25 ID string
26 Def any
27}
28
29// Checks Lexicon schema (fetched from the catalog) for the given record, with optional flags tweaking default validation rules.
30//
31// 'recordData' is typed as 'any', but is expected to be 'map[string]any'
32// 'ref' is a reference to the schema type, as an NSID with optional fragment. For records, the '$type' must match 'ref'
33// 'flags' are parameters tweaking Lexicon validation rules. Zero value is default.
34func ValidateRecord(cat Catalog, recordData any, ref string, flags ValidateFlags) error {
35 return validateRecordConfig(cat, recordData, ref, flags)
36}
37
38func validateRecordConfig(cat Catalog, recordData any, ref string, flags ValidateFlags) error {
39 def, err := cat.Resolve(ref)
40 if err != nil {
41 return err
42 }
43 s, ok := def.Def.(SchemaRecord)
44 if !ok {
45 return fmt.Errorf("schema is not of record type: %s", ref)
46 }
47 d, ok := recordData.(map[string]any)
48 if !ok {
49 return fmt.Errorf("record data is not object type")
50 }
51 t, ok := d["$type"]
52 if !ok || t != ref {
53 return fmt.Errorf("record data missing $type, or didn't match expected NSID")
54 }
55 return validateObject(cat, s.Record, d, flags)
56}
57
58func validateData(cat Catalog, def any, d any, flags ValidateFlags) error {
59 switch v := def.(type) {
60 case SchemaNull:
61 return v.Validate(d)
62 case SchemaBoolean:
63 return v.Validate(d)
64 case SchemaInteger:
65 return v.Validate(d)
66 case SchemaString:
67 return v.Validate(d, flags)
68 case SchemaBytes:
69 return v.Validate(d)
70 case SchemaCIDLink:
71 return v.Validate(d)
72 case SchemaArray:
73 arr, ok := d.([]any)
74 if !ok {
75 return fmt.Errorf("expected an array, got: %s", reflect.TypeOf(d))
76 }
77 return validateArray(cat, v, arr, flags)
78 case SchemaObject:
79 obj, ok := d.(map[string]any)
80 if !ok {
81 return fmt.Errorf("expected an object, got: %s", reflect.TypeOf(d))
82 }
83 return validateObject(cat, v, obj, flags)
84 case SchemaBlob:
85 return v.Validate(d, flags)
86 case SchemaRef:
87 // recurse
88 next, err := cat.Resolve(v.fullRef)
89 if err != nil {
90 return err
91 }
92 return validateData(cat, next.Def, d, flags)
93 case SchemaUnion:
94 return validateUnion(cat, v, d, flags)
95 case SchemaUnknown:
96 return v.Validate(d)
97 case SchemaToken:
98 return v.Validate(d)
99 default:
100 return fmt.Errorf("unhandled schema type: %s", reflect.TypeOf(v))
101 }
102}
103
104func validateObject(cat Catalog, s SchemaObject, d map[string]any, flags ValidateFlags) error {
105 for _, k := range s.Required {
106 if _, ok := d[k]; !ok {
107 return fmt.Errorf("required field missing: %s", k)
108 }
109 }
110 for k, def := range s.Properties {
111 if v, ok := d[k]; ok {
112 if v == nil && s.IsNullable(k) {
113 continue
114 }
115 err := validateData(cat, def.Inner, v, flags)
116 if err != nil {
117 return err
118 }
119 }
120 }
121 return nil
122}
123
124func validateArray(cat Catalog, s SchemaArray, arr []any, flags ValidateFlags) error {
125 if (s.MinLength != nil && len(arr) < *s.MinLength) || (s.MaxLength != nil && len(arr) > *s.MaxLength) {
126 return fmt.Errorf("array length out of bounds: %d", len(arr))
127 }
128 for _, v := range arr {
129 err := validateData(cat, s.Items.Inner, v, flags)
130 if err != nil {
131 return err
132 }
133 }
134 return nil
135}
136
137func validateUnion(cat Catalog, s SchemaUnion, d any, flags ValidateFlags) error {
138 closed := s.Closed != nil && *s.Closed == true
139
140 obj, ok := d.(map[string]any)
141 if !ok {
142 return fmt.Errorf("union data is not object type")
143 }
144 typeVal, ok := obj["$type"]
145 if !ok {
146 return fmt.Errorf("union data must have $type")
147 }
148 t, ok := typeVal.(string)
149 if !ok {
150 return fmt.Errorf("union data must have string $type")
151 }
152
153 for _, ref := range s.fullRefs {
154 if ref != t {
155 continue
156 }
157 def, err := cat.Resolve(ref)
158 if err != nil {
159 return fmt.Errorf("could not resolve known union variant $type: %s", ref)
160 }
161 return validateData(cat, def.Def, d, flags)
162 }
163 if closed {
164 return fmt.Errorf("data did not match any variant of closed union: %s", t)
165 }
166
167 // eagerly attempt validation of the open union type
168 // TODO: validate reference as NSID with optional fragment
169 def, err := cat.Resolve(t)
170 if err != nil {
171 if flags&StrictRecursiveValidation != 0 {
172 return fmt.Errorf("could not strictly validate open union variant $type: %s", t)
173 }
174 // by default, ignore validation of unknown open union data
175 return nil
176 }
177 return validateData(cat, def.Def, d, flags)
178}