fork of indigo with slightly nicer lexgen

lexicon: initial work on schema parsing

Changed files
+530
atproto
+59
atproto/lexicon/cmd/lextool/main.go
··· 1 + package main 2 + 3 + import ( 4 + "encoding/json" 5 + "fmt" 6 + "io" 7 + "log/slog" 8 + "os" 9 + 10 + "github.com/bluesky-social/indigo/atproto/lexicon" 11 + 12 + "github.com/urfave/cli/v2" 13 + ) 14 + 15 + func main() { 16 + app := cli.App{ 17 + Name: "lex-tool", 18 + Usage: "informal debugging CLI tool for atproto lexicons", 19 + } 20 + app.Commands = []*cli.Command{ 21 + &cli.Command{ 22 + Name: "parse-schema", 23 + Usage: "parse an individual lexicon schema file (JSON)", 24 + Action: runParseSchema, 25 + }, 26 + } 27 + h := slog.NewTextHandler(os.Stderr, &slog.HandlerOptions{Level: slog.LevelDebug}) 28 + slog.SetDefault(slog.New(h)) 29 + app.RunAndExitOnError() 30 + } 31 + 32 + func runParseSchema(cctx *cli.Context) error { 33 + p := cctx.Args().First() 34 + if p == "" { 35 + return fmt.Errorf("need to provide path to a schema file as an argument") 36 + } 37 + 38 + f, err := os.Open(p) 39 + if err != nil { 40 + return err 41 + } 42 + defer func() { _ = f.Close() }() 43 + 44 + b, err := io.ReadAll(f) 45 + if err != nil { 46 + return err 47 + } 48 + 49 + var sf lexicon.SchemaFile 50 + if err := json.Unmarshal(b, &sf); err != nil { 51 + return err 52 + } 53 + out, err := json.MarshalIndent(sf, "", " ") 54 + if err != nil { 55 + return err 56 + } 57 + fmt.Println(string(out)) 58 + return nil 59 + }
+4
atproto/lexicon/docs.go
··· 1 + /* 2 + Package atproto/lexicon provides generic Lexicon schema parsing and run-time validation. 3 + */ 4 + package lexicon
+20
atproto/lexicon/extract.go
··· 1 + package lexicon 2 + 3 + import ( 4 + "encoding/json" 5 + ) 6 + 7 + // Helper type for extracting record type from JSON 8 + type genericSchemaDef struct { 9 + Type string `json:"type"` 10 + } 11 + 12 + // Parses the top-level $type field from generic atproto JSON data 13 + func ExtractTypeJSON(b []byte) (string, error) { 14 + var gsd genericSchemaDef 15 + if err := json.Unmarshal(b, &gsd); err != nil { 16 + return "", err 17 + } 18 + 19 + return gsd.Type, nil 20 + }
+310
atproto/lexicon/language.go
··· 1 + package lexicon 2 + 3 + import ( 4 + "encoding/json" 5 + "fmt" 6 + ) 7 + 8 + // Serialization helper for top-level Lexicon schema JSON objects (files) 9 + type SchemaFile struct { 10 + Lexicon int `json:"lexicon,const=1"` 11 + ID string `json:"id"` 12 + Revision *int `json:"revision,omitempty"` 13 + Description *string `json:"description,omitempty"` 14 + Defs map[string]SchemaDef `json:"defs"` 15 + } 16 + 17 + // enum type to represent any of the schema fields 18 + type SchemaDef struct { 19 + Inner any 20 + } 21 + 22 + func (s SchemaDef) MarshalJSON() ([]byte, error) { 23 + return json.Marshal(s.Inner) 24 + } 25 + 26 + func (s *SchemaDef) UnmarshalJSON(b []byte) error { 27 + t, err := ExtractTypeJSON(b) 28 + if err != nil { 29 + return err 30 + } 31 + switch t { 32 + case "record": 33 + v := new(SchemaRecord) 34 + if err = json.Unmarshal(b, v); err != nil { 35 + return err 36 + } 37 + s.Inner = v 38 + return nil 39 + case "query": 40 + v := new(SchemaQuery) 41 + if err = json.Unmarshal(b, v); err != nil { 42 + return err 43 + } 44 + s.Inner = v 45 + return nil 46 + case "procedure": 47 + v := new(SchemaProcedure) 48 + if err = json.Unmarshal(b, v); err != nil { 49 + return err 50 + } 51 + s.Inner = v 52 + return nil 53 + case "subscription": 54 + v := new(SchemaSubscription) 55 + if err = json.Unmarshal(b, v); err != nil { 56 + return err 57 + } 58 + s.Inner = v 59 + return nil 60 + case "null": 61 + v := new(SchemaNull) 62 + if err = json.Unmarshal(b, v); err != nil { 63 + return err 64 + } 65 + s.Inner = v 66 + return nil 67 + case "boolean": 68 + v := new(SchemaBoolean) 69 + if err = json.Unmarshal(b, v); err != nil { 70 + return err 71 + } 72 + s.Inner = v 73 + return nil 74 + case "integer": 75 + v := new(SchemaInteger) 76 + if err = json.Unmarshal(b, v); err != nil { 77 + return err 78 + } 79 + s.Inner = v 80 + return nil 81 + case "string": 82 + v := new(SchemaString) 83 + if err = json.Unmarshal(b, v); err != nil { 84 + return err 85 + } 86 + s.Inner = v 87 + return nil 88 + case "bytes": 89 + v := new(SchemaBytes) 90 + if err = json.Unmarshal(b, v); err != nil { 91 + return err 92 + } 93 + s.Inner = v 94 + return nil 95 + case "cid-link": 96 + v := new(SchemaCIDLink) 97 + if err = json.Unmarshal(b, v); err != nil { 98 + return err 99 + } 100 + s.Inner = v 101 + return nil 102 + case "array": 103 + v := new(SchemaArray) 104 + if err = json.Unmarshal(b, v); err != nil { 105 + return err 106 + } 107 + s.Inner = v 108 + return nil 109 + case "object": 110 + v := new(SchemaObject) 111 + if err = json.Unmarshal(b, v); err != nil { 112 + return err 113 + } 114 + s.Inner = v 115 + return nil 116 + case "blob": 117 + v := new(SchemaBlob) 118 + if err = json.Unmarshal(b, v); err != nil { 119 + return err 120 + } 121 + s.Inner = v 122 + return nil 123 + case "params": 124 + v := new(SchemaParams) 125 + if err = json.Unmarshal(b, v); err != nil { 126 + return err 127 + } 128 + s.Inner = v 129 + return nil 130 + case "token": 131 + v := new(SchemaToken) 132 + if err = json.Unmarshal(b, v); err != nil { 133 + return err 134 + } 135 + s.Inner = v 136 + return nil 137 + case "ref": 138 + v := new(SchemaRef) 139 + if err = json.Unmarshal(b, v); err != nil { 140 + return err 141 + } 142 + s.Inner = v 143 + return nil 144 + case "union": 145 + v := new(SchemaUnion) 146 + if err = json.Unmarshal(b, v); err != nil { 147 + return err 148 + } 149 + s.Inner = v 150 + return nil 151 + case "unknown": 152 + v := new(SchemaUnknown) 153 + if err = json.Unmarshal(b, v); err != nil { 154 + return err 155 + } 156 + s.Inner = v 157 + return nil 158 + default: 159 + return fmt.Errorf("unexpected schema type: %s", t) 160 + } 161 + return fmt.Errorf("unexpected schema type: %s", t) 162 + } 163 + 164 + type SchemaRecord struct { 165 + Type string `json:"type,const=record"` 166 + Description *string `json:"description,omitempty"` 167 + Key string `json:"key"` 168 + Record SchemaObject `json:"record"` 169 + } 170 + 171 + type SchemaQuery struct { 172 + Type string `json:"type,const=query"` 173 + Description *string `json:"description,omitempty"` 174 + Parameters SchemaParams `json:"parameters"` 175 + Output *SchemaBody `json:"output"` 176 + Errors []SchemaError `json:"errors,omitempty"` // optional 177 + } 178 + 179 + type SchemaProcedure struct { 180 + Type string `json:"type,const=procedure"` 181 + Description *string `json:"description,omitempty"` 182 + Parameters SchemaParams `json:"parameters"` 183 + Output *SchemaBody `json:"output"` // optional 184 + Errors []SchemaError `json:"errors,omitempty"` // optional 185 + Input *SchemaBody `json:"input"` // optional 186 + } 187 + 188 + type SchemaSubscription struct { 189 + Type string `json:"type,const=subscription"` 190 + Description *string `json:"description,omitempty"` 191 + Parameters SchemaParams `json:"parameters"` 192 + Message *SchemaMessage `json:"message,omitempty"` // TODO(specs): is this really optional? 193 + } 194 + 195 + type SchemaBody struct { 196 + Description *string `json:"description,omitempty"` 197 + Encoding string `json:"encoding"` // required, mimetype 198 + Schema *SchemaDef `json:"schema"` // optional; type:object, type:ref, or type:union 199 + } 200 + 201 + type SchemaMessage struct { 202 + Description *string `json:"description,omitempty"` 203 + Schema SchemaDef `json:"schema"` // required; type:union only 204 + } 205 + 206 + type SchemaError struct { 207 + Name string `json:"name"` 208 + Description *string `json:"description"` 209 + } 210 + 211 + type SchemaNull struct { 212 + Type string `json:"type,const=null"` 213 + Description *string `json:"description,omitempty"` 214 + } 215 + 216 + type SchemaBoolean struct { 217 + Type string `json:"type,const=bool"` 218 + Description *string `json:"description,omitempty"` 219 + Default *bool `json:"default,omitempty"` 220 + Const *bool `json:"const,omitempty"` 221 + } 222 + 223 + type SchemaInteger struct { 224 + Type string `json:"type,const=integer"` 225 + Description *string `json:"description,omitempty"` 226 + Minimum *int `json:"minimum,omitempty"` 227 + Maximum *int `json:"maximum,omitempty"` 228 + Enum []int `json:"enum,omitempty"` 229 + Default *int `json:"default,omitempty"` 230 + Const *int `json:"const,omitempty"` 231 + } 232 + 233 + type SchemaString struct { 234 + Type string `json:"type,const=string"` 235 + Description *string `json:"description,omitempty"` 236 + Format *string `json:"format,omitempty"` 237 + MinLength *int `json:"minLength,omitempty"` 238 + MaxLength *int `json:"maxLength,omitempty"` 239 + MinGraphemes *int `json:"minGraphemes,omitempty"` 240 + MaxGraphemes *int `json:"maxGraphemes,omitempty"` 241 + KnownValues []string `json:"knownValues,omitempty"` 242 + Enum []string `json:"enum,omitempty"` 243 + Default *int `json:"default,omitempty"` 244 + Const *int `json:"const,omitempty"` 245 + } 246 + 247 + type SchemaBytes struct { 248 + Type string `json:"type,const=bytes"` 249 + Description *string `json:"description,omitempty"` 250 + MinLength *int `json:"minLength,omitempty"` 251 + MaxLength *int `json:"maxLength,omitempty"` 252 + } 253 + 254 + type SchemaCIDLink struct { 255 + Type string `json:"type,const=cid-link"` 256 + Description *string `json:"description,omitempty"` 257 + } 258 + 259 + type SchemaArray struct { 260 + Type string `json:"type,const=array"` 261 + Description *string `json:"description,omitempty"` 262 + Items SchemaDef `json:"items"` 263 + MinLength *int `json:"minLength,omitempty"` 264 + MaxLength *int `json:"maxLength,omitempty"` 265 + } 266 + 267 + type SchemaObject struct { 268 + Type string `json:"type,const=object"` 269 + Description *string `json:"description,omitempty"` 270 + Properties map[string]SchemaDef `json:"properties"` 271 + Required []string `json:"required,omitempty"` 272 + Nullable []string `json:"nullable,omitempty"` 273 + } 274 + 275 + type SchemaBlob struct { 276 + Type string `json:"type,const=blob"` 277 + Description *string `json:"description,omitempty"` 278 + Accept []string `json:"accept,omitempty"` 279 + MaxSize *int `json:"maxSize,omitempty"` 280 + } 281 + 282 + type SchemaParams struct { 283 + Type string `json:"type,const=params"` 284 + Description *string `json:"description,omitempty"` 285 + Properties map[string]SchemaDef `json:"properties"` // boolean, integer, string, or unknown; or an array of these types 286 + Required []string `json:"required,omitempty"` 287 + } 288 + 289 + type SchemaToken struct { 290 + Type string `json:"type,const=token"` 291 + Description *string `json:"description,omitempty"` 292 + } 293 + 294 + type SchemaRef struct { 295 + Type string `json:"type,const=ref"` 296 + Description *string `json:"description,omitempty"` 297 + Ref string `json:"ref"` 298 + } 299 + 300 + type SchemaUnion struct { 301 + Type string `json:"type,const=union"` 302 + Description *string `json:"description,omitempty"` 303 + Refs []string `json:"refs"` 304 + Closed *bool `json:"closed,omitempty"` 305 + } 306 + 307 + type SchemaUnknown struct { 308 + Type string `json:"type,const=unknown"` 309 + Description *string `json:"description,omitempty"` 310 + }
+47
atproto/lexicon/language_test.go
··· 1 + package lexicon 2 + 3 + import ( 4 + "encoding/json" 5 + "io" 6 + "os" 7 + "testing" 8 + 9 + "github.com/stretchr/testify/assert" 10 + ) 11 + 12 + func TestBasicLabelLexicon(t *testing.T) { 13 + assert := assert.New(t) 14 + 15 + f, err := os.Open("testdata/com_atproto_label_defs.json") 16 + if err != nil { 17 + t.Fatal(err) 18 + } 19 + defer func() { _ = f.Close() }() 20 + 21 + jsonBytes, err := io.ReadAll(f) 22 + if err != nil { 23 + t.Fatal(err) 24 + } 25 + 26 + var schema SchemaFile 27 + if err := json.Unmarshal(jsonBytes, &schema); err != nil { 28 + t.Fatal(err) 29 + } 30 + 31 + outBytes, err := json.Marshal(schema) 32 + if err != nil { 33 + t.Fatal(err) 34 + } 35 + 36 + var beforeMap map[string]any 37 + if err := json.Unmarshal(jsonBytes, &beforeMap); err != nil { 38 + t.Fatal(err) 39 + } 40 + 41 + var afterMap map[string]any 42 + if err := json.Unmarshal(outBytes, &afterMap); err != nil { 43 + t.Fatal(err) 44 + } 45 + 46 + assert.Equal(beforeMap, afterMap) 47 + }
+12
atproto/lexicon/lexicon.go
··· 1 + package lexicon 2 + 3 + import ( 4 + // "github.com/bluesky-social/indigo/atproto/syntax" 5 + ) 6 + 7 + // An aggregation of lexicon schemas, and methods for validating generic data against those schemas. 8 + type Catalog struct { 9 + } 10 + 11 + type Schema struct { 12 + }
+78
atproto/lexicon/testdata/com_atproto_label_defs.json
··· 1 + { 2 + "defs": { 3 + "label": { 4 + "description": "Metadata tag on an atproto resource (eg, repo or record)", 5 + "properties": { 6 + "cid": { 7 + "description": "optionally, CID specifying the specific version of 'uri' resource this label applies to", 8 + "format": "cid", 9 + "type": "string" 10 + }, 11 + "cts": { 12 + "description": "timestamp when this label was created", 13 + "format": "datetime", 14 + "type": "string" 15 + }, 16 + "neg": { 17 + "description": "if true, this is a negation label, overwriting a previous label", 18 + "type": "boolean" 19 + }, 20 + "src": { 21 + "description": "DID of the actor who created this label", 22 + "format": "did", 23 + "type": "string" 24 + }, 25 + "uri": { 26 + "description": "AT URI of the record, repository (account), or other resource which this label applies to", 27 + "format": "uri", 28 + "type": "string" 29 + }, 30 + "val": { 31 + "description": "the short string name of the value or type of this label", 32 + "maxLength": 128, 33 + "type": "string" 34 + } 35 + }, 36 + "required": [ 37 + "src", 38 + "uri", 39 + "val", 40 + "cts" 41 + ], 42 + "type": "object" 43 + }, 44 + "selfLabel": { 45 + "description": "Metadata tag on an atproto record, published by the author within the record. Note -- schemas should use #selfLabels, not #selfLabel.", 46 + "properties": { 47 + "val": { 48 + "description": "the short string name of the value or type of this label", 49 + "maxLength": 128, 50 + "type": "string" 51 + } 52 + }, 53 + "required": [ 54 + "val" 55 + ], 56 + "type": "object" 57 + }, 58 + "selfLabels": { 59 + "description": "Metadata tags on an atproto record, published by the author within the record.", 60 + "properties": { 61 + "values": { 62 + "items": { 63 + "ref": "#selfLabel", 64 + "type": "ref" 65 + }, 66 + "maxLength": 10, 67 + "type": "array" 68 + } 69 + }, 70 + "required": [ 71 + "values" 72 + ], 73 + "type": "object" 74 + } 75 + }, 76 + "id": "com.atproto.label.defs", 77 + "lexicon": 1 78 + }