1use intent_parser::ast;
7
8use crate::types::map_type;
9use crate::{Language, doc_text, format_ensures_item, format_expr, to_camel_case};
10
11const 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
65fn 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
75pub fn generate(file: &ast::File) -> String {
77 let lang = Language::Java;
78 let mut out = String::new();
79
80 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 let pkg_name = file.module.name.to_lowercase();
95 out.push_str(&format!("package {pkg_name};\n\n"));
96
97 let imports = generate_imports(file);
99 if !imports.is_empty() {
100 out.push_str(&imports);
101 out.push('\n');
102 }
103
104 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 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 }
156
157 if imports.is_empty() {
158 return String::new();
159 }
160
161 imports.join("\n") + "\n"
162}
163
164fn 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 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 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 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 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 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 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 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 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}