1defmodule Mix.Tasks.Atex.Lexicons do
2 @moduledoc """
3 Generate Elixir modules from AT Protocol lexicons, which can then be used to
4 validate data at runtime.
5
6 AT Protocol lexicons are JSON files that define parts of the AT Protocol data
7 model. This task processes these lexicon files and generates corresponding
8 Elixir modules.
9
10 ## Usage
11
12 mix atex.lexicons [OPTIONS] [PATHS]
13
14 ## Arguments
15
16 - `PATHS` - List of lexicon files to process. Also supports standard glob
17 syntax for reading many lexicons at once.
18
19 ## Options
20
21 - `-o`/`--output` - Output directory for generated modules (default:
22 `lib/atproto`)
23
24 ## Examples
25
26 Process all JSON files in the lexicons directory:
27
28 mix atex.lexicons lexicons/**/*.json
29
30 Process specific lexicon files:
31
32 mix atex.lexicons lexicons/com/atproto/repo/*.json lexicons/app/bsky/actor/profile.json
33
34 Generate modules to a custom output directory:
35
36 mix atex.lexicons lexicons/**/*.json --output lib/my_atproto
37 """
38 @shortdoc "Generate Elixir modules from AT Protocol lexicons."
39
40 use Mix.Task
41 require EEx
42
43 @switches [output: :string]
44 @aliases [o: :output]
45 @template_path Path.expand("../../../priv/templates/lexicon.eex", __DIR__)
46
47 @impl true
48 def run(args) do
49 {options, globs} = OptionParser.parse!(args, switches: @switches, aliases: @aliases)
50
51 output = Keyword.get(options, :output, "lib/atproto")
52 paths = Enum.flat_map(globs, &Path.wildcard/1)
53
54 if length(paths) == 0 do
55 Mix.shell().error("No valid search paths have been provided, aborting.")
56 else
57 Mix.shell().info("Generating modules for lexicons into #{output}")
58
59 Enum.each(paths, fn path ->
60 Mix.shell().info("- #{path}")
61 generate(path, output)
62 end)
63 end
64 end
65
66 # TODO: validate schema?
67 defp generate(input, output) do
68 lexicon =
69 input
70 |> File.read!()
71 |> JSON.decode!()
72
73 if not is_binary(lexicon["id"]) do
74 raise ArgumentError, message: "Malformed lexicon: does not have an `id` field."
75 end
76
77 code = lexicon |> template() |> Code.format_string!() |> Enum.join("")
78
79 file_path =
80 lexicon["id"]
81 |> String.split(".")
82 |> Enum.join("/")
83 |> then(&(&1 <> ".ex"))
84 |> then(&Path.join(output, &1))
85
86 file_path
87 |> Path.dirname()
88 |> File.mkdir_p!()
89
90 File.write!(file_path, code)
91 end
92
93 EEx.function_from_file(:defp, :template, @template_path, [:lexicon])
94end