1package lexicon
2
3import (
4 "encoding/json"
5 "fmt"
6 "reflect"
7 "strings"
8
9 "github.com/bluesky-social/indigo/atproto/data"
10 "github.com/bluesky-social/indigo/atproto/syntax"
11
12 "github.com/rivo/uniseg"
13)
14
15// Serialization helper type for top-level Lexicon schema JSON objects (files)
16type SchemaFile struct {
17 Lexicon int `json:"lexicon"` // must be 1
18 ID string `json:"id"`
19 Description *string `json:"description,omitempty"`
20 Defs map[string]SchemaDef `json:"defs"`
21}
22
23// enum type to represent any of the schema fields
24type SchemaDef struct {
25 Inner any
26}
27
28// Checks that the schema definition itself is valid (recursively).
29func (s *SchemaDef) CheckSchema() error {
30 switch v := s.Inner.(type) {
31 case SchemaRecord:
32 return v.CheckSchema()
33 case SchemaQuery:
34 return v.CheckSchema()
35 case SchemaProcedure:
36 return v.CheckSchema()
37 case SchemaSubscription:
38 return v.CheckSchema()
39 case SchemaNull:
40 return v.CheckSchema()
41 case SchemaBoolean:
42 return v.CheckSchema()
43 case SchemaInteger:
44 return v.CheckSchema()
45 case SchemaString:
46 return v.CheckSchema()
47 case SchemaBytes:
48 return v.CheckSchema()
49 case SchemaCIDLink:
50 return v.CheckSchema()
51 case SchemaArray:
52 return v.CheckSchema()
53 case SchemaObject:
54 return v.CheckSchema()
55 case SchemaBlob:
56 return v.CheckSchema()
57 case SchemaParams:
58 return v.CheckSchema()
59 case SchemaToken:
60 return v.CheckSchema()
61 case SchemaRef:
62 return v.CheckSchema()
63 case SchemaUnion:
64 return v.CheckSchema()
65 case SchemaUnknown:
66 return v.CheckSchema()
67 default:
68 return fmt.Errorf("unhandled schema type: %v", reflect.TypeOf(v))
69 }
70}
71
72// Helper to recurse down the definition tree and set full references on any sub-schemas which need to embed that metadata
73func (s *SchemaDef) SetBase(base string) {
74 switch v := s.Inner.(type) {
75 case SchemaRecord:
76 for i, val := range v.Record.Properties {
77 val.SetBase(base)
78 v.Record.Properties[i] = val
79 }
80 s.Inner = v
81 case SchemaQuery:
82 for i, val := range v.Parameters.Properties {
83 val.SetBase(base)
84 v.Parameters.Properties[i] = val
85 }
86 if v.Output != nil && v.Output.Schema != nil {
87 v.Output.Schema.SetBase(base)
88 }
89 s.Inner = v
90 case SchemaProcedure:
91 for i, val := range v.Parameters.Properties {
92 val.SetBase(base)
93 v.Parameters.Properties[i] = val
94 }
95 if v.Input != nil && v.Input.Schema != nil {
96 v.Input.Schema.SetBase(base)
97 }
98 if v.Output != nil && v.Output.Schema != nil {
99 v.Output.Schema.SetBase(base)
100 }
101 s.Inner = v
102 case SchemaSubscription:
103 for i, val := range v.Parameters.Properties {
104 val.SetBase(base)
105 v.Parameters.Properties[i] = val
106 }
107 if v.Message != nil {
108 v.Message.Schema.SetBase(base)
109 }
110 s.Inner = v
111 case SchemaArray:
112 v.Items.SetBase(base)
113 s.Inner = v
114 case SchemaObject:
115 for i, val := range v.Properties {
116 val.SetBase(base)
117 v.Properties[i] = val
118 }
119 s.Inner = v
120 case SchemaParams:
121 for i, val := range v.Properties {
122 val.SetBase(base)
123 v.Properties[i] = val
124 }
125 s.Inner = v
126 case SchemaRef:
127 // add fully-qualified name
128 if strings.HasPrefix(v.Ref, "#") {
129 v.fullRef = base + v.Ref
130 } else {
131 v.fullRef = v.Ref
132 }
133 s.Inner = v
134 case SchemaUnion:
135 // add fully-qualified name
136 for _, ref := range v.Refs {
137 if strings.HasPrefix(ref, "#") {
138 ref = base + ref
139 }
140 v.fullRefs = append(v.fullRefs, ref)
141 }
142 s.Inner = v
143 }
144 return
145}
146
147func (s SchemaDef) MarshalJSON() ([]byte, error) {
148 return json.Marshal(s.Inner)
149}
150
151func (s *SchemaDef) UnmarshalJSON(b []byte) error {
152 t, err := ExtractTypeJSON(b)
153 if err != nil {
154 return err
155 }
156 // TODO: should we call CheckSchema here, instead of in lexicon loading?
157 switch t {
158 case "record":
159 v := new(SchemaRecord)
160 if err = json.Unmarshal(b, v); err != nil {
161 return err
162 }
163 s.Inner = *v
164 return nil
165 case "query":
166 v := new(SchemaQuery)
167 if err = json.Unmarshal(b, v); err != nil {
168 return err
169 }
170 s.Inner = *v
171 return nil
172 case "procedure":
173 v := new(SchemaProcedure)
174 if err = json.Unmarshal(b, v); err != nil {
175 return err
176 }
177 s.Inner = *v
178 return nil
179 case "subscription":
180 v := new(SchemaSubscription)
181 if err = json.Unmarshal(b, v); err != nil {
182 return err
183 }
184 s.Inner = *v
185 return nil
186 case "null":
187 v := new(SchemaNull)
188 if err = json.Unmarshal(b, v); err != nil {
189 return err
190 }
191 s.Inner = *v
192 return nil
193 case "boolean":
194 v := new(SchemaBoolean)
195 if err = json.Unmarshal(b, v); err != nil {
196 return err
197 }
198 s.Inner = *v
199 return nil
200 case "integer":
201 v := new(SchemaInteger)
202 if err = json.Unmarshal(b, v); err != nil {
203 return err
204 }
205 s.Inner = *v
206 return nil
207 case "string":
208 v := new(SchemaString)
209 if err = json.Unmarshal(b, v); err != nil {
210 return err
211 }
212 s.Inner = *v
213 return nil
214 case "bytes":
215 v := new(SchemaBytes)
216 if err = json.Unmarshal(b, v); err != nil {
217 return err
218 }
219 s.Inner = *v
220 return nil
221 case "cid-link":
222 v := new(SchemaCIDLink)
223 if err = json.Unmarshal(b, v); err != nil {
224 return err
225 }
226 s.Inner = *v
227 return nil
228 case "array":
229 v := new(SchemaArray)
230 if err = json.Unmarshal(b, v); err != nil {
231 return err
232 }
233 s.Inner = *v
234 return nil
235 case "object":
236 v := new(SchemaObject)
237 if err = json.Unmarshal(b, v); err != nil {
238 return err
239 }
240 s.Inner = *v
241 return nil
242 case "blob":
243 v := new(SchemaBlob)
244 if err = json.Unmarshal(b, v); err != nil {
245 return err
246 }
247 s.Inner = *v
248 return nil
249 case "params":
250 v := new(SchemaParams)
251 if err = json.Unmarshal(b, v); err != nil {
252 return err
253 }
254 s.Inner = *v
255 return nil
256 case "token":
257 v := new(SchemaToken)
258 if err = json.Unmarshal(b, v); err != nil {
259 return err
260 }
261 s.Inner = *v
262 return nil
263 case "ref":
264 v := new(SchemaRef)
265 if err = json.Unmarshal(b, v); err != nil {
266 return err
267 }
268 s.Inner = *v
269 return nil
270 case "union":
271 v := new(SchemaUnion)
272 if err = json.Unmarshal(b, v); err != nil {
273 return err
274 }
275 s.Inner = *v
276 return nil
277 case "unknown":
278 v := new(SchemaUnknown)
279 if err = json.Unmarshal(b, v); err != nil {
280 return err
281 }
282 s.Inner = *v
283 return nil
284 default:
285 return fmt.Errorf("unexpected schema type: %s", t)
286 }
287}
288
289type SchemaRecord struct {
290 Type string `json:"type"` // "record"
291 Description *string `json:"description,omitempty"`
292 Key string `json:"key"`
293 Record SchemaObject `json:"record"`
294}
295
296func (s *SchemaRecord) CheckSchema() error {
297 switch s.Key {
298 case "tid", "nsid", "any":
299 // pass
300 default:
301 if !strings.HasPrefix(s.Key, "literal:") {
302 return fmt.Errorf("invalid record key specifier: %s", s.Key)
303 }
304 }
305 return s.Record.CheckSchema()
306}
307
308type SchemaQuery struct {
309 Type string `json:"type"` // "query"
310 Description *string `json:"description,omitempty"`
311 Parameters SchemaParams `json:"parameters"`
312 Output *SchemaBody `json:"output"`
313 Errors []SchemaError `json:"errors,omitempty"` // optional
314}
315
316func (s *SchemaQuery) CheckSchema() error {
317 if s.Output != nil {
318 if err := s.Output.CheckSchema(); err != nil {
319 return err
320 }
321 }
322 for _, e := range s.Errors {
323 if err := e.CheckSchema(); err != nil {
324 return err
325 }
326 }
327 return s.Parameters.CheckSchema()
328}
329
330type SchemaProcedure struct {
331 Type string `json:"type"` // "procedure"
332 Description *string `json:"description,omitempty"`
333 Parameters SchemaParams `json:"parameters"`
334 Output *SchemaBody `json:"output"` // optional
335 Errors []SchemaError `json:"errors,omitempty"` // optional
336 Input *SchemaBody `json:"input"` // optional
337}
338
339func (s *SchemaProcedure) CheckSchema() error {
340 if s.Input != nil {
341 if err := s.Input.CheckSchema(); err != nil {
342 return err
343 }
344 }
345 if s.Output != nil {
346 if err := s.Output.CheckSchema(); err != nil {
347 return err
348 }
349 }
350 for _, e := range s.Errors {
351 if err := e.CheckSchema(); err != nil {
352 return err
353 }
354 }
355 return s.Parameters.CheckSchema()
356}
357
358type SchemaSubscription struct {
359 Type string `json:"type"` // "subscription"
360 Description *string `json:"description,omitempty"`
361 Parameters SchemaParams `json:"parameters"`
362 Message *SchemaMessage `json:"message,omitempty"` // TODO(specs): is this really optional?
363}
364
365func (s *SchemaSubscription) CheckSchema() error {
366 if s.Message != nil {
367 if err := s.Message.CheckSchema(); err != nil {
368 return err
369 }
370 }
371 return s.Parameters.CheckSchema()
372}
373
374type SchemaBody struct {
375 Description *string `json:"description,omitempty"`
376 Encoding string `json:"encoding"` // required, mimetype
377 Schema *SchemaDef `json:"schema"` // optional; type:object, type:ref, or type:union
378}
379
380func (s *SchemaBody) CheckSchema() error {
381 // TODO: any validation of encoding?
382 if s.Schema != nil {
383 switch s.Schema.Inner.(type) {
384 case SchemaObject, SchemaRef, SchemaUnion:
385 // pass
386 default:
387 return fmt.Errorf("body type can only have object, ref, or union schema")
388 }
389 if err := s.Schema.CheckSchema(); err != nil {
390 return err
391 }
392 }
393 return nil
394}
395
396type SchemaMessage struct {
397 Description *string `json:"description,omitempty"`
398 Schema SchemaDef `json:"schema"` // required; type:union only
399}
400
401func (s *SchemaMessage) CheckSchema() error {
402 if _, ok := s.Schema.Inner.(SchemaUnion); !ok {
403 return fmt.Errorf("message must have schema type union")
404 }
405 return s.Schema.CheckSchema()
406}
407
408type SchemaError struct {
409 Name string `json:"name"`
410 Description *string `json:"description"`
411}
412
413func (s *SchemaError) CheckSchema() error {
414 return nil
415}
416func (s *SchemaError) Validate(d any) error {
417 e, ok := d.(map[string]any)
418 if !ok {
419 return fmt.Errorf("expected an object in error position")
420 }
421 n, ok := e["error"]
422 if !ok {
423 return fmt.Errorf("expected error type")
424 }
425 if n != s.Name {
426 return fmt.Errorf("error type mis-match: %s", n)
427 }
428 return nil
429}
430
431type SchemaNull struct {
432 Type string `json:"type"` // "null"
433 Description *string `json:"description,omitempty"`
434}
435
436func (s *SchemaNull) CheckSchema() error {
437 return nil
438}
439
440func (s *SchemaNull) Validate(d any) error {
441 if d != nil {
442 return fmt.Errorf("expected null data, got: %s", reflect.TypeOf(d))
443 }
444 return nil
445}
446
447type SchemaBoolean struct {
448 Type string `json:"type"` // "bool"
449 Description *string `json:"description,omitempty"`
450 Default *bool `json:"default,omitempty"`
451 Const *bool `json:"const,omitempty"`
452}
453
454func (s *SchemaBoolean) CheckSchema() error {
455 if s.Default != nil && s.Const != nil {
456 return fmt.Errorf("schema can't have both 'default' and 'const'")
457 }
458 return nil
459}
460
461func (s *SchemaBoolean) Validate(d any) error {
462 v, ok := d.(bool)
463 if !ok {
464 return fmt.Errorf("expected a boolean")
465 }
466 if s.Const != nil && v != *s.Const {
467 return fmt.Errorf("boolean val didn't match constant (%v): %v", *s.Const, v)
468 }
469 return nil
470}
471
472type SchemaInteger struct {
473 Type string `json:"type"` // "integer"
474 Description *string `json:"description,omitempty"`
475 Minimum *int `json:"minimum,omitempty"`
476 Maximum *int `json:"maximum,omitempty"`
477 Enum []int `json:"enum,omitempty"`
478 Default *int `json:"default,omitempty"`
479 Const *int `json:"const,omitempty"`
480}
481
482func (s *SchemaInteger) CheckSchema() error {
483 // TODO: enforce min/max against enum, default, const
484 if s.Default != nil && s.Const != nil {
485 return fmt.Errorf("schema can't have both 'default' and 'const'")
486 }
487 if s.Minimum != nil && s.Maximum != nil && *s.Maximum < *s.Minimum {
488 return fmt.Errorf("schema max < min")
489 }
490 return nil
491}
492
493func (s *SchemaInteger) Validate(d any) error {
494 v64, ok := d.(int64)
495 if !ok {
496 return fmt.Errorf("expected an integer")
497 }
498 v := int(v64)
499 if s.Const != nil && v != *s.Const {
500 return fmt.Errorf("integer val didn't match constant (%d): %d", *s.Const, v)
501 }
502 if (s.Minimum != nil && v < *s.Minimum) || (s.Maximum != nil && v > *s.Maximum) {
503 return fmt.Errorf("integer val outside specified range: %d", v)
504 }
505 if len(s.Enum) != 0 {
506 inEnum := false
507 for _, e := range s.Enum {
508 if e == v {
509 inEnum = true
510 break
511 }
512 }
513 if !inEnum {
514 return fmt.Errorf("integer val not in required enum: %d", v)
515 }
516 }
517 return nil
518}
519
520type SchemaString struct {
521 Type string `json:"type"` // "string"
522 Description *string `json:"description,omitempty"`
523 Format *string `json:"format,omitempty"`
524 MinLength *int `json:"minLength,omitempty"`
525 MaxLength *int `json:"maxLength,omitempty"`
526 MinGraphemes *int `json:"minGraphemes,omitempty"`
527 MaxGraphemes *int `json:"maxGraphemes,omitempty"`
528 KnownValues []string `json:"knownValues,omitempty"`
529 Enum []string `json:"enum,omitempty"`
530 Default *string `json:"default,omitempty"`
531 Const *string `json:"const,omitempty"`
532}
533
534func (s *SchemaString) CheckSchema() error {
535 // TODO: enforce min/max against enum, default, const
536 if s.Default != nil && s.Const != nil {
537 return fmt.Errorf("schema can't have both 'default' and 'const'")
538 }
539 if s.MinLength != nil && s.MaxLength != nil && *s.MaxLength < *s.MinLength {
540 return fmt.Errorf("schema max < min")
541 }
542 if s.MinGraphemes != nil && s.MaxGraphemes != nil && *s.MaxGraphemes < *s.MinGraphemes {
543 return fmt.Errorf("schema max < min")
544 }
545 if (s.MinLength != nil && *s.MinLength < 0) ||
546 (s.MaxLength != nil && *s.MaxLength < 0) ||
547 (s.MinGraphemes != nil && *s.MinGraphemes < 0) ||
548 (s.MaxGraphemes != nil && *s.MaxGraphemes < 0) {
549 return fmt.Errorf("string schema min or max below zero")
550 }
551 if s.Format != nil {
552 switch *s.Format {
553 case "at-identifier", "at-uri", "cid", "datetime", "did", "handle", "nsid", "uri", "language", "tid", "record-key":
554 // pass
555 default:
556 return fmt.Errorf("unknown string format: %s", *s.Format)
557 }
558 }
559 return nil
560}
561
562func (s *SchemaString) Validate(d any, flags ValidateFlags) error {
563 v, ok := d.(string)
564 if !ok {
565 return fmt.Errorf("expected a string: %v", reflect.TypeOf(d))
566 }
567 if s.Const != nil && v != *s.Const {
568 return fmt.Errorf("string val didn't match constant (%s): %s", *s.Const, v)
569 }
570 // TODO: is this actually counting UTF-8 length?
571 if (s.MinLength != nil && len(v) < *s.MinLength) || (s.MaxLength != nil && len(v) > *s.MaxLength) {
572 return fmt.Errorf("string length outside specified range: %d", len(v))
573 }
574 if len(s.Enum) != 0 {
575 inEnum := false
576 for _, e := range s.Enum {
577 if e == v {
578 inEnum = true
579 break
580 }
581 }
582 if !inEnum {
583 return fmt.Errorf("string val not in required enum: %s", v)
584 }
585 }
586 if s.MinGraphemes != nil || s.MaxGraphemes != nil {
587 lenG := uniseg.GraphemeClusterCount(v)
588 if (s.MinGraphemes != nil && lenG < *s.MinGraphemes) || (s.MaxGraphemes != nil && lenG > *s.MaxGraphemes) {
589 return fmt.Errorf("string length (graphemes) outside specified range: %d", lenG)
590 }
591 }
592 if s.Format != nil {
593 switch *s.Format {
594 case "at-identifier":
595 if _, err := syntax.ParseAtIdentifier(v); err != nil {
596 return err
597 }
598 case "at-uri":
599 if _, err := syntax.ParseATURI(v); err != nil {
600 return err
601 }
602 case "cid":
603 if _, err := syntax.ParseCID(v); err != nil {
604 return err
605 }
606 case "datetime":
607 if flags&AllowLenientDatetime != 0 {
608 if _, err := syntax.ParseDatetimeLenient(v); err != nil {
609 return err
610 }
611 } else {
612 if _, err := syntax.ParseDatetime(v); err != nil {
613 return err
614 }
615 }
616 case "did":
617 if _, err := syntax.ParseDID(v); err != nil {
618 return err
619 }
620 case "handle":
621 if _, err := syntax.ParseHandle(v); err != nil {
622 return err
623 }
624 case "nsid":
625 if _, err := syntax.ParseNSID(v); err != nil {
626 return err
627 }
628 case "uri":
629 if _, err := syntax.ParseURI(v); err != nil {
630 return err
631 }
632 case "language":
633 if _, err := syntax.ParseLanguage(v); err != nil {
634 return err
635 }
636 case "tid":
637 if _, err := syntax.ParseTID(v); err != nil {
638 return err
639 }
640 case "record-key":
641 if _, err := syntax.ParseRecordKey(v); err != nil {
642 return err
643 }
644 }
645 }
646 return nil
647}
648
649type SchemaBytes struct {
650 Type string `json:"type"` // "bytes"
651 Description *string `json:"description,omitempty"`
652 MinLength *int `json:"minLength,omitempty"`
653 MaxLength *int `json:"maxLength,omitempty"`
654}
655
656func (s *SchemaBytes) CheckSchema() error {
657 if s.MinLength != nil && s.MaxLength != nil && *s.MaxLength < *s.MinLength {
658 return fmt.Errorf("schema max < min")
659 }
660 if (s.MinLength != nil && *s.MinLength < 0) ||
661 (s.MaxLength != nil && *s.MaxLength < 0) {
662 return fmt.Errorf("bytes schema min or max below zero")
663 }
664 return nil
665}
666
667func (s *SchemaBytes) Validate(d any) error {
668 v, ok := d.(data.Bytes)
669 if !ok {
670 return fmt.Errorf("expecting bytes")
671 }
672 if (s.MinLength != nil && len(v) < *s.MinLength) || (s.MaxLength != nil && len(v) > *s.MaxLength) {
673 return fmt.Errorf("bytes size out of bounds: %d", len(v))
674 }
675 return nil
676}
677
678type SchemaCIDLink struct {
679 Type string `json:"type"` // "cid-link"
680 Description *string `json:"description,omitempty"`
681}
682
683func (s *SchemaCIDLink) CheckSchema() error {
684 return nil
685}
686
687func (s *SchemaCIDLink) Validate(d any) error {
688 _, ok := d.(data.CIDLink)
689 if !ok {
690 return fmt.Errorf("expecting a cid-link")
691 }
692 return nil
693}
694
695type SchemaArray struct {
696 Type string `json:"type"` // "array"
697 Description *string `json:"description,omitempty"`
698 Items SchemaDef `json:"items"`
699 MinLength *int `json:"minLength,omitempty"`
700 MaxLength *int `json:"maxLength,omitempty"`
701}
702
703func (s *SchemaArray) CheckSchema() error {
704 if s.MinLength != nil && s.MaxLength != nil && *s.MaxLength < *s.MinLength {
705 return fmt.Errorf("schema max < min")
706 }
707 if (s.MinLength != nil && *s.MinLength < 0) ||
708 (s.MaxLength != nil && *s.MaxLength < 0) {
709 return fmt.Errorf("array schema min or max below zero")
710 }
711 return s.Items.CheckSchema()
712}
713
714type SchemaObject struct {
715 Type string `json:"type"` // "object"
716 Description *string `json:"description,omitempty"`
717 Properties map[string]SchemaDef `json:"properties"`
718 Required []string `json:"required,omitempty"`
719 Nullable []string `json:"nullable,omitempty"`
720}
721
722func (s *SchemaObject) CheckSchema() error {
723 // TODO: check for set intersection between required and nullable
724 // TODO: check for set uniqueness of required and nullable
725 for _, k := range s.Required {
726 if _, ok := s.Properties[k]; !ok {
727 return fmt.Errorf("object 'required' field not in properties: %s", k)
728 }
729 }
730 for _, k := range s.Nullable {
731 if _, ok := s.Properties[k]; !ok {
732 return fmt.Errorf("object 'nullable' field not in properties: %s", k)
733 }
734 }
735 for k, def := range s.Properties {
736 // TODO: more checks on field name?
737 if len(k) == 0 {
738 return fmt.Errorf("empty object schema field name not allowed")
739 }
740 if err := def.CheckSchema(); err != nil {
741 return err
742 }
743 }
744 return nil
745}
746
747// Checks if a field name 'k' is one of the Nullable fields for this object
748func (s *SchemaObject) IsNullable(k string) bool {
749 for _, el := range s.Nullable {
750 if el == k {
751 return true
752 }
753 }
754 return false
755}
756
757type SchemaBlob struct {
758 Type string `json:"type"` // "blob"
759 Description *string `json:"description,omitempty"`
760 Accept []string `json:"accept,omitempty"`
761 MaxSize *int `json:"maxSize,omitempty"`
762}
763
764func (s *SchemaBlob) CheckSchema() error {
765 // TODO: validate Accept (mimetypes)?
766 if s.MaxSize != nil && *s.MaxSize <= 0 {
767 return fmt.Errorf("blob max size less or equal to zero")
768 }
769 return nil
770}
771
772func (s *SchemaBlob) Validate(d any, flags ValidateFlags) error {
773 v, ok := d.(data.Blob)
774 if !ok {
775 return fmt.Errorf("expected a blob")
776 }
777 if !(flags&AllowLegacyBlob != 0) && v.Size < 0 {
778 return fmt.Errorf("legacy blobs not allowed")
779 }
780 if len(s.Accept) > 0 {
781 typeOk := false
782 for _, pat := range s.Accept {
783 if acceptableMimeType(pat, v.MimeType) {
784 typeOk = true
785 break
786 }
787 }
788 if !typeOk {
789 return fmt.Errorf("blob mimetype doesn't match accepted: %s", v.MimeType)
790 }
791 }
792 if s.MaxSize != nil && int(v.Size) > *s.MaxSize {
793 return fmt.Errorf("blob size too large: %d", v.Size)
794 }
795 return nil
796}
797
798type SchemaParams struct {
799 Type string `json:"type"` // "params"
800 Description *string `json:"description,omitempty"`
801 Properties map[string]SchemaDef `json:"properties"` // boolean, integer, string, or unknown; or an array of these types
802 Required []string `json:"required,omitempty"`
803}
804
805func (s *SchemaParams) CheckSchema() error {
806 // TODO: check for set uniqueness of required
807 for _, k := range s.Required {
808 if _, ok := s.Properties[k]; !ok {
809 return fmt.Errorf("object 'required' field not in properties: %s", k)
810 }
811 }
812 for k, def := range s.Properties {
813 // TODO: more checks on field name?
814 if len(k) == 0 {
815 return fmt.Errorf("empty object schema field name not allowed")
816 }
817 switch v := def.Inner.(type) {
818 case SchemaBoolean, SchemaInteger, SchemaString, SchemaUnknown:
819 // pass
820 case SchemaArray:
821 switch v.Items.Inner.(type) {
822 case SchemaBoolean, SchemaInteger, SchemaString, SchemaUnknown:
823 // pass
824 default:
825 return fmt.Errorf("params array item type must be boolean, integer, string, or unknown")
826 }
827 default:
828 return fmt.Errorf("params field type must be boolean, integer, string, or unknown")
829 }
830 if err := def.CheckSchema(); err != nil {
831 return err
832 }
833 }
834 return nil
835}
836
837type SchemaToken struct {
838 Type string `json:"type"` // "token"
839 Description *string `json:"description,omitempty"`
840 // the fully-qualified identifier of this token
841 fullName string
842}
843
844func (s *SchemaToken) CheckSchema() error {
845 if s.fullName == "" {
846 return fmt.Errorf("expected fully-qualified token name")
847 }
848 return nil
849}
850
851func (s *SchemaToken) Validate(d any) error {
852 str, ok := d.(string)
853 if !ok {
854 return fmt.Errorf("expected a string for token, got: %s", reflect.TypeOf(d))
855 }
856 if s.fullName == "" {
857 return fmt.Errorf("token name was not populated at parse time")
858 }
859 if str != s.fullName {
860 return fmt.Errorf("token name did not match expected: %s", str)
861 }
862 return nil
863}
864
865type SchemaRef struct {
866 Type string `json:"type"` // "ref"
867 Description *string `json:"description,omitempty"`
868 Ref string `json:"ref"`
869 // full path of reference
870 fullRef string
871}
872
873func (s *SchemaRef) CheckSchema() error {
874 // TODO: more validation of ref string?
875 if len(s.Ref) == 0 {
876 return fmt.Errorf("empty schema ref")
877 }
878 if len(s.fullRef) == 0 {
879 return fmt.Errorf("empty full schema ref")
880 }
881 return nil
882}
883
884type SchemaUnion struct {
885 Type string `json:"type"` // "union"
886 Description *string `json:"description,omitempty"`
887 Refs []string `json:"refs"`
888 Closed *bool `json:"closed,omitempty"`
889 // fully qualified
890 fullRefs []string
891}
892
893func (s *SchemaUnion) CheckSchema() error {
894 // TODO: uniqueness check on refs
895 for _, ref := range s.Refs {
896 // TODO: more validation of ref string?
897 if len(ref) == 0 {
898 return fmt.Errorf("empty schema ref")
899 }
900 }
901 if len(s.fullRefs) != len(s.Refs) {
902 return fmt.Errorf("union refs were not expanded")
903 }
904 return nil
905}
906
907type SchemaUnknown struct {
908 Type string `json:"type"` // "unknown"
909 Description *string `json:"description,omitempty"`
910}
911
912func (s *SchemaUnknown) CheckSchema() error {
913 return nil
914}
915
916func (s *SchemaUnknown) Validate(d any) error {
917 _, ok := d.(map[string]any)
918 if !ok {
919 return fmt.Errorf("'unknown' data must an object")
920 }
921 return nil
922}