defmodule Mix.Tasks.Atex.Lexicons do @moduledoc """ Generate Elixir modules from AT Protocol lexicons, which can then be used to validate data at runtime. AT Protocol lexicons are JSON files that define parts of the AT Protocol data model. This task processes these lexicon files and generates corresponding Elixir modules. ## Usage mix atex.lexicons [OPTIONS] [PATHS] ## Arguments - `PATHS` - List of lexicon files to process. Also supports standard glob syntax for reading many lexicons at once. ## Options - `-o`/`--output` - Output directory for generated modules (default: `lib/atproto`) ## Examples Process all JSON files in the lexicons directory: mix atex.lexicons lexicons/**/*.json Process specific lexicon files: mix atex.lexicons lexicons/com/atproto/repo/*.json lexicons/app/bsky/actor/profile.json Generate modules to a custom output directory: mix atex.lexicons lexicons/**/*.json --output lib/my_atproto """ @shortdoc "Generate Elixir modules from AT Protocol lexicons." use Mix.Task require EEx @switches [output: :string] @aliases [o: :output] @template_path Path.expand("../../../priv/templates/lexicon.eex", __DIR__) @impl true def run(args) do {options, globs} = OptionParser.parse!(args, switches: @switches, aliases: @aliases) output = Keyword.get(options, :output, "lib/atproto") paths = Enum.flat_map(globs, &Path.wildcard/1) if length(paths) == 0 do Mix.shell().error("No valid search paths have been provided, aborting.") else Mix.shell().info("Generating modules for lexicons into #{output}") Enum.each(paths, fn path -> Mix.shell().info("- #{path}") generate(path, output) end) end end # TODO: validate schema? defp generate(input, output) do lexicon = input |> File.read!() |> JSON.decode!() if not is_binary(lexicon["id"]) do raise ArgumentError, message: "Malformed lexicon: does not have an `id` field." end code = lexicon |> template() |> Code.format_string!() |> Enum.join("") file_path = lexicon["id"] |> String.split(".") |> Enum.join("/") |> then(&(&1 <> ".ex")) |> then(&Path.join(output, &1)) file_path |> Path.dirname() |> File.mkdir_p!() File.write!(file_path, code) end EEx.function_from_file(:defp, :template, @template_path, [:lexicon]) end