+59
atproto/lexicon/cmd/lextool/main.go
+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
+4
atproto/lexicon/docs.go
+20
atproto/lexicon/extract.go
+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
+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
+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
+12
atproto/lexicon/lexicon.go
+78
atproto/lexicon/testdata/com_atproto_label_defs.json
+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
+
}