Skip to content

Commit 16f536e

Browse files
committed
feat: mix task to codegen lexicon validations
1 parent 9e142f6 commit 16f536e

File tree

3 files changed

+147
-112
lines changed

3 files changed

+147
-112
lines changed

lib/atex/lexicon.ex

Lines changed: 142 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -24,12 +24,21 @@ defmodule Atex.Lexicon do
2424
|> then(&Recase.Enumerable.atomize_keys/1)
2525
|> then(&Atex.Lexicon.Schema.lexicon!/1)
2626

27-
# TODO: support returning typedefs
2827
defs =
2928
lexicon.defs
3029
|> Enum.flat_map(fn {def_name, def} -> def_to_schema(lexicon.id, def_name, def) end)
31-
|> Enum.map(fn {schema_key, quoted_schema} ->
30+
|> Enum.map(fn {schema_key, quoted_schema, quoted_type} ->
31+
identity_type =
32+
if schema_key === :main do
33+
quote do
34+
@type t() :: unquote(quoted_type)
35+
end
36+
end
37+
3238
quote do
39+
@type unquote(schema_key)() :: unquote(quoted_type)
40+
unquote(identity_type)
41+
3342
defschema unquote(schema_key), unquote(quoted_schema)
3443
end
3544
end)
@@ -41,15 +50,15 @@ defmodule Atex.Lexicon do
4150
end
4251
end
4352

44-
# TODO: generate typedefs
4553
@spec def_to_schema(nsid :: String.t(), def_name :: String.t(), lexicon_def :: map()) ::
46-
list({key :: atom(), quoted :: term()})
54+
list({key :: atom(), quoted_schema :: term(), quoted_type :: term()})
4755

4856
defp def_to_schema(nsid, def_name, %{type: "record", record: record}) do
4957
# TODO: record rkey format validator
5058
def_to_schema(nsid, def_name, record)
5159
end
5260

61+
# TODO: add `$type` field. It's just a string though.
5362
defp def_to_schema(
5463
nsid,
5564
def_name,
@@ -63,15 +72,39 @@ defmodule Atex.Lexicon do
6372

6473
properties
6574
|> Enum.map(fn {key, field} ->
66-
field_to_schema(field, nsid)
67-
|> then(
68-
&if key in nullable, do: quote(do: {:either, {{:literal, nil}, unquote(&1)}}), else: &1
69-
)
70-
|> then(&if key in required, do: quote(do: {:required, unquote(&1)}), else: &1)
71-
|> then(&{key, &1})
75+
{quoted_schema, quoted_type} = field_to_schema(field, nsid)
76+
is_nullable = key in nullable
77+
is_required = key in required
78+
79+
quoted_schema =
80+
quoted_schema
81+
|> then(
82+
&if is_nullable, do: quote(do: {:either, {{:literal, nil}, unquote(&1)}}), else: &1
83+
)
84+
|> then(&if is_required, do: quote(do: {:required, unquote(&1)}), else: &1)
85+
|> then(&{key, &1})
86+
87+
key_type = if is_required, do: :required, else: :optional
88+
89+
quoted_type =
90+
quoted_type
91+
|> then(
92+
&if is_nullable do
93+
{:|, [], [&1, nil]}
94+
else
95+
&1
96+
end
97+
)
98+
|> then(&{{key_type, [], [key]}, &1})
99+
100+
{quoted_schema, quoted_type}
101+
end)
102+
|> Enum.reduce({[], []}, fn {quoted_schema, quoted_type}, {schemas, types} ->
103+
{[quoted_schema | schemas], [quoted_type | types]}
104+
end)
105+
|> then(fn {quoted_schemas, quoted_types} ->
106+
[{atomise(def_name), {:%{}, [], quoted_schemas}, {:%{}, [], quoted_types}}]
72107
end)
73-
|> then(&{:%{}, [], &1})
74-
|> then(&[{atomise(def_name), &1}])
75108
end
76109

77110
# TODO: validating errors?
@@ -154,7 +187,15 @@ defmodule Atex.Lexicon do
154187

155188
defp def_to_schema(_nsid, def_name, %{type: "token"}) do
156189
# TODO: make it a validator that expects the nsid + key.
157-
[{atomise(def_name), :string}]
190+
[
191+
{
192+
atomise(def_name),
193+
:string,
194+
quote do
195+
String.t()
196+
end
197+
}
198+
]
158199
end
159200

160201
defp def_to_schema(nsid, def_name, %{type: type} = def)
@@ -168,10 +209,12 @@ defmodule Atex.Lexicon do
168209
"cid-link",
169210
"unknown"
170211
] do
171-
[{atomise(def_name), field_to_schema(def, nsid)}]
212+
{quoted_schema, quoted_type} = field_to_schema(def, nsid)
213+
[{atomise(def_name), quoted_schema, quoted_type}]
172214
end
173215

174-
@spec field_to_schema(field_def :: %{type: String.t()}, nsid :: String.t()) :: Peri.schema_def()
216+
@spec field_to_schema(field_def :: %{type: String.t()}, nsid :: String.t()) ::
217+
{quoted_schema :: term(), quoted_typespec :: term()}
175218
defp field_to_schema(%{type: "string"} = field, _nsid) do
176219
fixed_schema = const_or_enum(field)
177220

@@ -189,14 +232,24 @@ defmodule Atex.Lexicon do
189232
|> Enum.map(fn {k, v} -> {Recase.to_snake(k), v} end)
190233
|> then(&{:custom, {Validators.String, :validate, [&1]}})
191234
|> maybe_default(field)
192-
|> then(&Macro.escape/1)
235+
|> then(
236+
&{Macro.escape(&1),
237+
quote do
238+
String.t()
239+
end}
240+
)
193241
end
194242
end
195243

196244
defp field_to_schema(%{type: "boolean"} = field, _nsid) do
197245
(const(field) || :boolean)
198246
|> maybe_default(field)
199-
|> then(&Macro.escape/1)
247+
|> then(
248+
&{Macro.escape(&1),
249+
quote do
250+
boolean()
251+
end}
252+
)
200253
end
201254

202255
defp field_to_schema(%{type: "integer"} = field, _nsid) do
@@ -211,11 +264,19 @@ defmodule Atex.Lexicon do
211264
|> then(&{:custom, {Validators.Integer, [&1]}})
212265
|> maybe_default(field)
213266
end
214-
|> then(&Macro.escape/1)
267+
|> then(
268+
&{
269+
Macro.escape(&1),
270+
# TODO: turn into range definition based on maximum/minimum
271+
quote do
272+
integer()
273+
end
274+
}
275+
)
215276
end
216277

217278
defp field_to_schema(%{type: "array", items: items} = field, nsid) do
218-
inner_schema = field_to_schema(items, nsid)
279+
{inner_schema, inner_type} = field_to_schema(items, nsid)
219280

220281
field
221282
|> Map.take([:maxLength, :minLength])
@@ -228,27 +289,48 @@ defmodule Atex.Lexicon do
228289
{inner_schema, _} = Code.eval_quoted(quoted_inner_schema)
229290
{:custom, {:{}, c, [Validators.Array, :validate, [inner_schema | args]]}}
230291
end)
292+
|> then(
293+
&{&1,
294+
quote do
295+
list(unquote(inner_type))
296+
end}
297+
)
231298
end
232299

233300
defp field_to_schema(%{type: "blob"} = field, _nsid) do
234301
field
235302
|> Map.take([:accept, :maxSize])
236303
|> Enum.map(fn {k, v} -> {Recase.to_snake(k), v} end)
237304
|> Validators.blob()
238-
|> then(&Macro.escape/1)
305+
|> then(
306+
&{Macro.escape(&1),
307+
quote do
308+
Validators.blob()
309+
end}
310+
)
239311
end
240312

241313
defp field_to_schema(%{type: "bytes"} = field, _nsid) do
242314
field
243315
|> Map.take([:maxLength, :minLength])
244316
|> Enum.map(fn {k, v} -> {Recase.to_snake(k), v} end)
245317
|> Validators.bytes()
246-
|> then(&Macro.escape/1)
318+
|> then(
319+
&{Macro.escape(&1),
320+
quote do
321+
Validators.bytes()
322+
end}
323+
)
247324
end
248325

249326
defp field_to_schema(%{type: "cid-link"}, _nsid) do
250327
Validators.cid_link()
251-
|> then(&Macro.escape/1)
328+
|> then(
329+
&{Macro.escape(&1),
330+
quote do
331+
Validators.cid_link()
332+
end}
333+
)
252334
end
253335

254336
# TODO: do i need to make sure these two deal with brands? Check objects in atp.tools
@@ -258,37 +340,52 @@ defmodule Atex.Lexicon do
258340
|> Atex.NSID.expand_possible_fragment_shorthand(ref)
259341
|> Atex.NSID.to_atom_with_fragment()
260342

261-
quote do
262-
unquote(nsid).get_schema(unquote(fragment))
263-
end
343+
{quote do
344+
unquote(nsid).get_schema(unquote(fragment))
345+
end,
346+
quote do
347+
unquote(nsid).unquote(fragment)()
348+
end}
264349
end
265350

266351
defp field_to_schema(%{type: "union", refs: refs}, nsid) do
267-
# refs =
268352
refs
269353
|> Enum.map(fn ref ->
270354
{nsid, fragment} =
271355
nsid
272356
|> Atex.NSID.expand_possible_fragment_shorthand(ref)
273357
|> Atex.NSID.to_atom_with_fragment()
274358

275-
quote do
276-
unquote(nsid).get_schema(unquote(fragment))
277-
end
359+
{quote do
360+
unquote(nsid).get_schema(unquote(fragment))
361+
end,
362+
quote do
363+
unquote(nsid).unquote(fragment)()
364+
end}
365+
end)
366+
|> Enum.reduce({[], []}, fn {quoted_schema, quoted_type}, {schemas, types} ->
367+
{[quoted_schema | schemas], [quoted_type | types]}
368+
end)
369+
|> then(fn {schemaa, types} ->
370+
{quote do
371+
{:oneof, unquote(schemaa)}
372+
end,
373+
quote do
374+
unquote(join_with_pipe(types))
375+
end}
376+
|> IO.inspect()
278377
end)
279-
|> then(
280-
&quote do
281-
{:oneof, unquote(&1)}
282-
end
283-
)
284378
end
285379

286380
# TODO: apparently should be a data object, not a primitive?
287381
defp field_to_schema(%{type: "unknown"}, _nsid) do
288-
:any
382+
{:any,
383+
quote do
384+
term()
385+
end}
289386
end
290387

291-
defp field_to_schema(_field_def, _nsid), do: nil
388+
defp field_to_schema(_field_def, _nsid), do: {nil, nil}
292389

293390
defp maybe_default(schema, field) do
294391
if field[:default] != nil,
@@ -306,4 +403,13 @@ defmodule Atex.Lexicon do
306403

307404
defp atomise(x) when is_atom(x), do: x
308405
defp atomise(x) when is_binary(x), do: String.to_atom(x)
406+
407+
defp join_with_pipe(list) when is_list(list) do
408+
[piped] = do_join_with_pipe(list)
409+
piped
410+
end
411+
412+
defp do_join_with_pipe([head]), do: [head]
413+
defp do_join_with_pipe([head | tail]), do: [{:|, [], [head | do_join_with_pipe(tail)]}]
414+
defp do_join_with_pipe([]), do: []
309415
end

lib/atex/lexicon/validators.ex

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ defmodule Atex.Lexicon.Validators do
33

44
@type blob_option() :: {:accept, list(String.t())} | {:max_size, pos_integer()}
55

6-
@type blob_t() ::
6+
@type blob() ::
77
%{
88
"$type": String.t(),
99
ref: %{"$link": String.t()},
@@ -15,6 +15,10 @@ defmodule Atex.Lexicon.Validators do
1515
mimeType: String.t()
1616
}
1717

18+
@type cid_link() :: %{"$link": String.t()}
19+
20+
@type bytes() :: %{"$bytes": binary()}
21+
1822
@spec string(list(Validators.String.option())) :: Peri.custom_def()
1923
def string(options \\ []), do: {:custom, {Validators.String, :validate, [options]}}
2024

0 commit comments

Comments
 (0)