porting all github actions from bluesky-social/indigo to tangled CI

lexicon: progress on validation

+5 -2
atproto/lexicon/interop_record_test.go
··· 83 83 if err != nil { 84 84 t.Fatal(err) 85 85 } 86 - 87 - assert.Error(cat.ValidateRecord(d, "example.lexicon.record")) 86 + err = cat.ValidateRecord(d, "example.lexicon.record") 87 + if err == nil { 88 + fmt.Println(" FAIL") 89 + } 90 + assert.Error(err) 88 91 } 89 92 }
+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
··· 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
··· 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
··· 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 + }