Skip to main content

cargo_mercury/
lib.rs

1//! Mercury - Rust to PureScript Type Generator
2//!
3//! Mercury automatically generates PureScript type definitions and Argonaut JSON
4//! codecs from Rust types annotated with `#[mercury]`.
5//!
6//! # Overview
7//!
8//! This library provides the core functionality for:
9//! - Scanning Rust source files for `#[mercury]` annotations
10//! - Parsing Rust type definitions using the `syn` crate
11//! - Analyzing and mapping Rust types to PureScript equivalents
12//! - Generating PureScript type definitions and JSON codecs
13//! - Writing organized multi-module output
14//!
15//! # Example
16//!
17//! ```rust,ignore
18//! use mercury::generate;
19//!
20//! // Generate PureScript from annotated Rust types
21//! let result = generate(".")?;
22//! println!("Generated {} types in {} modules", result.type_count, result.module_count);
23//! ```
24
25pub mod analyzer;
26pub mod codec_gen;
27pub mod codegen;
28pub mod error;
29pub mod parser;
30pub mod scanner;
31pub mod serde_attrs;
32pub mod types;
33pub mod writer;
34
35pub use error::{MercuryError, Result};
36
37use std::path::Path;
38
39/// Result of a successful code generation run
40#[derive(Debug, Clone)]
41pub struct GenerationResult {
42    /// Number of types generated
43    pub type_count: usize,
44    /// Number of modules written
45    pub module_count: usize,
46    /// List of generated file paths
47    pub generated_files: Vec<String>,
48}
49
50/// Main entry point for code generation
51///
52/// Scans the workspace for `#[mercury]` annotated types and generates
53/// PureScript type definitions and JSON codecs.
54///
55/// # Arguments
56///
57/// * `workspace_root` - Path to the Cargo workspace root
58///
59/// # Returns
60///
61/// Returns a `GenerationResult` containing statistics about the generation.
62///
63/// # Errors
64///
65/// Returns an error if:
66/// - The workspace cannot be scanned
67/// - Rust source files cannot be parsed
68/// - PureScript code generation fails
69/// - Output files cannot be written
70pub fn generate<P: AsRef<Path>>(workspace_root: P) -> Result<GenerationResult> {
71    let workspace_root = workspace_root.as_ref();
72
73    // Step 1: Scan for annotated files
74    let annotated_files = scanner::scan_workspace(workspace_root)?;
75
76    if annotated_files.is_empty() {
77        return Ok(GenerationResult {
78            type_count: 0,
79            module_count: 0,
80            generated_files: vec![],
81        });
82    }
83
84    // Step 2: Parse each file to extract type definitions
85    let mut all_type_defs = Vec::new();
86    for annotated_file in &annotated_files {
87        let contents = std::fs::read_to_string(&annotated_file.path)?;
88
89        // Make path relative to workspace root to avoid exposing absolute paths
90        let relative_path = annotated_file
91            .path
92            .strip_prefix(workspace_root)
93            .unwrap_or(&annotated_file.path);
94
95        let type_defs = parser::parse_file(relative_path, &contents)?;
96        all_type_defs.extend(type_defs);
97    }
98
99    // Step 3: Group types by source file
100    let modules = group_types_by_module(&all_type_defs, workspace_root);
101
102    // Step 3.5: Build a map of type name -> module name for cross-module imports
103    let type_to_module = build_type_to_module_map(&modules);
104
105    // Step 4: Generate and write each module
106    let output_dir = workspace_root.join("frontend/src/Generated");
107    std::fs::create_dir_all(&output_dir)?;
108
109    let mut generated_files = Vec::new();
110    for (module_name, type_defs) in &modules {
111        let module_code = codegen::generate_module(module_name, type_defs, &type_to_module);
112        let file_name = format!("{}.purs", module_name.replace('.', "/"));
113        let output_path = output_dir.join(&file_name);
114
115        // Create parent directories if needed
116        if let Some(parent) = output_path.parent() {
117            std::fs::create_dir_all(parent)?;
118        }
119
120        writer::write_file(&output_path, &module_code)?;
121        generated_files.push(output_path.display().to_string());
122    }
123
124    Ok(GenerationResult {
125        type_count: all_type_defs.len(),
126        module_count: modules.len(),
127        generated_files,
128    })
129}
130
131/// Group type definitions by their source module
132fn group_types_by_module(
133    type_defs: &[types::TypeDefinition],
134    workspace_root: &Path,
135) -> std::collections::BTreeMap<String, Vec<types::TypeDefinition>> {
136    use std::collections::BTreeMap;
137
138    let mut modules: BTreeMap<String, Vec<types::TypeDefinition>> = BTreeMap::new();
139
140    for type_def in type_defs {
141        let module_name = source_path_to_module_name(&type_def.source_file, workspace_root);
142        modules
143            .entry(module_name)
144            .or_default()
145            .push(type_def.clone());
146    }
147
148    modules
149}
150
151/// Build a map from type name to module name for resolving cross-module imports
152fn build_type_to_module_map(
153    modules: &std::collections::BTreeMap<String, Vec<types::TypeDefinition>>,
154) -> std::collections::HashMap<String, String> {
155    use std::collections::HashMap;
156
157    let mut map = HashMap::new();
158
159    for (module_name, type_defs) in modules {
160        for type_def in type_defs {
161            map.insert(type_def.name.clone(), module_name.clone());
162        }
163    }
164
165    map
166}
167
168/// Convert a Rust source file path to a PureScript module name
169///
170/// Examples:
171/// - `app/backend/src/models.rs` -> `Generated.Models`
172/// - `lib/constitution/src/models/merchant.rs` -> `Generated.Merchant`
173/// - `test-mercury/test.rs` -> `Generated.Test`
174fn source_path_to_module_name(source_path: &Path, workspace_root: &Path) -> String {
175    // Get relative path from workspace root
176    let relative = source_path
177        .strip_prefix(workspace_root)
178        .unwrap_or(source_path);
179
180    // Extract the meaningful part of the path
181    let _path_str = relative.to_string_lossy();
182
183    // Simple heuristic: use the file name without extension as the module name
184    // This can be enhanced later with more sophisticated mapping
185    let file_name = source_path
186        .file_stem()
187        .and_then(|s| s.to_str())
188        .unwrap_or("Unknown");
189
190    // Convert to PascalCase
191    let module_suffix = to_pascal_case(file_name);
192
193    format!("Generated.{}", module_suffix)
194}
195
196/// Convert a string to PascalCase
197fn to_pascal_case(s: &str) -> String {
198    s.split('_')
199        .map(|word| {
200            let mut chars = word.chars();
201            match chars.next() {
202                None => String::new(),
203                Some(first) => first.to_uppercase().chain(chars).collect(),
204            }
205        })
206        .collect()
207}