1use std::collections::HashSet;
2
3use crate::codegen::traits::file_writer::FileInfo;
4use crate::generators::request_inputs::{
5 RequestInputField, RequestInputFieldKind, RequestInputModel, RequestInputPlan,
6};
7use crate::ir::types::{
8 IrEnum, IrEnumValueType, IrIntersection, IrObject, IrSchema, IrSchemaKind, IrSpec,
9 IrTaggedUnion, IrTypeExpr, IrUnion, TaggingStyle,
10};
11use heck::{ToLowerCamelCase, ToPascalCase};
12use sigil_stitch::lang::java::Java;
13use sigil_stitch::prelude::*;
14
15use super::util::{
16 build_java_getter, escape_java_string, java_boxed_type_str, java_field_name, java_type_str,
17 type_uses_list, type_uses_map, unique_name,
18};
19
20const RENDER_WIDTH: usize = 100;
21
22pub fn generate_model_files(
23 ir: &IrSpec,
24 package_name: &str,
25 header: &str,
26 request_inputs: &RequestInputPlan,
27) -> Result<Vec<FileInfo>, String> {
28 let mut files = Vec::new();
29 for (_name, schema) in &ir.schemas {
30 let body = emit_model_body(schema, package_name).ok_or_else(|| {
31 format!(
32 "unsupported schema kind for {}: {:?}",
33 schema.name, schema.kind
34 )
35 })?;
36 let class_name = schema.name.to_pascal_case();
37 let filename = format!("{class_name}.java");
38 let mut content = String::with_capacity(header.len() + body.len());
39 content.push_str(header);
40 content.push_str(&body);
41 files.push(FileInfo::model(filename, content));
42 }
43 for model in request_inputs.models() {
44 files.push(request_input_model_file(model, package_name, header));
45 }
46 Ok(files)
47}
48
49fn request_input_model_file(
50 model: &RequestInputModel,
51 package_name: &str,
52 header: &str,
53) -> FileInfo {
54 let class_name = model.name.to_pascal_case();
55 let needs_upload = model.fields.iter().any(RequestInputField::is_upload);
56 let mut content = String::new();
57 content.push_str(header);
58 content.push_str(&format!("package {package_name}.models;\n\n"));
59 if needs_upload {
60 content.push_str(&format!("import {package_name}.runtime.UploadFile;\n\n"));
61 }
62 content.push_str(&format!("public final class {class_name} {{\n"));
63 for field in &model.fields {
64 content.push_str(&format!(
65 " private final {} {};\n",
66 request_input_java_type(field),
67 java_field_name(&field.wire_name)
68 ));
69 }
70 content.push('\n');
71 content.push_str(&format!(" public {class_name}("));
72 let params = model
73 .fields
74 .iter()
75 .map(|field| {
76 format!(
77 "{} {}",
78 request_input_java_type(field),
79 java_field_name(&field.wire_name)
80 )
81 })
82 .collect::<Vec<_>>()
83 .join(", ");
84 content.push_str(¶ms);
85 content.push_str(") {\n");
86 for field in &model.fields {
87 let name = java_field_name(&field.wire_name);
88 content.push_str(&format!(" this.{name} = {name};\n"));
89 }
90 content.push_str(" }\n\n");
91 for field in &model.fields {
92 let field_name = java_field_name(&field.wire_name);
93 let getter = format!("get{}", field.wire_name.to_pascal_case());
94 content.push_str(&format!(
95 " public {} {}() {{\n return {};\n }}\n\n",
96 request_input_java_type(field),
97 getter,
98 field_name
99 ));
100 }
101 content.push_str("}\n");
102
103 FileInfo::model(format!("{class_name}.java"), content)
104}
105
106fn request_input_java_type(field: &RequestInputField) -> String {
107 match field.kind {
108 RequestInputFieldKind::UploadFile { .. } => "UploadFile".to_string(),
109 RequestInputFieldKind::SchemaValue => java_boxed_type_str(&field.type_expr),
110 }
111}
112
113fn emit_model_body(schema: &IrSchema, package_name: &str) -> Option<String> {
114 let file_spec = match &schema.kind {
115 IrSchemaKind::Object(obj) => emit_object(schema, obj, package_name),
116 IrSchemaKind::Enum(en) => emit_enum(schema, en, package_name),
117 IrSchemaKind::Alias(expr) => emit_alias(schema, expr, package_name),
118 IrSchemaKind::Union(u) => emit_union(schema, u, package_name),
119 IrSchemaKind::Intersection(i) => emit_intersection(schema, i, package_name),
120 IrSchemaKind::TaggedUnion(tu) => emit_tagged_union(schema, tu, package_name),
121 }?;
122 file_spec.render(RENDER_WIDTH).ok()
123}
124
125fn package_header(package_name: &str) -> CodeBlock {
126 sigil_quote!(Java {
127 package $L(format!("{package_name}.models"));
128 })
129 .expect("package header builds")
130}
131
132fn emit_object(schema: &IrSchema, obj: &IrObject, package_name: &str) -> Option<FileSpec> {
137 let name = schema.name.to_pascal_case();
138
139 let mut file =
140 FileSpec::builder_with("model.java", Java::new()).header(package_header(package_name));
141
142 let needs_serialized_name = obj.properties.iter().any(|(json_name, prop)| {
143 let field_name = java_field_name(&prop.name);
144 *json_name != field_name
145 });
146 if needs_serialized_name {
147 file = file.add_import(ImportSpec::named(
148 "com.google.gson.annotations",
149 "SerializedName",
150 ));
151 }
152
153 let needs_list = obj
154 .properties
155 .iter()
156 .any(|(_, prop)| type_uses_list(&prop.type_expr));
157 if needs_list {
158 file = file.add_import(ImportSpec::named("java.util", "List"));
159 }
160
161 let needs_map = obj
162 .properties
163 .iter()
164 .any(|(_, prop)| type_uses_map(&prop.type_expr));
165 if needs_map {
166 file = file.add_import(ImportSpec::named("java.util", "Map"));
167 }
168
169 let mut tb = TypeSpec::builder(&name, TypeKind::Struct).visibility(Visibility::Public);
170 if let Some(doc) = &schema.description {
171 tb = tb.doc(doc);
172 }
173
174 for (json_name, prop) in &obj.properties {
176 let field_name = java_field_name(&prop.name);
177 let required = prop.required && !prop.nullable;
178 let type_str = if required {
179 java_type_str(&prop.type_expr)
180 } else {
181 java_boxed_type_str(&prop.type_expr)
182 };
183
184 let annotation = if *json_name != field_name {
185 Some(format!(
186 "@SerializedName(\"{}\")",
187 escape_java_string(json_name)
188 ))
189 } else {
190 None
191 };
192
193 let mut fb = FieldSpec::builder(&field_name, TypeName::primitive(&type_str))
194 .visibility(Visibility::Private);
195 if let Some(ann) = annotation {
196 fb = fb.annotation(CodeBlock::of(&ann, ()).expect("annotation"));
197 }
198 tb = tb.add_field(fb.build().expect("field"));
199 }
200
201 let mut ctor = FunSpec::builder(&name);
203 ctor = ctor.visibility(Visibility::Public);
204 for (_json_name, prop) in &obj.properties {
205 let field_name = java_field_name(&prop.name);
206 let required = prop.required && !prop.nullable;
207 let type_str = if required {
208 java_type_str(&prop.type_expr)
209 } else {
210 java_boxed_type_str(&prop.type_expr)
211 };
212 ctor = ctor.add_param(
213 ParameterSpec::new(&format!("{type_str} {field_name}"), TypeName::primitive(""))
214 .expect("ctor param"),
215 );
216 }
217 let assignment_fields: Vec<String> = obj
218 .properties
219 .iter()
220 .map(|(_json_name, prop)| java_field_name(&prop.name))
221 .collect();
222 let ctor_body = sigil_quote!(Java {
223 $for(field_name in &assignment_fields) {
224 this.$L(field_name.as_str()) = $L(field_name.as_str());
225 }
226 })
227 .expect("ctor body");
228 ctor = ctor.body(ctor_body);
229 tb = tb.add_method(ctor.build().expect("constructor"));
230
231 for (_json_name, prop) in &obj.properties {
233 let field_name = java_field_name(&prop.name);
234 let required = prop.required && !prop.nullable;
235 let type_str = if required {
236 java_type_str(&prop.type_expr)
237 } else {
238 java_boxed_type_str(&prop.type_expr)
239 };
240 let getter_name = format!("get{}", prop.name.to_pascal_case());
241 tb = tb.add_method(build_java_getter(&getter_name, &type_str, &field_name));
242 }
243
244 file = file.add_type(tb.build().ok()?);
245 file.build().ok()
246}
247
248fn emit_enum(schema: &IrSchema, en: &IrEnum, package_name: &str) -> Option<FileSpec> {
253 let name = schema.name.to_pascal_case();
254
255 let mut file =
256 FileSpec::builder_with("model.java", Java::new()).header(package_header(package_name));
257
258 if en.value_type == IrEnumValueType::Mixed {
259 return emit_comment_class(&name, "Object", schema.description.as_deref(), package_name);
260 }
261
262 file = file.add_import(ImportSpec::named(
263 "com.google.gson.annotations",
264 "SerializedName",
265 ));
266
267 let base_type = match en.value_type {
268 IrEnumValueType::String => "String",
269 IrEnumValueType::Integer => "int",
270 IrEnumValueType::Number => "double",
271 IrEnumValueType::Mixed => unreachable!(),
272 };
273
274 let mut tb = TypeSpec::builder(&name, TypeKind::Enum).visibility(Visibility::Public);
275 if let Some(doc) = &schema.description {
276 tb = tb.doc(doc);
277 }
278
279 for v in &en.values {
280 let (variant_name, literal, raw_value) = match en.value_type {
281 IrEnumValueType::String => {
282 let s = v.value.as_str()?;
283 (
284 enum_variant_name(s),
285 format!("\"{}\"", escape_java_string(s)),
286 s.to_string(),
287 )
288 }
289 IrEnumValueType::Integer | IrEnumValueType::Number => {
290 let n = v.value.as_number()?;
291 let variant = format!("N{}", n.to_string().replace(['-', '.'], "_"));
292 let s = n.to_string();
293 (variant, s.clone(), s)
294 }
295 IrEnumValueType::Mixed => unreachable!(),
296 };
297
298 let variant_name = if variant_name.is_empty() {
299 "UNKNOWN".to_string()
300 } else {
301 variant_name
302 };
303
304 let mut ev = EnumVariantSpec::builder(&variant_name);
305 ev = ev.annotation(
306 CodeBlock::of(
307 &format!("@SerializedName(\"{}\")", escape_java_string(&raw_value)),
308 (),
309 )
310 .expect("annotation"),
311 );
312 ev = ev.value(CodeBlock::of(&literal, ()).expect("literal"));
313 tb = tb.add_variant(ev.build().expect("enum variant"));
314 }
315
316 tb = tb.add_field(
318 FieldSpec::builder("value", TypeName::primitive(base_type))
319 .visibility(Visibility::Private)
320 .is_readonly()
321 .build()
322 .expect("value field"),
323 );
324
325 let mut ctor = FunSpec::builder(&name);
326 ctor = ctor.add_param(
327 ParameterSpec::new("value", TypeName::primitive(base_type)).expect("ctor param"),
328 );
329 let ctor_body = sigil_quote!(Java {
330 this.value = value;
331 })
332 .expect("ctor body");
333 ctor = ctor.body(ctor_body);
334 tb = tb.add_method(ctor.build().expect("enum ctor"));
335
336 tb = tb.add_method(build_java_getter("getValue", base_type, "value"));
337
338 file = file.add_type(tb.build().ok()?);
339 file.build().ok()
340}
341
342fn emit_alias(schema: &IrSchema, expr: &IrTypeExpr, package_name: &str) -> Option<FileSpec> {
347 let name = schema.name.to_pascal_case();
348 let rhs = java_type_str(expr);
349 emit_comment_class(&name, &rhs, schema.description.as_deref(), package_name)
350}
351
352fn emit_union(schema: &IrSchema, _union: &IrUnion, package_name: &str) -> Option<FileSpec> {
357 let name = schema.name.to_pascal_case();
358 emit_comment_class(&name, "Object", schema.description.as_deref(), package_name)
359}
360
361fn emit_intersection(
366 schema: &IrSchema,
367 inter: &IrIntersection,
368 package_name: &str,
369) -> Option<FileSpec> {
370 let name = schema.name.to_pascal_case();
371 let mut file =
372 FileSpec::builder_with("model.java", Java::new()).header(package_header(package_name));
373
374 let mut tb = TypeSpec::builder(&name, TypeKind::Struct).visibility(Visibility::Public);
375 if let Some(doc) = &schema.description {
376 tb = tb.doc(doc);
377 }
378
379 let mut used_names: HashSet<String> = HashSet::new();
380 let member_bindings: Vec<(String, String)> = inter
381 .members
382 .iter()
383 .map(|member| {
384 let member_type = java_type_str(member);
385 let field_name = unique_name(&member_type.to_lower_camel_case(), &mut used_names);
386 (member_type, field_name)
387 })
388 .collect();
389
390 for (member_type, field_name) in &member_bindings {
391 tb = tb.add_field(
392 FieldSpec::builder(field_name, TypeName::primitive(member_type))
393 .visibility(Visibility::Private)
394 .build()
395 .expect("field"),
396 );
397 }
398
399 let mut ctor = FunSpec::builder(&name);
401 ctor = ctor.visibility(Visibility::Public);
402 for (member_type, field_name) in &member_bindings {
403 ctor = ctor.add_param(
404 ParameterSpec::new(
405 &format!("{member_type} {field_name}"),
406 TypeName::primitive(""),
407 )
408 .expect("param"),
409 );
410 }
411 let assignment_fields: Vec<String> = member_bindings
412 .iter()
413 .map(|(_member_type, field_name)| field_name.clone())
414 .collect();
415 let ctor_body = sigil_quote!(Java {
416 $for(field_name in &assignment_fields) {
417 this.$L(field_name.as_str()) = $L(field_name.as_str());
418 }
419 })
420 .expect("ctor body");
421 ctor = ctor.body(ctor_body);
422 tb = tb.add_method(ctor.build().expect("constructor"));
423
424 for (member_type, field_name) in &member_bindings {
426 let getter_name = format!("get{}", field_name.to_pascal_case());
427 tb = tb.add_method(build_java_getter(&getter_name, member_type, field_name));
428 }
429
430 file = file.add_type(tb.build().ok()?);
431 file.build().ok()
432}
433
434fn emit_tagged_union(
439 schema: &IrSchema,
440 tu: &IrTaggedUnion,
441 package_name: &str,
442) -> Option<FileSpec> {
443 let name = schema.name.to_pascal_case();
444
445 if tu.variants.is_empty() {
446 return emit_comment_class(&name, "Object", schema.description.as_deref(), package_name);
447 }
448
449 let hint = match &tu.tagging {
450 TaggingStyle::Internal => {
451 format!("Discriminator: {} (internal).", tu.discriminator_field)
452 }
453 TaggingStyle::Adjacent { content_field } => format!(
454 "Discriminator: {} / content: {} (adjacent).",
455 tu.discriminator_field, content_field
456 ),
457 TaggingStyle::External => "Discriminator: variant key (external).".to_string(),
458 };
459 let doc = match &schema.description {
460 Some(desc) => format!("{desc}\n\n{hint}"),
461 None => hint,
462 };
463
464 emit_comment_class(&name, "Object", Some(&doc), package_name)
465}
466
467fn emit_comment_class(
472 name: &str,
473 underlying_type: &str,
474 doc: Option<&str>,
475 package_name: &str,
476) -> Option<FileSpec> {
477 let mut file =
478 FileSpec::builder_with("model.java", Java::new()).header(package_header(package_name));
479
480 let mut tb = TypeSpec::builder(name, TypeKind::Struct).visibility(Visibility::Public);
481 if let Some(d) = doc {
482 tb = tb.doc(d);
483 }
484
485 tb = tb.add_field(
487 FieldSpec::builder("value", TypeName::primitive(underlying_type))
488 .visibility(Visibility::Private)
489 .build()
490 .expect("value field"),
491 );
492
493 let mut ctor = FunSpec::builder(name);
495 ctor = ctor.visibility(Visibility::Public);
496 ctor = ctor.add_param(
497 ParameterSpec::new(&format!("{underlying_type} value"), TypeName::primitive(""))
498 .expect("param"),
499 );
500 let body = sigil_quote!(Java {
501 this.value = value;
502 })
503 .expect("ctor body");
504 ctor = ctor.body(body);
505 tb = tb.add_method(ctor.build().expect("constructor"));
506
507 tb = tb.add_method(build_java_getter("getValue", underlying_type, "value"));
509
510 file = file.add_type(tb.build().ok()?);
511 file.build().ok()
512}
513
514fn enum_variant_name(s: &str) -> String {
515 let upper: String = s
516 .chars()
517 .map(|c| {
518 if c.is_alphanumeric() {
519 c.to_ascii_uppercase()
520 } else {
521 '_'
522 }
523 })
524 .collect();
525 if upper.is_empty() {
526 "UNKNOWN".to_string()
527 } else if upper.chars().next().unwrap().is_ascii_digit() {
528 format!("N{upper}")
529 } else {
530 upper
531 }
532}