A set of utilities for working with the AT Protocol in Elixir.
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