Skip to main content

intent_codegen/
java.rs

1//! Java skeleton code generator.
2//!
3//! Generates Java 16+ records for entities, enums for union types,
4//! and static methods for actions inside a module-level final class.
5
6use intent_parser::ast;
7
8use crate::types::map_type;
9use crate::{Language, doc_text, format_ensures_item, format_expr, to_camel_case};
10
11/// Java reserved keywords that cannot be used as identifiers.
12const JAVA_KEYWORDS: &[&str] = &[
13    "abstract",
14    "assert",
15    "boolean",
16    "break",
17    "byte",
18    "case",
19    "catch",
20    "char",
21    "class",
22    "const",
23    "continue",
24    "default",
25    "do",
26    "double",
27    "else",
28    "enum",
29    "extends",
30    "final",
31    "finally",
32    "float",
33    "for",
34    "goto",
35    "if",
36    "implements",
37    "import",
38    "instanceof",
39    "int",
40    "interface",
41    "long",
42    "native",
43    "new",
44    "package",
45    "private",
46    "protected",
47    "public",
48    "return",
49    "short",
50    "static",
51    "strictfp",
52    "super",
53    "switch",
54    "synchronized",
55    "this",
56    "throw",
57    "throws",
58    "transient",
59    "try",
60    "void",
61    "volatile",
62    "while",
63];
64
65/// Escape a Java identifier if it collides with a reserved keyword.
66fn safe_ident(name: &str) -> String {
67    let camel = to_camel_case(name);
68    if JAVA_KEYWORDS.contains(&camel.as_str()) {
69        format!("{camel}_")
70    } else {
71        camel
72    }
73}
74
75/// Generate Java skeleton code from a parsed intent file.
76pub fn generate(file: &ast::File) -> String {
77    let lang = Language::Java;
78    let mut out = String::new();
79
80    // Header
81    out.push_str(&format!(
82        "// Generated from {}.intent. DO NOT EDIT.\n",
83        file.module.name
84    ));
85    if let Some(doc) = &file.doc {
86        out.push('\n');
87        for line in &doc.lines {
88            out.push_str(&format!("// {line}\n"));
89        }
90    }
91    out.push('\n');
92
93    // Package declaration (lowercase module name)
94    let pkg_name = file.module.name.to_lowercase();
95    out.push_str(&format!("package {pkg_name};\n\n"));
96
97    // Imports
98    let imports = generate_imports(file);
99    if !imports.is_empty() {
100        out.push_str(&imports);
101        out.push('\n');
102    }
103
104    // Module class wrapper
105    out.push_str(&format!("public final class {} {{\n\n", file.module.name));
106    out.push_str(&format!(
107        "    private {}() {{}} // Prevent instantiation\n\n",
108        file.module.name
109    ));
110
111    for item in &file.items {
112        match item {
113            ast::TopLevelItem::Entity(e) => generate_entity(&mut out, e, &lang),
114            ast::TopLevelItem::Action(a) => generate_action(&mut out, a, &lang),
115            ast::TopLevelItem::Invariant(inv) => generate_invariant(&mut out, inv),
116            ast::TopLevelItem::EdgeCases(ec) => generate_edge_cases(&mut out, ec),
117            ast::TopLevelItem::Test(_) => {}
118        }
119    }
120
121    // Close module class
122    out.push_str("}\n");
123    out
124}
125
126fn generate_imports(file: &ast::File) -> String {
127    let source = collect_type_names(file);
128    let has_action = file
129        .items
130        .iter()
131        .any(|item| matches!(item, ast::TopLevelItem::Action(_)));
132
133    let mut imports = Vec::new();
134
135    if source.contains("UUID") {
136        imports.push("import java.util.UUID;");
137    }
138    if source.contains("Decimal") {
139        imports.push("import java.math.BigDecimal;");
140    }
141    if source.contains("DateTime") {
142        imports.push("import java.time.Instant;");
143    }
144    if source.contains("List<") {
145        imports.push("import java.util.List;");
146    }
147    if source.contains("Set<") {
148        imports.push("import java.util.Set;");
149    }
150    if source.contains("Map<") {
151        imports.push("import java.util.Map;");
152    }
153    if has_action {
154        // UnsupportedOperationException is in java.lang, no import needed
155    }
156
157    if imports.is_empty() {
158        return String::new();
159    }
160
161    imports.join("\n") + "\n"
162}
163
164/// Collect all type names as a single string for import detection.
165fn collect_type_names(file: &ast::File) -> String {
166    let mut names = String::new();
167    for item in &file.items {
168        match item {
169            ast::TopLevelItem::Entity(e) => {
170                for f in &e.fields {
171                    collect_type_name(&f.ty, &mut names);
172                }
173            }
174            ast::TopLevelItem::Action(a) => {
175                for p in &a.params {
176                    collect_type_name(&p.ty, &mut names);
177                }
178            }
179            _ => {}
180        }
181    }
182    names
183}
184
185fn collect_type_name(ty: &ast::TypeExpr, out: &mut String) {
186    match &ty.ty {
187        ast::TypeKind::Simple(n) => {
188            out.push_str(n);
189            out.push(' ');
190        }
191        ast::TypeKind::Parameterized { name, .. } => {
192            out.push_str(name);
193            out.push(' ');
194        }
195        ast::TypeKind::List(inner) => {
196            out.push_str("List<");
197            collect_type_name(inner, out);
198        }
199        ast::TypeKind::Set(inner) => {
200            out.push_str("Set<");
201            collect_type_name(inner, out);
202        }
203        ast::TypeKind::Map(k, v) => {
204            out.push_str("Map<");
205            collect_type_name(k, out);
206            collect_type_name(v, out);
207        }
208        ast::TypeKind::Union(_) => {}
209    }
210}
211
212fn generate_entity(out: &mut String, entity: &ast::EntityDecl, lang: &Language) {
213    // Emit enum types for union-typed fields
214    for field in &entity.fields {
215        if let ast::TypeKind::Union(variants) = &field.ty.ty {
216            let enum_name = format!("{}{}", entity.name, capitalize(&field.name));
217            generate_union_enum(out, &enum_name, variants);
218        }
219    }
220
221    // Javadoc
222    if let Some(doc) = &entity.doc {
223        out.push_str("    /**\n");
224        for line in doc_text(doc).lines() {
225            out.push_str(&format!("     * {line}\n"));
226        }
227        out.push_str("     */\n");
228    }
229
230    // Record declaration
231    let params: Vec<String> = entity
232        .fields
233        .iter()
234        .map(|f| {
235            let ty = if let ast::TypeKind::Union(_) = &f.ty.ty {
236                format!("{}{}", entity.name, capitalize(&f.name))
237            } else {
238                map_type(&f.ty, lang)
239            };
240            format!("{ty} {}", safe_ident(&f.name))
241        })
242        .collect();
243
244    out.push_str(&format!("    public record {}(\n", entity.name));
245    for (i, param) in params.iter().enumerate() {
246        let comma = if i < params.len() - 1 { "," } else { "" };
247        out.push_str(&format!("        {param}{comma}\n"));
248    }
249    out.push_str("    ) {}\n\n");
250}
251
252fn generate_union_enum(out: &mut String, name: &str, variants: &[ast::TypeKind]) {
253    let names: Vec<&str> = variants
254        .iter()
255        .filter_map(|v| match v {
256            ast::TypeKind::Simple(n) => Some(n.as_str()),
257            _ => None,
258        })
259        .collect();
260
261    out.push_str(&format!("    public enum {name} {{\n"));
262    for (i, n) in names.iter().enumerate() {
263        let comma = if i < names.len() - 1 { "," } else { "" };
264        out.push_str(&format!("        {n}{comma}\n"));
265    }
266    out.push_str("    }\n\n");
267}
268
269fn generate_action(out: &mut String, action: &ast::ActionDecl, lang: &Language) {
270    let fn_name = to_camel_case(&action.name);
271
272    // Javadoc
273    out.push_str("    /**\n");
274    if let Some(doc) = &action.doc {
275        for line in doc_text(doc).lines() {
276            out.push_str(&format!("     * {line}\n"));
277        }
278    }
279
280    // Requires
281    if let Some(req) = &action.requires {
282        out.push_str("     *\n     * <p>Requires:\n     * <ul>\n");
283        for cond in &req.conditions {
284            out.push_str(&format!(
285                "     *   <li>{{@code {}}}</li>\n",
286                format_expr(cond)
287            ));
288        }
289        out.push_str("     * </ul>\n");
290    }
291
292    // Ensures
293    if let Some(ens) = &action.ensures {
294        out.push_str("     *\n     * <p>Ensures:\n     * <ul>\n");
295        for item in &ens.items {
296            out.push_str(&format!(
297                "     *   <li>{{@code {}}}</li>\n",
298                format_ensures_item(item)
299            ));
300        }
301        out.push_str("     * </ul>\n");
302    }
303
304    // Properties
305    if let Some(props) = &action.properties {
306        out.push_str("     *\n     * <p>Properties:\n     * <ul>\n");
307        for entry in &props.entries {
308            out.push_str(&format!(
309                "     *   <li>{}: {}</li>\n",
310                entry.key,
311                crate::format_prop_value(&entry.value)
312            ));
313        }
314        out.push_str("     * </ul>\n");
315    }
316
317    out.push_str("     */\n");
318
319    // Method signature
320    let params: Vec<String> = action
321        .params
322        .iter()
323        .map(|p| {
324            let ty = map_type(&p.ty, lang);
325            format!("{ty} {}", safe_ident(&p.name))
326        })
327        .collect();
328
329    out.push_str(&format!(
330        "    public static void {fn_name}({}) {{\n",
331        params.join(", ")
332    ));
333    out.push_str(&format!(
334        "        throw new UnsupportedOperationException(\"TODO: implement {fn_name}\");\n"
335    ));
336    out.push_str("    }\n\n");
337}
338
339fn generate_invariant(out: &mut String, inv: &ast::InvariantDecl) {
340    out.push_str(&format!("    // Invariant: {}\n", inv.name));
341    if let Some(doc) = &inv.doc {
342        for line in doc_text(doc).lines() {
343            out.push_str(&format!("    // {line}\n"));
344        }
345    }
346    out.push_str(&format!("    // {}\n\n", format_expr(&inv.body)));
347}
348
349fn generate_edge_cases(out: &mut String, ec: &ast::EdgeCasesDecl) {
350    out.push_str("    // Edge cases:\n");
351    for rule in &ec.rules {
352        out.push_str(&format!(
353            "    // when {} => {}()\n",
354            format_expr(&rule.condition),
355            rule.action.name,
356        ));
357    }
358    out.push('\n');
359}
360
361fn capitalize(s: &str) -> String {
362    let mut chars = s.chars();
363    match chars.next() {
364        None => String::new(),
365        Some(c) => c.to_uppercase().collect::<String>() + chars.as_str(),
366    }
367}