this repo has no description
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

lexicon tweaks for easier parsing (#1160)

This came up adding a linter, and would also be an issue for codegen and
similar use-cases, where it is desirable to run CheckSchema before
loading in to a catalog

authored by

bnewbold and committed by
GitHub
01067319 09157497

+156 -60
+7 -21
atproto/lexicon/catalog.go
··· 50 50 51 51 // Inserts a schema loaded from a JSON file in to the catalog. 52 52 func (c *BaseCatalog) AddSchemaFile(sf SchemaFile) error { 53 - if sf.Lexicon != 1 { 54 - return fmt.Errorf("unsupported lexicon language version: %d", sf.Lexicon) 53 + 54 + if err := sf.CheckSchema(); err != nil { 55 + return err 55 56 } 57 + 56 58 base := sf.ID 57 59 for frag, def := range sf.Defs { 58 - if len(frag) == 0 || strings.Contains(frag, "#") || strings.Contains(frag, ".") { 59 - // TODO: more validation here? 60 - return fmt.Errorf("schema name invalid: %s", frag) 61 - } 62 60 name := base + "#" + frag 63 61 if _, ok := c.schemas[name]; ok { 64 62 return fmt.Errorf("catalog already contained a schema with name: %s", name) 65 63 } 66 - // "A file can have at most one definition with one of the "primary" types. Primary types should always have the name main. It is possible for main to describe a non-primary type." 67 - switch s := def.Inner.(type) { 68 - case SchemaRecord, SchemaQuery, SchemaProcedure, SchemaSubscription, SchemaPermissionSet: 69 - if frag != "main" { 70 - return fmt.Errorf("record, query, procedure, and subscription types must be 'main', not: %s", frag) 71 - } 72 - case SchemaToken: 73 - // add fully-qualified name to token 74 - s.fullName = name 75 - def.Inner = s 76 - } 77 - def.SetBase(base) 78 - if err := def.CheckSchema(); err != nil { 79 - return err 80 - } 81 64 s := Schema{ 82 65 ID: name, 83 66 Def: def.Inner, ··· 91 74 func (c *BaseCatalog) addSchemaFromBytes(b []byte) error { 92 75 var sf SchemaFile 93 76 if err := json.Unmarshal(b, &sf); err != nil { 77 + return err 78 + } 79 + if err := sf.FinishParse(); err != nil { 94 80 return err 95 81 } 96 82 if err := c.AddSchemaFile(sf); err != nil {
+34 -27
atproto/lexicon/language.go
··· 12 12 "github.com/rivo/uniseg" 13 13 ) 14 14 15 - // Serialization helper type for top-level Lexicon schema JSON objects (files) 16 - type 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 15 // enum type to represent any of the schema fields 24 16 type SchemaDef struct { 25 17 Inner any ··· 74 66 } 75 67 76 68 // Helper to recurse down the definition tree and set full references on any sub-schemas which need to embed that metadata 77 - func (s *SchemaDef) SetBase(base string) { 69 + func (s *SchemaDef) setBase(base string) { 78 70 switch v := s.Inner.(type) { 79 71 case SchemaRecord: 80 72 for i, val := range v.Record.Properties { 81 - val.SetBase(base) 73 + val.setBase(base) 82 74 v.Record.Properties[i] = val 83 75 } 84 76 s.Inner = v 85 77 case SchemaQuery: 86 78 if v.Parameters != nil { 87 79 for i, val := range v.Parameters.Properties { 88 - val.SetBase(base) 80 + val.setBase(base) 89 81 v.Parameters.Properties[i] = val 90 82 } 91 83 } 92 84 if v.Output != nil && v.Output.Schema != nil { 93 - v.Output.Schema.SetBase(base) 85 + v.Output.Schema.setBase(base) 94 86 } 95 87 s.Inner = v 96 88 case SchemaProcedure: 97 89 if v.Parameters != nil { 98 90 for i, val := range v.Parameters.Properties { 99 - val.SetBase(base) 91 + val.setBase(base) 100 92 v.Parameters.Properties[i] = val 101 93 } 102 94 } 103 95 if v.Input != nil && v.Input.Schema != nil { 104 - v.Input.Schema.SetBase(base) 96 + v.Input.Schema.setBase(base) 105 97 } 106 98 if v.Output != nil && v.Output.Schema != nil { 107 - v.Output.Schema.SetBase(base) 99 + v.Output.Schema.setBase(base) 108 100 } 109 101 s.Inner = v 110 102 case SchemaSubscription: 111 103 if v.Parameters != nil { 112 104 for i, val := range v.Parameters.Properties { 113 - val.SetBase(base) 105 + val.setBase(base) 114 106 v.Parameters.Properties[i] = val 115 107 } 116 108 } 117 109 if v.Message != nil { 118 - v.Message.Schema.SetBase(base) 110 + v.Message.Schema.setBase(base) 119 111 } 120 112 s.Inner = v 121 113 case SchemaArray: 122 - v.Items.SetBase(base) 114 + v.Items.setBase(base) 123 115 s.Inner = v 124 116 case SchemaObject: 125 117 for i, val := range v.Properties { 126 - val.SetBase(base) 118 + val.setBase(base) 127 119 v.Properties[i] = val 128 120 } 129 121 s.Inner = v 130 122 case SchemaParams: 131 123 for i, val := range v.Properties { 132 - val.SetBase(base) 124 + val.setBase(base) 133 125 v.Properties[i] = val 134 126 } 135 127 s.Inner = v ··· 412 404 413 405 type SchemaPermissionSet struct { 414 406 Type string `json:"type"` // "permission-set" 415 - Description *string `json:"description,omitempty"` 407 + Title *string `json:"title,omitempty"` 408 + TitleLangs map[string]string `json:"title:langs,omitempty"` 409 + Detail *string `json:"detail,omitempty"` 410 + DetailLangs map[string]string `json:"detail:langs,omitempty"` 416 411 Permissions []SchemaPermission `json:"permissions"` 417 412 } 418 413 419 414 func (s *SchemaPermissionSet) CheckSchema() error { 415 + for lang, _ := range s.TitleLangs { 416 + _, err := syntax.ParseLanguage(lang) 417 + if err != nil { 418 + return err 419 + } 420 + } 421 + for lang, _ := range s.DetailLangs { 422 + _, err := syntax.ParseLanguage(lang) 423 + if err != nil { 424 + return err 425 + } 426 + } 420 427 for _, p := range s.Permissions { 421 428 if err := p.CheckSchema(); err != nil { 422 429 return err ··· 492 499 if (s.InheritAud == true && s.Audience != "") || (s.InheritAud == false && s.Audience == "") { 493 500 return fmt.Errorf("rpc permission must have eith 'aud' or 'inheritAud' defined") 494 501 } 495 - if s.Audience != "" { 502 + if s.Audience != "" && s.Audience != "*" { 496 503 // TODO: helper for service refs 497 504 parts := strings.SplitN(s.Audience, "#", 3) 498 505 if len(parts) != 2 || parts[1] == "" { ··· 978 985 type SchemaToken struct { 979 986 Type string `json:"type"` // "token" 980 987 Description *string `json:"description,omitempty"` 981 - // the fully-qualified identifier of this token 982 - fullName string 988 + // the fully-qualified identifier of this token. this is not included in the schema file; it must be added when parsing 989 + FullName string `json:"-"` 983 990 } 984 991 985 992 func (s *SchemaToken) CheckSchema() error { 986 - if s.fullName == "" { 993 + if s.FullName == "" { 987 994 return fmt.Errorf("expected fully-qualified token name") 988 995 } 989 996 return nil ··· 994 1001 if !ok { 995 1002 return fmt.Errorf("expected a string for token, got: %s", reflect.TypeOf(d)) 996 1003 } 997 - if s.fullName == "" { 1004 + if s.FullName == "" { 998 1005 return fmt.Errorf("token name was not populated at parse time") 999 1006 } 1000 - if str != s.fullName { 1007 + if str != s.FullName { 1001 1008 return fmt.Errorf("token name did not match expected: %s", str) 1002 1009 } 1003 1010 return nil
+68
atproto/lexicon/schemafile.go
··· 1 + package lexicon 2 + 3 + import ( 4 + "fmt" 5 + "strings" 6 + ) 7 + 8 + // Serialization helper type for top-level Lexicon schema JSON objects (files). 9 + // 10 + // Note that the [FinishParse] method should always be called after unmarshalling a SchemaFile from JSON. 11 + type SchemaFile struct { 12 + Lexicon int `json:"lexicon"` // must be 1 13 + ID string `json:"id"` 14 + Description *string `json:"description,omitempty"` 15 + Defs map[string]SchemaDef `json:"defs"` 16 + } 17 + 18 + // Helper method which should always be called after parsing a schema file (eg, from JSON). 19 + // 20 + // Does some very basic validation (eg, lexicon language version), and fills in 21 + // internal references (for example full name of tokens). 22 + func (sf *SchemaFile) FinishParse() error { 23 + if sf.Lexicon != 1 { 24 + return fmt.Errorf("unsupported lexicon language version: %d", sf.Lexicon) 25 + } 26 + base := sf.ID 27 + for frag, def := range sf.Defs { 28 + if len(frag) == 0 || strings.Contains(frag, "#") || strings.Contains(frag, ".") { 29 + // TODO: more validation here? 30 + return fmt.Errorf("schema name invalid: %s", frag) 31 + } 32 + name := base + "#" + frag 33 + switch s := def.Inner.(type) { 34 + case SchemaToken: 35 + // add fully-qualified name to token 36 + s.FullName = name 37 + def.Inner = s 38 + } 39 + def.setBase(base) 40 + sf.Defs[frag] = def 41 + } 42 + return nil 43 + } 44 + 45 + // Calls [SchemaDef.CheckSchema] recursively over all defs 46 + func (sf *SchemaFile) CheckSchema() error { 47 + if sf.Lexicon != 1 { 48 + return fmt.Errorf("unsupported lexicon language version: %d", sf.Lexicon) 49 + } 50 + 51 + for frag, def := range sf.Defs { 52 + if len(frag) == 0 || strings.Contains(frag, "#") || strings.Contains(frag, ".") { 53 + // TODO: more validation here? 54 + return fmt.Errorf("schema name invalid: %s", frag) 55 + } 56 + // "A file can have at most one definition with one of the "primary" types. Primary types should always have the name main. It is possible for main to describe a non-primary type." 57 + switch def.Inner.(type) { 58 + case SchemaRecord, SchemaQuery, SchemaProcedure, SchemaSubscription, SchemaPermissionSet: 59 + if frag != "main" { 60 + return fmt.Errorf("record, query, procedure, and subscription types must be 'main', not: %s", frag) 61 + } 62 + } 63 + if err := def.CheckSchema(); err != nil { 64 + return err 65 + } 66 + } 67 + return nil 68 + }
+1 -2
atproto/lexicon/testdata/catalog/minimal-query.json
··· 1 1 { 2 2 "lexicon": 1, 3 3 "id": "example.lexicon.minimal.query", 4 - "revision": 1, 5 - "description": "exercizes many lexicon features for the query type", 4 + "description": "exercises many lexicon features for the query type", 6 5 "defs": { 7 6 "main": { 8 7 "type": "query",
+43 -6
atproto/lexicon/testdata/catalog/permission-set.json
··· 1 1 { 2 2 "lexicon": 1, 3 3 "id": "example.lexicon.permissionset", 4 - "description": "exercizes many lexicon features for the permission-set type", 4 + "description": "exercises many lexicon features for the permission-set type", 5 5 "defs": { 6 6 "main": { 7 7 "type": "permission-set", 8 + "title": "Example for Moderation", 9 + "title:lang": { 10 + "fr": "Example for Modération" 11 + }, 12 + "detail": "Create moderation reports", 13 + "detail:lang": { 14 + "fr-FR": "Créer des rapports de modération" 15 + }, 8 16 "permissions": [ 9 17 { 10 18 "type": "permission", 11 19 "resource": "blob", 12 20 "accept": [ 13 - "image/*" 21 + "image/*", 22 + "video/*" 14 23 ] 15 24 }, 16 25 { ··· 39 48 ] 40 49 }, 41 50 { 42 - "type": "permission", 43 - "resource": "rpc", 44 - "aud": "did:web:example.com#foo", 45 - "lxm": ["com.example.calendar.listEvents"] 51 + "type": "permission", 52 + "resource": "repo", 53 + "collection": [ 54 + "com.example.calendar.eventV2" 55 + ], 56 + "action": [ 57 + "create" 58 + ] 59 + }, 60 + { 61 + "type": "permission", 62 + "resource": "rpc", 63 + "aud": "did:web:example.com#foo", 64 + "lxm": [ 65 + "com.example.calendar.listEvents" 66 + ] 67 + }, 68 + { 69 + "type": "permission", 70 + "resource": "rpc", 71 + "aud": "did:web:example.com#bar", 72 + "lxm": [ 73 + "*" 74 + ] 46 75 }, 47 76 { 48 77 "type": "permission", 49 78 "resource": "rpc", 50 79 "inheritAud": true, 80 + "lxm": [ 81 + "com.example.calendar.listEvents" 82 + ] 83 + }, 84 + { 85 + "type": "permission", 86 + "resource": "rpc", 87 + "aud": "*", 51 88 "lxm": [ 52 89 "com.example.calendar.listEvents" 53 90 ]
+1 -1
atproto/lexicon/testdata/catalog/procedure.json
··· 1 1 { 2 2 "lexicon": 1, 3 3 "id": "example.lexicon.procedure", 4 - "description": "demonstrates lexicon features for the procedure type", 5 4 "defs": { 6 5 "main": { 7 6 "type": "procedure", 7 + "description": "demonstrates lexicon features for the procedure type", 8 8 "parameters": { 9 9 "type": "params", 10 10 "properties": {
+1 -2
atproto/lexicon/testdata/catalog/query.json
··· 1 1 { 2 2 "lexicon": 1, 3 3 "id": "example.lexicon.query", 4 - "revision": 1, 5 - "description": "exercizes many lexicon features for the query type", 4 + "description": "exercises many lexicon features for the query type", 6 5 "defs": { 7 6 "main": { 8 7 "type": "query",
-1
atproto/lexicon/testdata/catalog/record.json
··· 1 1 { 2 2 "lexicon": 1, 3 3 "id": "example.lexicon.record", 4 - "revision": 1, 5 4 "description": "demonstrates lexicon features for the record type", 6 5 "defs": { 7 6 "main": {
+1
atproto/lexicon/testdata/catalog/subscription.json
··· 5 5 "defs": { 6 6 "main": { 7 7 "type": "subscription", 8 + "description": "an example event stream", 8 9 "parameters": { 9 10 "type": "params", 10 11 "properties": {