//// Lexicon -> Gleam codegen. Reads `/**/*.json` and writes typed //// modules into ``, importable as `/...`. See //// `codegen/lexicon` (parsing), `codegen/naming` (names/refs), `codegen/emit` //// (source emission). import argv import atproto_codegen/config.{type Config, Config} import atproto_codegen/emit import atproto_codegen/lexicon.{ type Def, type FieldType, type Lexicon, Object, Record, TArray, TRef, Unsupported, } import atproto_codegen/naming import gleam/erlang/application import gleam/io import gleam/json import gleam/list import gleam/option.{None, Some} import gleam/result import gleam/string import simplifile const generated_banner = "//// Generated by codegen from priv/internal.gleam. Do not edit by hand.\n\n" const usage = "usage: gleam run -- [,...]" pub fn main() -> Nil { case argv.load().arguments { [lexicons_dir, out_dir, out_module, prefixes] -> run(Config( lexicons_dir:, out_dir:, out_module:, nsid_prefixes: string.split(prefixes, ","), )) _ -> io.println(usage) } } fn run(cfg: Config) -> Nil { let assert Ok(files) = simplifile.get_files(cfg.lexicons_dir) let lexicons = files |> list.filter(string.ends_with(_, ".json")) |> list.filter_map(fn(path) { case load_lexicon(path) { Ok(lex) -> Ok(lex) Error(reason) -> { io.println(" WARN skipping " <> path <> ": " <> reason) Error(Nil) } } }) report_unresolved_refs(lexicons) let assert Ok(_) = simplifile.create_directory_all(cfg.out_dir) let assert Ok(internal) = simplifile.read(internal_template()) let assert Ok(_) = simplifile.write( to: cfg.out_dir <> "/internal.gleam", contents: generated_banner <> internal, ) list.each(lexicons, fn(lex) { list.each(lex.defs, fn(d) { case d { Unsupported(nsid, name, reason) -> io.println(" skip " <> nsid <> "#" <> name <> " (" <> reason <> ")") _ -> Nil } }) case emit.emit_lexicon(cfg, lex) { Some(source) -> { let path = cfg.out_dir <> "/" <> naming.module_subpath(cfg.nsid_prefixes, lex.nsid) <> ".gleam" let assert Ok(_) = simplifile.create_directory_all(parent_dir(path)) let assert Ok(_) = simplifile.write(to: path, contents: source) io.println(" gen " <> path) } None -> Nil } }) io.println("codegen: done") } // Resolved via the app priv dir so the tool also works installed as a dep. fn internal_template() -> String { let priv = application.priv_directory("atproto_codegen") |> result.unwrap("priv") priv <> "/internal.gleam" } fn load_lexicon(path: String) -> Result(Lexicon, String) { use contents <- result.try( simplifile.read(path) |> result.map_error(fn(e) { "read failed: " <> string.inspect(e) }), ) json.parse(contents, lexicon.lexicon_decoder()) |> result.map_error(fn(e) { "parse failed: " <> string.inspect(e) }) } /// Warn about any `ref` that points at a def we never generate (a different /// lexicon's def, an external NSID, or an unsupported def). Such a ref would /// otherwise emit Gleam that doesn't compile, with no signal from codegen. fn report_unresolved_refs(lexicons: List(Lexicon)) -> Nil { let known = list.flat_map(lexicons, fn(lex) { list.filter_map(lex.defs, fn(d) { case d { Record(nsid, _) -> Ok(nsid <> "#main") Object(nsid, name, _) -> Ok(nsid <> "#" <> name) _ -> Error(Nil) } }) }) let refs = list.flat_map(lexicons, fn(lex) { list.flat_map(lex.defs, fn(d) { list.flat_map(def_properties(d), fn(p) { field_ref_keys(lex.nsid, p.field_type) }) }) }) refs |> list.unique |> list.filter(fn(key) { !list.contains(known, key) }) |> list.each(fn(key) { io.println( " WARN unresolved ref " <> key <> " (referencing def won't compile)", ) }) } fn def_properties(def: Def) -> List(lexicon.Property) { case def { Record(_, props) -> props Object(_, _, props) -> props _ -> [] } } fn field_ref_keys(current: String, ft: FieldType) -> List(String) { case ft { TRef(ref) -> { let r = naming.parse_ref(current, ref) [r.nsid <> "#" <> r.def_name] } TArray(inner) -> field_ref_keys(current, inner) _ -> [] } } fn parent_dir(path: String) -> String { case string.split(path, "/") |> list.reverse { [_, ..rest] -> rest |> list.reverse |> string.join("/") [] -> "." } }