tangled
alpha
login
or
join now
lekkice.moe
/
atex
forked from
comet.sh/atex
0
fork
atom
A set of utilities for working with the AT Protocol in Elixir.
0
fork
atom
overview
issues
pulls
1
pipelines
feat(deflexicon): generate typespecs
ovyerus.com
7 months ago
ee40feb3
9e142f68
verified
This commit was signed with the committer's
known signature
.
ovyerus.com
SSH Key Fingerprint:
SHA256:mXbp9WNBIT0nRNe28t2hrxfSwnSX7UBeW2DVlIyf0uw=
+146
-112
3 changed files
expand all
collapse all
unified
split
lib
atex
lexicon
validators.ex
lexicon.ex
atproto
sh
comet
v0
feed
track.ex
+141
-36
lib/atex/lexicon.ex
···
24
24
|> then(&Recase.Enumerable.atomize_keys/1)
25
25
|> then(&Atex.Lexicon.Schema.lexicon!/1)
26
26
27
27
-
# TODO: support returning typedefs
28
27
defs =
29
28
lexicon.defs
30
29
|> Enum.flat_map(fn {def_name, def} -> def_to_schema(lexicon.id, def_name, def) end)
31
31
-
|> Enum.map(fn {schema_key, quoted_schema} ->
30
30
+
|> Enum.map(fn {schema_key, quoted_schema, quoted_type} ->
31
31
+
identity_type =
32
32
+
if schema_key === :main do
33
33
+
quote do
34
34
+
@type t() :: unquote(quoted_type)
35
35
+
end
36
36
+
end
37
37
+
32
38
quote do
39
39
+
@type unquote(schema_key)() :: unquote(quoted_type)
40
40
+
unquote(identity_type)
41
41
+
33
42
defschema unquote(schema_key), unquote(quoted_schema)
34
43
end
35
44
end)
···
41
50
end
42
51
end
43
52
44
44
-
# TODO: generate typedefs
45
53
@spec def_to_schema(nsid :: String.t(), def_name :: String.t(), lexicon_def :: map()) ::
46
46
-
list({key :: atom(), quoted :: term()})
54
54
+
list({key :: atom(), quoted_schema :: term(), quoted_type :: term()})
47
55
48
56
defp def_to_schema(nsid, def_name, %{type: "record", record: record}) do
49
57
# TODO: record rkey format validator
50
58
def_to_schema(nsid, def_name, record)
51
59
end
52
60
61
61
+
# TODO: add `$type` field. It's just a string though.
53
62
defp def_to_schema(
54
63
nsid,
55
64
def_name,
···
63
72
64
73
properties
65
74
|> Enum.map(fn {key, field} ->
66
66
-
field_to_schema(field, nsid)
67
67
-
|> then(
68
68
-
&if key in nullable, do: quote(do: {:either, {{:literal, nil}, unquote(&1)}}), else: &1
69
69
-
)
70
70
-
|> then(&if key in required, do: quote(do: {:required, unquote(&1)}), else: &1)
71
71
-
|> then(&{key, &1})
75
75
+
{quoted_schema, quoted_type} = field_to_schema(field, nsid)
76
76
+
is_nullable = key in nullable
77
77
+
is_required = key in required
78
78
+
79
79
+
quoted_schema =
80
80
+
quoted_schema
81
81
+
|> then(
82
82
+
&if is_nullable, do: quote(do: {:either, {{:literal, nil}, unquote(&1)}}), else: &1
83
83
+
)
84
84
+
|> then(&if is_required, do: quote(do: {:required, unquote(&1)}), else: &1)
85
85
+
|> then(&{key, &1})
86
86
+
87
87
+
key_type = if is_required, do: :required, else: :optional
88
88
+
89
89
+
quoted_type =
90
90
+
quoted_type
91
91
+
|> then(
92
92
+
&if is_nullable do
93
93
+
{:|, [], [&1, nil]}
94
94
+
else
95
95
+
&1
96
96
+
end
97
97
+
)
98
98
+
|> then(&{{key_type, [], [key]}, &1})
99
99
+
100
100
+
{quoted_schema, quoted_type}
101
101
+
end)
102
102
+
|> Enum.reduce({[], []}, fn {quoted_schema, quoted_type}, {schemas, types} ->
103
103
+
{[quoted_schema | schemas], [quoted_type | types]}
72
104
end)
73
73
-
|> then(&{:%{}, [], &1})
74
74
-
|> then(&[{atomise(def_name), &1}])
105
105
+
|> then(fn {quoted_schemas, quoted_types} ->
106
106
+
[{atomise(def_name), {:%{}, [], quoted_schemas}, {:%{}, [], quoted_types}}]
107
107
+
end)
75
108
end
76
109
77
110
# TODO: validating errors?
···
154
187
155
188
defp def_to_schema(_nsid, def_name, %{type: "token"}) do
156
189
# TODO: make it a validator that expects the nsid + key.
157
157
-
[{atomise(def_name), :string}]
190
190
+
[
191
191
+
{
192
192
+
atomise(def_name),
193
193
+
:string,
194
194
+
quote do
195
195
+
String.t()
196
196
+
end
197
197
+
}
198
198
+
]
158
199
end
159
200
160
201
defp def_to_schema(nsid, def_name, %{type: type} = def)
···
168
209
"cid-link",
169
210
"unknown"
170
211
] do
171
171
-
[{atomise(def_name), field_to_schema(def, nsid)}]
212
212
+
{quoted_schema, quoted_type} = field_to_schema(def, nsid)
213
213
+
[{atomise(def_name), quoted_schema, quoted_type}]
172
214
end
173
215
174
174
-
@spec field_to_schema(field_def :: %{type: String.t()}, nsid :: String.t()) :: Peri.schema_def()
216
216
+
@spec field_to_schema(field_def :: %{type: String.t()}, nsid :: String.t()) ::
217
217
+
{quoted_schema :: term(), quoted_typespec :: term()}
175
218
defp field_to_schema(%{type: "string"} = field, _nsid) do
176
219
fixed_schema = const_or_enum(field)
177
220
···
189
232
|> Enum.map(fn {k, v} -> {Recase.to_snake(k), v} end)
190
233
|> then(&{:custom, {Validators.String, :validate, [&1]}})
191
234
|> maybe_default(field)
192
192
-
|> then(&Macro.escape/1)
235
235
+
|> then(
236
236
+
&{Macro.escape(&1),
237
237
+
quote do
238
238
+
String.t()
239
239
+
end}
240
240
+
)
193
241
end
194
242
end
195
243
196
244
defp field_to_schema(%{type: "boolean"} = field, _nsid) do
197
245
(const(field) || :boolean)
198
246
|> maybe_default(field)
199
199
-
|> then(&Macro.escape/1)
247
247
+
|> then(
248
248
+
&{Macro.escape(&1),
249
249
+
quote do
250
250
+
boolean()
251
251
+
end}
252
252
+
)
200
253
end
201
254
202
255
defp field_to_schema(%{type: "integer"} = field, _nsid) do
···
211
264
|> then(&{:custom, {Validators.Integer, [&1]}})
212
265
|> maybe_default(field)
213
266
end
214
214
-
|> then(&Macro.escape/1)
267
267
+
|> then(
268
268
+
&{
269
269
+
Macro.escape(&1),
270
270
+
# TODO: turn into range definition based on maximum/minimum
271
271
+
quote do
272
272
+
integer()
273
273
+
end
274
274
+
}
275
275
+
)
215
276
end
216
277
217
278
defp field_to_schema(%{type: "array", items: items} = field, nsid) do
218
218
-
inner_schema = field_to_schema(items, nsid)
279
279
+
{inner_schema, inner_type} = field_to_schema(items, nsid)
219
280
220
281
field
221
282
|> Map.take([:maxLength, :minLength])
···
228
289
{inner_schema, _} = Code.eval_quoted(quoted_inner_schema)
229
290
{:custom, {:{}, c, [Validators.Array, :validate, [inner_schema | args]]}}
230
291
end)
292
292
+
|> then(
293
293
+
&{&1,
294
294
+
quote do
295
295
+
list(unquote(inner_type))
296
296
+
end}
297
297
+
)
231
298
end
232
299
233
300
defp field_to_schema(%{type: "blob"} = field, _nsid) do
···
235
302
|> Map.take([:accept, :maxSize])
236
303
|> Enum.map(fn {k, v} -> {Recase.to_snake(k), v} end)
237
304
|> Validators.blob()
238
238
-
|> then(&Macro.escape/1)
305
305
+
|> then(
306
306
+
&{Macro.escape(&1),
307
307
+
quote do
308
308
+
Validators.blob()
309
309
+
end}
310
310
+
)
239
311
end
240
312
241
313
defp field_to_schema(%{type: "bytes"} = field, _nsid) do
···
243
315
|> Map.take([:maxLength, :minLength])
244
316
|> Enum.map(fn {k, v} -> {Recase.to_snake(k), v} end)
245
317
|> Validators.bytes()
246
246
-
|> then(&Macro.escape/1)
318
318
+
|> then(
319
319
+
&{Macro.escape(&1),
320
320
+
quote do
321
321
+
Validators.bytes()
322
322
+
end}
323
323
+
)
247
324
end
248
325
249
326
defp field_to_schema(%{type: "cid-link"}, _nsid) do
250
327
Validators.cid_link()
251
251
-
|> then(&Macro.escape/1)
328
328
+
|> then(
329
329
+
&{Macro.escape(&1),
330
330
+
quote do
331
331
+
Validators.cid_link()
332
332
+
end}
333
333
+
)
252
334
end
253
335
254
336
# TODO: do i need to make sure these two deal with brands? Check objects in atp.tools
···
258
340
|> Atex.NSID.expand_possible_fragment_shorthand(ref)
259
341
|> Atex.NSID.to_atom_with_fragment()
260
342
261
261
-
quote do
262
262
-
unquote(nsid).get_schema(unquote(fragment))
263
263
-
end
343
343
+
{quote do
344
344
+
unquote(nsid).get_schema(unquote(fragment))
345
345
+
end,
346
346
+
quote do
347
347
+
unquote(nsid).unquote(fragment)()
348
348
+
end}
264
349
end
265
350
266
351
defp field_to_schema(%{type: "union", refs: refs}, nsid) do
267
267
-
# refs =
268
352
refs
269
353
|> Enum.map(fn ref ->
270
354
{nsid, fragment} =
···
272
356
|> Atex.NSID.expand_possible_fragment_shorthand(ref)
273
357
|> Atex.NSID.to_atom_with_fragment()
274
358
275
275
-
quote do
276
276
-
unquote(nsid).get_schema(unquote(fragment))
277
277
-
end
359
359
+
{quote do
360
360
+
unquote(nsid).get_schema(unquote(fragment))
361
361
+
end,
362
362
+
quote do
363
363
+
unquote(nsid).unquote(fragment)()
364
364
+
end}
278
365
end)
279
279
-
|> then(
280
280
-
"e do
281
281
-
{:oneof, unquote(&1)}
282
282
-
end
283
283
-
)
366
366
+
|> Enum.reduce({[], []}, fn {quoted_schema, quoted_type}, {schemas, types} ->
367
367
+
{[quoted_schema | schemas], [quoted_type | types]}
368
368
+
end)
369
369
+
|> then(fn {schemaa, types} ->
370
370
+
{quote do
371
371
+
{:oneof, unquote(schemaa)}
372
372
+
end,
373
373
+
quote do
374
374
+
unquote(join_with_pipe(types))
375
375
+
end}
376
376
+
end)
284
377
end
285
378
286
379
# TODO: apparently should be a data object, not a primitive?
287
380
defp field_to_schema(%{type: "unknown"}, _nsid) do
288
288
-
:any
381
381
+
{:any,
382
382
+
quote do
383
383
+
term()
384
384
+
end}
289
385
end
290
386
291
291
-
defp field_to_schema(_field_def, _nsid), do: nil
387
387
+
defp field_to_schema(_field_def, _nsid), do: {nil, nil}
292
388
293
389
defp maybe_default(schema, field) do
294
390
if field[:default] != nil,
···
306
402
307
403
defp atomise(x) when is_atom(x), do: x
308
404
defp atomise(x) when is_binary(x), do: String.to_atom(x)
405
405
+
406
406
+
defp join_with_pipe(list) when is_list(list) do
407
407
+
[piped] = do_join_with_pipe(list)
408
408
+
piped
409
409
+
end
410
410
+
411
411
+
defp do_join_with_pipe([head]), do: [head]
412
412
+
defp do_join_with_pipe([head | tail]), do: [{:|, [], [head | do_join_with_pipe(tail)]}]
413
413
+
defp do_join_with_pipe([]), do: []
309
414
end
+5
-1
lib/atex/lexicon/validators.ex
···
3
3
4
4
@type blob_option() :: {:accept, list(String.t())} | {:max_size, pos_integer()}
5
5
6
6
-
@type blob_t() ::
6
6
+
@type blob() ::
7
7
%{
8
8
"$type": String.t(),
9
9
ref: %{"$link": String.t()},
···
14
14
cid: String.t(),
15
15
mimeType: String.t()
16
16
}
17
17
+
18
18
+
@type cid_link() :: %{"$link": String.t()}
19
19
+
20
20
+
@type bytes() :: %{"$bytes": binary()}
17
21
18
22
@spec string(list(Validators.String.option())) :: Peri.custom_def()
19
23
def string(options \\ []), do: {:custom, {Validators.String, :validate, [options]}}
-75
lib/atproto/sh/comet/v0/feed/track.ex
···
1
1
defmodule Sh.Comet.V0.Feed.Track do
2
2
-
@moduledoc """
3
3
-
The following `deflexicon` call should result in something similar to the following output:
4
4
-
5
5
-
import Peri
6
6
-
import Atex.Lexicon.Validators
7
7
-
8
8
-
@type main() :: %{}
9
9
-
10
10
-
"""
11
2
use Atex.Lexicon
12
12
-
# import Atex.Lexicon
13
13
-
# import Atex.Lexicon.Validators
14
14
-
# import Peri
15
15
-
16
16
-
# TODO: need an example with `nullable` fields to demonstrate how those are handled (and also the weird extra types in lexicon defs like union)
17
17
-
18
18
-
@type main() :: %{
19
19
-
required(:audio) => Atex.Lexicon.Validators.blob_t(),
20
20
-
required(:title) => String.t(),
21
21
-
required(:createdAt) => String.t(),
22
22
-
# TODO: check if peri replaces with `nil` or omits them completely.
23
23
-
optional(:description) => String.t(),
24
24
-
optional(:descriptionFacets) => Sh.Comet.V0.Richtext.Facet.main(),
25
25
-
optional(:explicit) => boolean(),
26
26
-
optional(:image) => Atex.Lexicon.Validators.blob_t(),
27
27
-
optional(:link) => Sh.Comet.V0.Feed.Defs.link(),
28
28
-
optional(:releasedAt) => String.t(),
29
29
-
optional(:tags) => list(String.t())
30
30
-
}
31
31
-
32
32
-
@type view() :: %{
33
33
-
required(:uri) => String.t(),
34
34
-
required(:cid) => String.t(),
35
35
-
required(:author) => Sh.Comet.V0.Actor.Profile.viewFull(),
36
36
-
required(:audio) => String.t(),
37
37
-
required(:record) => main(),
38
38
-
required(:indexedAt) => String.t(),
39
39
-
optional(:image) => String.t(),
40
40
-
optional(:commentCount) => integer(),
41
41
-
optional(:likeCount) => integer(),
42
42
-
optional(:playCount) => integer(),
43
43
-
optional(:repostCount) => integer(),
44
44
-
optional(:viewer) => Sh.Comet.V0.Feed.Defs.viewerState()
45
45
-
}
46
46
-
47
47
-
# Should probably be a separate validator for all rkey formats.
48
48
-
# defschema :main_rkey, string(format: :tid)
49
49
-
50
50
-
# defschema :main, %{
51
51
-
# audio: {:required, blob(accept: ["audio/ogg"], max_size: 100_000_000)},
52
52
-
# title: {:required, string(min_length: 1, max_length: 2560, max_graphemes: 256)},
53
53
-
# createdAt: {:required, string(format: :datetime)},
54
54
-
# description: string(max_length: 20000, max_graphemes: 2000),
55
55
-
# # This is `ref`
56
56
-
# descriptionFacets: Sh.Comet.V0.Richtext.Facet.get_schema(:main),
57
57
-
# explicit: :boolean,
58
58
-
# image: blob(accept: ["image/png", "image/jpeg"], max_size: 1_000_000),
59
59
-
# link: Sh.Comet.V0.Feed.Defs.get_schema(:link),
60
60
-
# releasedAt: string(format: :datetime),
61
61
-
# tags: array(string(max_graphemes: 64, max_length: 640), max_length: 8)
62
62
-
# }
63
63
-
64
64
-
# defschema :view, %{
65
65
-
# uri: {:required, string(format: :at_uri)},
66
66
-
# cid: {:required, string(format: :cid)},
67
67
-
# author: {:required, Sh.Comet.V0.Actor.Profile.get_schema(:viewFull)},
68
68
-
# audio: {:required, string(format: :uri)},
69
69
-
# record: {:required, get_schema(:main)},
70
70
-
# indexedAt: {:required, string(format: :datetime)},
71
71
-
# image: string(format: :uri),
72
72
-
# commentCount: :integer,
73
73
-
# likeCount: :integer,
74
74
-
# playCount: :integer,
75
75
-
# repostCount: :integer,
76
76
-
# viewer: Sh.Comet.V0.Feed.Defs.get_schema(:viewerState)
77
77
-
# }
78
3
79
4
deflexicon(%{
80
5
"defs" => %{