porting all github actions from bluesky-social/indigo to tangled CI
at main 5.3 kB view raw
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}