+5
-2
atproto/lexicon/interop_record_test.go
+5
-2
atproto/lexicon/interop_record_test.go
+61
-4
atproto/lexicon/language.go
+61
-4
atproto/lexicon/language.go
···
8
8
9
9
"github.com/bluesky-social/indigo/atproto/data"
10
10
"github.com/bluesky-social/indigo/atproto/syntax"
11
+
12
+
"github.com/rivo/uniseg"
11
13
)
12
14
13
15
// Serialization helper for top-level Lexicon schema JSON objects (files)
···
419
421
return fmt.Errorf("expected an integer")
420
422
}
421
423
v := int(v64)
422
-
// TODO: enforce enum
423
424
if s.Const != nil && v != *s.Const {
424
425
return fmt.Errorf("integer val didn't match constant (%d): %d", *s.Const, v)
425
426
}
426
427
if (s.Minimum != nil && v < *s.Minimum) || (s.Maximum != nil && v > *s.Maximum) {
427
428
return fmt.Errorf("integer val outside specified range: %d", v)
429
+
}
430
+
if len(s.Enum) != 0 {
431
+
inEnum := false
432
+
for _, e := range s.Enum {
433
+
if e == v {
434
+
inEnum = true
435
+
break
436
+
}
437
+
}
438
+
if !inEnum {
439
+
return fmt.Errorf("integer val not in required enum: %d", v)
440
+
}
428
441
}
429
442
return nil
430
443
}
···
476
489
if !ok {
477
490
return fmt.Errorf("expected a string: %v", reflect.TypeOf(d))
478
491
}
479
-
// TODO: enforce enum
480
492
if s.Const != nil && v != *s.Const {
481
493
return fmt.Errorf("string val didn't match constant (%s): %s", *s.Const, v)
482
494
}
···
484
496
if (s.MinLength != nil && len(v) < *s.MinLength) || (s.MaxLength != nil && len(v) > *s.MaxLength) {
485
497
return fmt.Errorf("string length outside specified range: %d", len(v))
486
498
}
487
-
// TODO: grapheme length
499
+
if len(s.Enum) != 0 {
500
+
inEnum := false
501
+
for _, e := range s.Enum {
502
+
if e == v {
503
+
inEnum = true
504
+
break
505
+
}
506
+
}
507
+
if !inEnum {
508
+
return fmt.Errorf("string val not in required enum: %s", v)
509
+
}
510
+
}
511
+
if s.MinGraphemes != nil || s.MaxGraphemes != nil {
512
+
lenG := uniseg.GraphemeClusterCount(v)
513
+
if (s.MinGraphemes != nil && lenG < *s.MinGraphemes) || (s.MaxGraphemes != nil && lenG > *s.MaxGraphemes) {
514
+
return fmt.Errorf("string length (graphemes) outside specified range: %d", lenG)
515
+
}
516
+
}
488
517
if s.Format != nil {
489
518
switch *s.Format {
490
519
case "at-identifier":
···
656
685
if !ok {
657
686
return fmt.Errorf("expected a blob")
658
687
}
659
-
// TODO: validate accept mimetype
688
+
if len(s.Accept) > 0 {
689
+
typeOk := false
690
+
for _, pat := range s.Accept {
691
+
if acceptableMimeType(pat, v.MimeType) {
692
+
typeOk = true
693
+
break
694
+
}
695
+
}
696
+
if !typeOk {
697
+
return fmt.Errorf("blob mimetype doesn't match accepted: %s", v.MimeType)
698
+
}
699
+
}
660
700
if s.MaxSize != nil && int(v.Size) > *s.MaxSize {
661
701
return fmt.Errorf("blob size too large: %d", v.Size)
662
702
}
···
702
742
return nil
703
743
}
704
744
745
+
// XXX: implementation?
705
746
func (s *SchemaParams) Validate(d any) error {
706
747
return nil
707
748
}
···
709
750
type SchemaToken struct {
710
751
Type string `json:"type,const=token"`
711
752
Description *string `json:"description,omitempty"`
753
+
// the fully-qualified identifier of this token
754
+
Name string
712
755
}
713
756
714
757
func (s *SchemaToken) CheckSchema() error {
758
+
return nil
759
+
}
760
+
761
+
func (s *SchemaToken) Validate(d any) error {
762
+
str, ok := d.(string)
763
+
if !ok {
764
+
return fmt.Errorf("expected a string for token, got: %s", reflect.TypeOf(d))
765
+
}
766
+
if s.Name == "" {
767
+
return fmt.Errorf("token name was not populated at parse time")
768
+
}
769
+
if str != s.Name {
770
+
return fmt.Errorf("token name did not match expected: %s", str)
771
+
}
715
772
return nil
716
773
}
717
774
+25
-9
atproto/lexicon/lexicon.go
+25
-9
atproto/lexicon/lexicon.go
···
61
61
if frag != "main" {
62
62
return fmt.Errorf("record, query, procedure, and subscription types must be 'main', not: %s", frag)
63
63
}
64
+
case SchemaToken:
65
+
token := def.Inner.(SchemaToken)
66
+
token.Name = name
67
+
def.Inner = token
64
68
}
65
69
s := Schema{
66
70
ID: name,
···
169
173
}
170
174
return c.validateData(next.Def, d)
171
175
case SchemaUnion:
172
-
//return fmt.Errorf("XXX: union validation not implemented")
173
-
return nil
176
+
return c.validateUnion(v, d)
174
177
case SchemaUnknown:
175
178
return v.Validate(d)
176
179
case SchemaToken:
177
-
str, ok := d.(string)
178
-
if !ok {
179
-
return fmt.Errorf("expected a string for token, got: %s", reflect.TypeOf(d))
180
-
}
181
-
// XXX: token validation not implemented
182
-
_ = str
183
-
return nil
180
+
return v.Validate(d)
184
181
default:
185
182
return fmt.Errorf("unhandled schema type: %s", reflect.TypeOf(v))
186
183
}
···
218
215
}
219
216
return nil
220
217
}
218
+
219
+
func (c *Catalog) validateUnion(s SchemaUnion, d any) error {
220
+
closed := s.Closed != nil && *s.Closed == true
221
+
for _, ref := range s.Refs {
222
+
def, err := c.Resolve(ref)
223
+
if err != nil {
224
+
// TODO: how to actually handle unknown defs?
225
+
return err
226
+
}
227
+
if err = c.validateData(def.Def, d); nil == err { // if success
228
+
return nil
229
+
}
230
+
}
231
+
if closed {
232
+
return fmt.Errorf("data did not match any variant of closed union")
233
+
}
234
+
// TODO: anything matches if an open union?
235
+
return nil
236
+
}
+19
atproto/lexicon/mimetype.go
+19
atproto/lexicon/mimetype.go
···
1
+
package lexicon
2
+
3
+
import (
4
+
"strings"
5
+
)
6
+
7
+
// checks if val matches pattern, with optional trailing glob on pattern. case-sensitive.
8
+
func acceptableMimeType(pattern, val string) bool {
9
+
if val == "" || pattern == "" {
10
+
return false
11
+
}
12
+
if strings.HasSuffix(pattern, "*") {
13
+
prefix := pattern[:len(pattern)-1]
14
+
return strings.HasPrefix(val, prefix)
15
+
} else {
16
+
return pattern == val
17
+
}
18
+
return false
19
+
}
+21
atproto/lexicon/mimetype_test.go
+21
atproto/lexicon/mimetype_test.go
···
1
+
package lexicon
2
+
3
+
import (
4
+
"testing"
5
+
6
+
"github.com/stretchr/testify/assert"
7
+
)
8
+
9
+
func TestAcceptableMimeType(t *testing.T) {
10
+
assert := assert.New(t)
11
+
12
+
assert.True(acceptableMimeType("image/*", "image/png"))
13
+
assert.True(acceptableMimeType("text/plain", "text/plain"))
14
+
15
+
assert.False(acceptableMimeType("image/*", "text/plain"))
16
+
assert.False(acceptableMimeType("text/plain", "image/png"))
17
+
assert.False(acceptableMimeType("text/plain", ""))
18
+
assert.False(acceptableMimeType("", "text/plain"))
19
+
20
+
// TODO: application/json, application/json+thing
21
+
}