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
···50505151// Inserts a schema loaded from a JSON file in to the catalog.
5252func (c *BaseCatalog) AddSchemaFile(sf SchemaFile) error {
5353- if sf.Lexicon != 1 {
5454- return fmt.Errorf("unsupported lexicon language version: %d", sf.Lexicon)
5353+5454+ if err := sf.CheckSchema(); err != nil {
5555+ return err
5556 }
5757+5658 base := sf.ID
5759 for frag, def := range sf.Defs {
5858- if len(frag) == 0 || strings.Contains(frag, "#") || strings.Contains(frag, ".") {
5959- // TODO: more validation here?
6060- return fmt.Errorf("schema name invalid: %s", frag)
6161- }
6260 name := base + "#" + frag
6361 if _, ok := c.schemas[name]; ok {
6462 return fmt.Errorf("catalog already contained a schema with name: %s", name)
6563 }
6666- // "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."
6767- switch s := def.Inner.(type) {
6868- case SchemaRecord, SchemaQuery, SchemaProcedure, SchemaSubscription, SchemaPermissionSet:
6969- if frag != "main" {
7070- return fmt.Errorf("record, query, procedure, and subscription types must be 'main', not: %s", frag)
7171- }
7272- case SchemaToken:
7373- // add fully-qualified name to token
7474- s.fullName = name
7575- def.Inner = s
7676- }
7777- def.SetBase(base)
7878- if err := def.CheckSchema(); err != nil {
7979- return err
8080- }
8164 s := Schema{
8265 ID: name,
8366 Def: def.Inner,
···9174func (c *BaseCatalog) addSchemaFromBytes(b []byte) error {
9275 var sf SchemaFile
9376 if err := json.Unmarshal(b, &sf); err != nil {
7777+ return err
7878+ }
7979+ if err := sf.FinishParse(); err != nil {
9480 return err
9581 }
9682 if err := c.AddSchemaFile(sf); err != nil {
+34-27
atproto/lexicon/language.go
···1212 "github.com/rivo/uniseg"
1313)
14141515-// Serialization helper type for top-level Lexicon schema JSON objects (files)
1616-type SchemaFile struct {
1717- Lexicon int `json:"lexicon"` // must be 1
1818- ID string `json:"id"`
1919- Description *string `json:"description,omitempty"`
2020- Defs map[string]SchemaDef `json:"defs"`
2121-}
2222-2315// enum type to represent any of the schema fields
2416type SchemaDef struct {
2517 Inner any
···7466}
75677668// Helper to recurse down the definition tree and set full references on any sub-schemas which need to embed that metadata
7777-func (s *SchemaDef) SetBase(base string) {
6969+func (s *SchemaDef) setBase(base string) {
7870 switch v := s.Inner.(type) {
7971 case SchemaRecord:
8072 for i, val := range v.Record.Properties {
8181- val.SetBase(base)
7373+ val.setBase(base)
8274 v.Record.Properties[i] = val
8375 }
8476 s.Inner = v
8577 case SchemaQuery:
8678 if v.Parameters != nil {
8779 for i, val := range v.Parameters.Properties {
8888- val.SetBase(base)
8080+ val.setBase(base)
8981 v.Parameters.Properties[i] = val
9082 }
9183 }
9284 if v.Output != nil && v.Output.Schema != nil {
9393- v.Output.Schema.SetBase(base)
8585+ v.Output.Schema.setBase(base)
9486 }
9587 s.Inner = v
9688 case SchemaProcedure:
9789 if v.Parameters != nil {
9890 for i, val := range v.Parameters.Properties {
9999- val.SetBase(base)
9191+ val.setBase(base)
10092 v.Parameters.Properties[i] = val
10193 }
10294 }
10395 if v.Input != nil && v.Input.Schema != nil {
104104- v.Input.Schema.SetBase(base)
9696+ v.Input.Schema.setBase(base)
10597 }
10698 if v.Output != nil && v.Output.Schema != nil {
107107- v.Output.Schema.SetBase(base)
9999+ v.Output.Schema.setBase(base)
108100 }
109101 s.Inner = v
110102 case SchemaSubscription:
111103 if v.Parameters != nil {
112104 for i, val := range v.Parameters.Properties {
113113- val.SetBase(base)
105105+ val.setBase(base)
114106 v.Parameters.Properties[i] = val
115107 }
116108 }
117109 if v.Message != nil {
118118- v.Message.Schema.SetBase(base)
110110+ v.Message.Schema.setBase(base)
119111 }
120112 s.Inner = v
121113 case SchemaArray:
122122- v.Items.SetBase(base)
114114+ v.Items.setBase(base)
123115 s.Inner = v
124116 case SchemaObject:
125117 for i, val := range v.Properties {
126126- val.SetBase(base)
118118+ val.setBase(base)
127119 v.Properties[i] = val
128120 }
129121 s.Inner = v
130122 case SchemaParams:
131123 for i, val := range v.Properties {
132132- val.SetBase(base)
124124+ val.setBase(base)
133125 v.Properties[i] = val
134126 }
135127 s.Inner = v
···412404413405type SchemaPermissionSet struct {
414406 Type string `json:"type"` // "permission-set"
415415- Description *string `json:"description,omitempty"`
407407+ Title *string `json:"title,omitempty"`
408408+ TitleLangs map[string]string `json:"title:langs,omitempty"`
409409+ Detail *string `json:"detail,omitempty"`
410410+ DetailLangs map[string]string `json:"detail:langs,omitempty"`
416411 Permissions []SchemaPermission `json:"permissions"`
417412}
418413419414func (s *SchemaPermissionSet) CheckSchema() error {
415415+ for lang, _ := range s.TitleLangs {
416416+ _, err := syntax.ParseLanguage(lang)
417417+ if err != nil {
418418+ return err
419419+ }
420420+ }
421421+ for lang, _ := range s.DetailLangs {
422422+ _, err := syntax.ParseLanguage(lang)
423423+ if err != nil {
424424+ return err
425425+ }
426426+ }
420427 for _, p := range s.Permissions {
421428 if err := p.CheckSchema(); err != nil {
422429 return err
···492499 if (s.InheritAud == true && s.Audience != "") || (s.InheritAud == false && s.Audience == "") {
493500 return fmt.Errorf("rpc permission must have eith 'aud' or 'inheritAud' defined")
494501 }
495495- if s.Audience != "" {
502502+ if s.Audience != "" && s.Audience != "*" {
496503 // TODO: helper for service refs
497504 parts := strings.SplitN(s.Audience, "#", 3)
498505 if len(parts) != 2 || parts[1] == "" {
···978985type SchemaToken struct {
979986 Type string `json:"type"` // "token"
980987 Description *string `json:"description,omitempty"`
981981- // the fully-qualified identifier of this token
982982- fullName string
988988+ // the fully-qualified identifier of this token. this is not included in the schema file; it must be added when parsing
989989+ FullName string `json:"-"`
983990}
984991985992func (s *SchemaToken) CheckSchema() error {
986986- if s.fullName == "" {
993993+ if s.FullName == "" {
987994 return fmt.Errorf("expected fully-qualified token name")
988995 }
989996 return nil
···9941001 if !ok {
9951002 return fmt.Errorf("expected a string for token, got: %s", reflect.TypeOf(d))
9961003 }
997997- if s.fullName == "" {
10041004+ if s.FullName == "" {
9981005 return fmt.Errorf("token name was not populated at parse time")
9991006 }
10001000- if str != s.fullName {
10071007+ if str != s.FullName {
10011008 return fmt.Errorf("token name did not match expected: %s", str)
10021009 }
10031010 return nil
+68
atproto/lexicon/schemafile.go
···11+package lexicon
22+33+import (
44+ "fmt"
55+ "strings"
66+)
77+88+// Serialization helper type for top-level Lexicon schema JSON objects (files).
99+//
1010+// Note that the [FinishParse] method should always be called after unmarshalling a SchemaFile from JSON.
1111+type SchemaFile struct {
1212+ Lexicon int `json:"lexicon"` // must be 1
1313+ ID string `json:"id"`
1414+ Description *string `json:"description,omitempty"`
1515+ Defs map[string]SchemaDef `json:"defs"`
1616+}
1717+1818+// Helper method which should always be called after parsing a schema file (eg, from JSON).
1919+//
2020+// Does some very basic validation (eg, lexicon language version), and fills in
2121+// internal references (for example full name of tokens).
2222+func (sf *SchemaFile) FinishParse() error {
2323+ if sf.Lexicon != 1 {
2424+ return fmt.Errorf("unsupported lexicon language version: %d", sf.Lexicon)
2525+ }
2626+ base := sf.ID
2727+ for frag, def := range sf.Defs {
2828+ if len(frag) == 0 || strings.Contains(frag, "#") || strings.Contains(frag, ".") {
2929+ // TODO: more validation here?
3030+ return fmt.Errorf("schema name invalid: %s", frag)
3131+ }
3232+ name := base + "#" + frag
3333+ switch s := def.Inner.(type) {
3434+ case SchemaToken:
3535+ // add fully-qualified name to token
3636+ s.FullName = name
3737+ def.Inner = s
3838+ }
3939+ def.setBase(base)
4040+ sf.Defs[frag] = def
4141+ }
4242+ return nil
4343+}
4444+4545+// Calls [SchemaDef.CheckSchema] recursively over all defs
4646+func (sf *SchemaFile) CheckSchema() error {
4747+ if sf.Lexicon != 1 {
4848+ return fmt.Errorf("unsupported lexicon language version: %d", sf.Lexicon)
4949+ }
5050+5151+ for frag, def := range sf.Defs {
5252+ if len(frag) == 0 || strings.Contains(frag, "#") || strings.Contains(frag, ".") {
5353+ // TODO: more validation here?
5454+ return fmt.Errorf("schema name invalid: %s", frag)
5555+ }
5656+ // "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."
5757+ switch def.Inner.(type) {
5858+ case SchemaRecord, SchemaQuery, SchemaProcedure, SchemaSubscription, SchemaPermissionSet:
5959+ if frag != "main" {
6060+ return fmt.Errorf("record, query, procedure, and subscription types must be 'main', not: %s", frag)
6161+ }
6262+ }
6363+ if err := def.CheckSchema(); err != nil {
6464+ return err
6565+ }
6666+ }
6767+ return nil
6868+}
···11{
22 "lexicon": 1,
33 "id": "example.lexicon.minimal.query",
44- "revision": 1,
55- "description": "exercizes many lexicon features for the query type",
44+ "description": "exercises many lexicon features for the query type",
65 "defs": {
76 "main": {
87 "type": "query",
···11{
22 "lexicon": 1,
33 "id": "example.lexicon.procedure",
44- "description": "demonstrates lexicon features for the procedure type",
54 "defs": {
65 "main": {
76 "type": "procedure",
77+ "description": "demonstrates lexicon features for the procedure type",
88 "parameters": {
99 "type": "params",
1010 "properties": {
+1-2
atproto/lexicon/testdata/catalog/query.json
···11{
22 "lexicon": 1,
33 "id": "example.lexicon.query",
44- "revision": 1,
55- "description": "exercizes many lexicon features for the query type",
44+ "description": "exercises many lexicon features for the query type",
65 "defs": {
76 "main": {
87 "type": "query",
-1
atproto/lexicon/testdata/catalog/record.json
···11{
22 "lexicon": 1,
33 "id": "example.lexicon.record",
44- "revision": 1,
54 "description": "demonstrates lexicon features for the record type",
65 "defs": {
76 "main": {