Skip to main content

elm_client_gen_cli/
lib.rs

1//! Core of the reference CLI, extracted from `main.rs` so integration
2//! tests can drive it with fixture types instead of having to link a
3//! separate binary that carries them.
4//!
5//! `main.rs` collects types from `registered_types()` and calls
6//! [`run_codegen`]; tests construct their own `&[ElmTypeInfo]` and call
7//! it directly with a tempdir as output.
8
9use std::fmt::Write as _;
10use std::path::{Path, PathBuf};
11
12use anyhow::{Context, Result};
13use elm_client_gen_builder::{
14    build_merged_module, group_by_module, DefaultStrategy, MaybeEncoderRef, NameMap, TypeOverrides,
15};
16use elm_client_gen_core::ElmTypeInfo;
17
18/// Everything the CLI's request-handling layer needs to know.
19pub struct CodegenOptions<'a> {
20    /// Output directory for generated `.elm` files. Unused when
21    /// `dry_run` is `true`.
22    pub output: PathBuf,
23    /// Filter to specific Elm type names. Empty means no filter.
24    pub filter_names: &'a [String],
25    /// If `true`, return the rendered modules as a string instead of
26    /// writing to disk.
27    pub dry_run: bool,
28}
29
30/// Outcome of a codegen run.
31#[derive(Debug)]
32pub enum CodegenOutcome {
33    /// Files were written to disk. Reports how many modules and the
34    /// output root for status-line rendering.
35    Wrote { module_count: usize, root: PathBuf },
36    /// Dry run: the rendered modules concatenated in the same format
37    /// `main.rs` prints to stdout.
38    DryRun(String),
39}
40
41/// Run codegen over an explicit list of types. Callers that want the
42/// default "all registered types" behavior should pass
43/// `registered_types()` themselves.
44pub fn run_codegen(types: Vec<ElmTypeInfo>, options: CodegenOptions<'_>) -> Result<CodegenOutcome> {
45    let overrides = TypeOverrides::new();
46    let types: Vec<ElmTypeInfo> = types.into_iter().map(|t| overrides.apply(t)).collect();
47    let mut types = types;
48    types.sort_by(|a, b| {
49        a.module_path
50            .cmp(&b.module_path)
51            .then_with(|| a.type_name.cmp(b.type_name))
52    });
53
54    let names = NameMap::from_types(&types);
55
56    let targets: Vec<ElmTypeInfo> = if options.filter_names.is_empty() {
57        types
58    } else {
59        types
60            .into_iter()
61            .filter(|t| options.filter_names.iter().any(|n| n == t.type_name))
62            .collect()
63    };
64
65    if targets.is_empty() {
66        anyhow::bail!("No types matched. Did you `use my_schema_crate as _;` in main.rs?");
67    }
68
69    let strategy = DefaultStrategy;
70    let maybe = MaybeEncoderRef::new(vec!["Json", "Encode", "Extra"], "maybe");
71    let groups = group_by_module(&targets);
72
73    if options.dry_run {
74        let mut buffer = String::new();
75        for (module_path, group) in &groups {
76            let module = build_merged_module(module_path, group, &names, &strategy, &maybe);
77            writeln!(
78                &mut buffer,
79                "-- {}.elm --\n{}\n",
80                module_path.join("."),
81                elm_ast::pretty_print(&module)
82            )
83            .expect("writing to String can't fail");
84        }
85        return Ok(CodegenOutcome::DryRun(buffer));
86    }
87
88    for (module_path, group) in &groups {
89        let module = build_merged_module(module_path, group, &names, &strategy, &maybe);
90        write_module(&options.output, module_path, &module)?;
91    }
92    Ok(CodegenOutcome::Wrote {
93        module_count: groups.len(),
94        root: options.output,
95    })
96}
97
98fn write_module(
99    output_dir: &Path,
100    module_path: &[&str],
101    module: &elm_ast::file::ElmModule,
102) -> Result<()> {
103    let mut file_path = output_dir.to_path_buf();
104    for segment in module_path {
105        file_path.push(segment);
106    }
107    file_path.set_extension("elm");
108
109    let parent = file_path
110        .parent()
111        .with_context(|| format!("no parent directory for {}", file_path.display()))?;
112    std::fs::create_dir_all(parent)
113        .with_context(|| format!("creating directory {}", parent.display()))?;
114    std::fs::write(&file_path, elm_ast::pretty_print(module))
115        .with_context(|| format!("writing {}", file_path.display()))?;
116    Ok(())
117}