1use std::collections::{BTreeMap, HashSet};
2
3use crate::codegen::traits::file_writer::FileInfo;
4use crate::generators::multipart::{MultipartValueEncoding, multipart_parts_for_request_body};
5use crate::generators::request_inputs::{RequestInputPlan, request_input_for_operation};
6use crate::ir::types::{
7 IrOperation, IrParameter, IrRequestBody, IrResponse, IrSpec, IrTypeExpr, ParameterLocation,
8};
9use heck::{ToLowerCamelCase, ToPascalCase};
10use sigil_stitch::lang::java::Java;
11use sigil_stitch::prelude::*;
12
13use super::util::{
14 build_java_getter, java_boxed_type_str, java_field_name, java_ident, java_type_str,
15 render_value_as_string, sanitize_operation_id, unique_name,
16};
17
18const RENDER_WIDTH: usize = 100;
19
20pub fn generate_api_files(
21 ir: &IrSpec,
22 package_name: &str,
23 header: &str,
24 request_inputs: &RequestInputPlan,
25) -> Result<Vec<FileInfo>, String> {
26 let by_tag = group_by_tag(&ir.operations);
27 let mut files = Vec::with_capacity(by_tag.len());
28 for (tag, ops) in &by_tag {
29 let class_name = format!("{}Api", tag.to_pascal_case());
30 let filename = format!("{class_name}.java");
31 let body = emit_api_file(tag, ops, ir, package_name, request_inputs);
32 let content = format!("{header}{body}");
33 files.push(FileInfo::api(filename, content));
34 }
35 Ok(files)
36}
37
38fn group_by_tag(operations: &[IrOperation]) -> BTreeMap<String, Vec<&IrOperation>> {
39 let mut out: BTreeMap<String, Vec<&IrOperation>> = BTreeMap::new();
40 for op in operations {
41 let tags: Vec<String> = if op.tags.is_empty() {
42 vec!["default".to_string()]
43 } else {
44 op.tags.clone()
45 };
46 for tag in tags {
47 out.entry(tag).or_default().push(op);
48 }
49 }
50 out
51}
52
53fn emit_api_file(
58 tag: &str,
59 ops: &[&IrOperation],
60 ir: &IrSpec,
61 package_name: &str,
62 request_inputs: &RequestInputPlan,
63) -> String {
64 let class_name = format!("{}Api", tag.to_pascal_case());
65 let plans: Vec<OpPlan> = ops
66 .iter()
67 .map(|op| plan_operation(op, ir, request_inputs))
68 .collect();
69
70 let filename = format!("{class_name}.java");
71 let mut fb = FileSpec::builder_with(&filename, Java::new())
72 .header(package_header(package_name))
73 .add_import(ImportSpec::named(&format!("{package_name}.models"), "*"))
74 .add_import(ImportSpec::named(
75 &format!("{package_name}.runtime"),
76 "ApiClient",
77 ))
78 .add_import(ImportSpec::named(
79 &format!("{package_name}.runtime"),
80 "ApiException",
81 ))
82 .add_import(ImportSpec::named("com.google.gson", "Gson"))
83 .add_import(ImportSpec::named("com.google.gson.reflect", "TypeToken"))
84 .add_import(ImportSpec::named("java.io", "IOException"))
85 .add_import(ImportSpec::named("java.nio.charset", "StandardCharsets"))
86 .add_import(ImportSpec::named("java.util", "HashMap"))
87 .add_import(ImportSpec::named("java.util", "List"))
88 .add_import(ImportSpec::named("java.util", "Map"))
89 .add_import(ImportSpec::named("java.util.stream", "Collectors"))
90 .add_import(ImportSpec::named("okhttp3", "Request"))
91 .add_import(ImportSpec::named("okhttp3", "Response"));
92 let has_supported_multipart_body = plans.iter().any(|plan| {
93 plan.body.as_ref().is_some_and(|body| {
94 media_type_base(&body.media_type) == "multipart/form-data"
95 && body.multipart_parts.is_some()
96 })
97 });
98 let has_raw_request_body = plans.iter().any(|plan| plan.body.is_some());
99 if has_supported_multipart_body {
100 fb = fb.add_import(ImportSpec::named("okhttp3", "MultipartBody"));
101 }
102 if has_raw_request_body {
103 fb = fb.add_import(ImportSpec::named("okhttp3", "RequestBody"));
104 }
105 if has_supported_multipart_body || has_raw_request_body {
106 fb = fb.add_import(ImportSpec::named("okhttp3", "MediaType"));
107 }
108
109 for plan in &plans {
111 fb = fb.add_type(build_response_class(plan));
112 }
113
114 let mut cls = TypeSpec::builder(&class_name, TypeKind::Class).visibility(Visibility::Public);
116 cls = cls.doc(&format!(
117 "{class_name} groups operations under the {tag} tag."
118 ));
119
120 cls = cls.add_field(
122 FieldSpec::builder("client", TypeName::primitive("ApiClient"))
123 .visibility(Visibility::Private)
124 .is_readonly()
125 .build()
126 .expect("client field"),
127 );
128 cls = cls.add_field(
129 FieldSpec::builder("gson", TypeName::primitive("Gson"))
130 .visibility(Visibility::Private)
131 .is_readonly()
132 .initializer(CodeBlock::of("new Gson()", ()).expect("gson init"))
133 .build()
134 .expect("gson field"),
135 );
136
137 let mut ctor = FunSpec::builder(&class_name);
139 ctor = ctor.visibility(Visibility::Public);
140 ctor = ctor.add_param(
141 ParameterSpec::new("ApiClient client", TypeName::primitive("")).expect("client param"),
142 );
143 let ctor_body = sigil_quote!(Java {
144 this.client = client;
145 })
146 .expect("ctor body");
147 ctor = ctor.body(ctor_body);
148 cls = cls.add_method(ctor.build().expect("constructor"));
149
150 for plan in &plans {
152 cls = cls.add_method(build_operation_fun(plan));
153 }
154
155 fb = fb.add_type(cls.build().expect("API class builds"));
156
157 let file = fb.build().expect("FileSpec builds for API file");
158 file.render(RENDER_WIDTH)
159 .expect("FileSpec renders for API file")
160}
161
162fn package_header(package_name: &str) -> CodeBlock {
163 sigil_quote!(Java {
164 package $L(format!("{package_name}.apis"));
165 })
166 .expect("package header builds")
167}
168
169fn build_response_class(plan: &OpPlan<'_>) -> TypeSpec {
174 let mut tb =
175 TypeSpec::builder(&plan.response_type, TypeKind::Struct).visibility(Visibility::Public);
176 tb = tb.doc(&format!(
177 "{} carries the response from {}.",
178 plan.response_type, plan.method_name
179 ));
180
181 tb = tb.add_field(
183 FieldSpec::builder("statusCode", TypeName::primitive("int"))
184 .visibility(Visibility::Private)
185 .is_readonly()
186 .build()
187 .expect("field"),
188 );
189 tb = tb.add_field(
190 FieldSpec::builder("raw", TypeName::primitive("Response"))
191 .visibility(Visibility::Private)
192 .is_readonly()
193 .build()
194 .expect("field"),
195 );
196
197 let mut seen: HashSet<String> = HashSet::new();
198 for tr in &plan.typed_responses {
199 if !seen.insert(tr.field_name.clone()) {
200 continue;
201 }
202 tb = tb.add_field(
203 FieldSpec::builder(&tr.field_name, TypeName::primitive(&tr.java_type))
204 .visibility(Visibility::Private)
205 .is_readonly()
206 .build()
207 .expect("field"),
208 );
209 }
210
211 let mut ctor = FunSpec::builder(&plan.response_type);
213 ctor = ctor.visibility(Visibility::Public);
214 ctor = ctor
215 .add_param(ParameterSpec::new("int statusCode", TypeName::primitive("")).expect("param"));
216 ctor =
217 ctor.add_param(ParameterSpec::new("Response raw", TypeName::primitive("")).expect("param"));
218 let mut ctor_seen: HashSet<String> = HashSet::new();
219 for tr in &plan.typed_responses {
220 if !ctor_seen.insert(tr.field_name.clone()) {
221 continue;
222 }
223 ctor = ctor.add_param(
224 ParameterSpec::new(
225 &format!("{} {}", tr.java_type, tr.field_name),
226 TypeName::primitive(""),
227 )
228 .expect("param"),
229 );
230 }
231 let mut assignment_fields = Vec::new();
232 let mut body_seen: HashSet<String> = HashSet::new();
233 for tr in &plan.typed_responses {
234 if !body_seen.insert(tr.field_name.clone()) {
235 continue;
236 }
237 assignment_fields.push(tr.field_name.clone());
238 }
239 let ctor_body = sigil_quote!(Java {
240 this.statusCode = statusCode;
241 this.raw = raw;
242 $for(field_name in &assignment_fields) {
243 this.$L(field_name.as_str()) = $L(field_name.as_str());
244 }
245 })
246 .expect("ctor body");
247 ctor = ctor.body(ctor_body);
248 tb = tb.add_method(ctor.build().expect("response ctor"));
249
250 tb = tb.add_method(build_java_getter("getStatusCode", "int", "statusCode"));
252 tb = tb.add_method(build_java_getter("getRaw", "Response", "raw"));
253
254 let mut getter_seen: HashSet<String> = HashSet::new();
255 for tr in &plan.typed_responses {
256 if !getter_seen.insert(tr.field_name.clone()) {
257 continue;
258 }
259 let getter_name = format!("get{}", tr.field_name.to_pascal_case());
260 tb = tb.add_method(build_java_getter(
261 &getter_name,
262 &tr.java_type,
263 &tr.field_name,
264 ));
265 }
266
267 tb.build().expect("response class builds")
268}
269
270fn build_operation_fun(plan: &OpPlan<'_>) -> FunSpec {
275 let mut fb = FunSpec::builder(&plan.method_name);
276 fb = fb.visibility(Visibility::Public);
277
278 if let Some(summary) = &plan.op.summary {
279 fb = fb.doc(summary);
280 } else {
281 fb = fb.doc(&format!(
282 "{} {} {}.",
283 plan.method_name,
284 plan.op.method.to_uppercase(),
285 plan.op.path,
286 ));
287 }
288
289 for p in plan
291 .path_params
292 .iter()
293 .chain(&plan.query_params)
294 .chain(&plan.header_params)
295 {
296 fb = fb.add_param(
297 ParameterSpec::new(
298 &format!("{} {}", p.java_type, p.var_name),
299 TypeName::primitive(""),
300 )
301 .expect("param"),
302 );
303 }
304 if let Some(body) = &plan.body {
305 fb = fb.add_param(
306 ParameterSpec::new(
307 &format!("{} {}", body.java_type, body.var_name),
308 TypeName::primitive(""),
309 )
310 .expect("body param"),
311 );
312 }
313
314 fb = fb.returns(TypeName::primitive(&plan.response_type));
315 fb = fb.suffix("throws IOException");
316 fb = fb.body(emit_method_body(plan));
317
318 fb.build().expect("operation FunSpec builds")
319}
320
321fn emit_method_body(plan: &OpPlan<'_>) -> CodeBlock {
326 let mut cb = CodeBlock::builder();
327
328 let mut path_expr = format!("\"{}\"", plan.op.path);
330 for p in &plan.path_params {
331 let placeholder = format!("{{{}}}", p.param.name);
332 let stringified = render_value_as_string(&p.var_name, &p.param.type_expr);
333 path_expr = format!("{path_expr}.replace(\"{placeholder}\", {stringified})");
334 }
335 cb.add_statement(&format!("String path = {path_expr}"), ());
336
337 let has_query = !plan.query_params.is_empty();
339 if has_query {
340 cb.add_statement("Map<String, String> query = new HashMap<>()", ());
341 for p in &plan.query_params {
342 let stringified = render_value_as_string(&p.var_name, &p.param.type_expr);
343 cb.add_code(java_query_param_put(
344 p.param.required,
345 &p.var_name,
346 &p.param.name,
347 &stringified,
348 ));
349 }
350 }
351
352 let method = plan.op.method.to_uppercase();
354 if let Some(body) = &plan.body {
355 cb.add_statement("Request request", ());
356 if body.encoding == BodyEncoding::Multipart {
357 if let Some(parts) = &body.multipart_parts {
358 emit_multipart_body(&mut cb, body, parts);
359 cb.add_code(java_new_request_with_body(
360 &method,
361 has_query,
362 "multipartBody",
363 ));
364 } else {
365 cb.add_statement(
366 "throw new IllegalArgumentException(\"unsupported multipart request body: schema must be object-shaped\")",
367 (),
368 );
369 }
370 } else {
371 emit_request_body(&mut cb, body);
372 cb.add_code(java_new_request_with_body(
373 &method,
374 has_query,
375 "requestBody",
376 ));
377 }
378 } else {
379 cb.add_code(java_new_request(&method, has_query));
380 }
381
382 for p in &plan.header_params {
384 let stringified = render_value_as_string(&p.var_name, &p.param.type_expr);
385 cb.add_code(java_header_param_set(
386 p.param.required,
387 &p.var_name,
388 &p.param.name,
389 &stringified,
390 ));
391 }
392
393 cb.add_statement("Response response = client.execute(request)", ());
395 cb.add_line();
396
397 let error_block = sigil_quote!(Java {
399 if (!response.isSuccessful()) {
400 String errorBody = response.body() != null ? response.body().string() : "";
401 throw new ApiException(response.code(), response.message(), errorBody);
402 }
403 })
404 .expect("error block");
405 cb.add_code(error_block);
406
407 if !plan.typed_responses.is_empty() {
409 cb.add_statement(
410 "byte[] responseBytes = response.body() != null ? response.body().bytes() : new byte[0]",
411 (),
412 );
413 cb.add_statement(
414 "String responseText = new String(responseBytes, StandardCharsets.UTF_8)",
415 (),
416 );
417 let mut seen: HashSet<String> = HashSet::new();
418
419 for tr in &plan.typed_responses {
421 if !seen.insert(tr.field_name.clone()) {
422 continue;
423 }
424 cb.add_statement(&format!("{} {} = null", tr.java_type, tr.field_name), ());
425 cb.add_code(java_response_decode_assignment(tr));
426 }
427
428 let args: Vec<String> = std::iter::once("response.code()".to_string())
430 .chain(std::iter::once("response".to_string()))
431 .chain(plan.typed_responses.iter().map(|tr| tr.field_name.clone()))
432 .collect();
433 let mut dedup_args: Vec<String> = Vec::new();
435 let mut args_seen: HashSet<String> = HashSet::new();
436 for a in args {
437 if args_seen.insert(a.clone()) {
438 dedup_args.push(a);
439 }
440 }
441 cb.add_code(
442 sigil_quote!(Java {
443 return new $N(plan.response_type.as_str())($for(arg in &dedup_args; separator = ", ") { $L(arg.as_str()) });
444 })
445 .expect("typed response constructor return"),
446 );
447 } else {
448 cb.add_statement(
449 &format!(
450 "return new {}(response.code(), response)",
451 plan.response_type
452 ),
453 (),
454 );
455 }
456
457 cb.build().expect("method body builds")
458}
459
460fn java_new_request(method: &str, has_query: bool) -> CodeBlock {
461 let with_query =
462 format!("Request request = client.newRequest(\"{method}\", path, query, null);");
463 let without_query =
464 format!("Request request = client.newRequest(\"{method}\", path, null, null);");
465 sigil_quote!(Java {
466 $if(has_query) {
467 $L(with_query.as_str())
468 } $else {
469 $L(without_query.as_str())
470 }
471 })
472 .expect("Java request construction builds")
473}
474
475fn java_new_request_with_body(method: &str, has_query: bool, body_expr: &str) -> CodeBlock {
476 let with_query =
477 format!("request = client.newRequestWithBody(\"{method}\", path, query, {body_expr});");
478 let without_query =
479 format!("request = client.newRequestWithBody(\"{method}\", path, null, {body_expr});");
480 sigil_quote!(Java {
481 $if(has_query) {
482 $L(with_query.as_str())
483 } $else {
484 $L(without_query.as_str())
485 }
486 })
487 .expect("Java request body construction builds")
488}
489
490fn java_query_param_put(
491 required: bool,
492 var_name: &str,
493 param_name: &str,
494 value_expr: &str,
495) -> CodeBlock {
496 sigil_quote!(Java {
497 $if(required) {
498 query.put($S(param_name), $L(value_expr));
499 } $else {
500 if ($L(var_name) != null) {
501 query.put($S(param_name), $L(value_expr));
502 }
503 }
504 })
505 .expect("Java query param put builds")
506}
507
508fn java_header_param_set(
509 required: bool,
510 var_name: &str,
511 param_name: &str,
512 value_expr: &str,
513) -> CodeBlock {
514 sigil_quote!(Java {
515 $if(required) {
516 request = request.newBuilder().header($S(param_name), $L(value_expr)).build();
517 } $else {
518 if ($L(var_name) != null) {
519 request = request.newBuilder().header($S(param_name), $L(value_expr)).build();
520 }
521 }
522 })
523 .expect("Java header param set builds")
524}
525
526fn emit_multipart_body(
527 cb: &mut sigil_stitch::code_block::CodeBlockBuilder,
528 body: &BodyBinding,
529 parts: &[MultipartPart],
530) {
531 if !body.required {
532 cb.add_code(
533 sigil_quote!(Java {
534 RequestBody multipartBody = RequestBody.create(new byte[0], null);
535 })
536 .expect("default multipart body builds"),
537 );
538 cb.begin_control_flow(&format!("if ({} != null)", body.var_name), ());
539 }
540 cb.add_code(
541 sigil_quote!(Java {
542 MultipartBody.Builder multipartBuilder = new MultipartBody.Builder().setType(MultipartBody.FORM);
543 })
544 .expect("multipart builder builds"),
545 );
546 for part in parts {
547 let access = format!(
548 "{}.get{}()",
549 body.var_name,
550 part.field_name.to_pascal_case()
551 );
552 if part.required {
553 emit_required_multipart_part(cb, part, &access);
554 } else {
555 cb.begin_control_flow(&format!("if ({access} != null)"), ());
556 emit_required_multipart_part(cb, part, &access);
557 cb.end_control_flow();
558 }
559 }
560 cb.add_code(java_multipart_body_finish(body.required));
561 if !body.required {
562 cb.end_control_flow();
563 }
564}
565
566fn java_multipart_body_finish(body_required: bool) -> CodeBlock {
567 sigil_quote!(Java {
568 $if(body_required) {
569 RequestBody multipartBody = multipartBuilder.build();
570 } $else {
571 multipartBody = multipartBuilder.build();
572 }
573 })
574 .expect("multipart body finish builds")
575}
576
577fn emit_request_body(cb: &mut sigil_stitch::code_block::CodeBlockBuilder, body: &BodyBinding) {
578 if !body.required {
579 cb.add_code(
580 sigil_quote!(Java {
581 RequestBody requestBody = RequestBody.create(new byte[0], null);
582 })
583 .expect("default request body builds"),
584 );
585 cb.begin_control_flow(&format!("if ({} != null)", body.var_name), ());
586 }
587 match body.encoding {
588 BodyEncoding::Json => {
589 let body_var = body.var_name.as_str();
590 let media_type = body.media_type.as_str();
591 cb.add_code(java_json_request_body(body.required, body_var, media_type));
592 }
593 BodyEncoding::TextPlain | BodyEncoding::OctetStream => {
594 let body_var = body.var_name.as_str();
595 let media_type = body.media_type.as_str();
596 cb.add_code(java_raw_request_body(body.required, body_var, media_type));
597 }
598 BodyEncoding::FormUrlEncoded | BodyEncoding::Xml | BodyEncoding::Other => {
599 let message = format!("unsupported request body media type: {}", body.media_type);
600 cb.add_code(
601 sigil_quote!(Java {
602 throw new IllegalArgumentException($S(message));
603 })
604 .expect("unsupported request body builds"),
605 );
606 }
607 BodyEncoding::Multipart => unreachable!("multipart handled separately"),
608 }
609 if !body.required {
610 cb.end_control_flow();
611 }
612}
613
614fn java_json_request_body(body_required: bool, body_var: &str, media_type: &str) -> CodeBlock {
615 sigil_quote!(Java {
616 String jsonBody = gson.toJson($L(body_var));
617 $if(body_required) {
618 RequestBody requestBody = RequestBody.create(jsonBody, MediaType.get($S(media_type)));
619 } $else {
620 requestBody = RequestBody.create(jsonBody, MediaType.get($S(media_type)));
621 }
622 })
623 .expect("json request body builds")
624}
625
626fn java_raw_request_body(body_required: bool, body_var: &str, media_type: &str) -> CodeBlock {
627 sigil_quote!(Java {
628 $if(body_required) {
629 RequestBody requestBody = RequestBody.create($L(body_var), MediaType.get($S(media_type)));
630 } $else {
631 requestBody = RequestBody.create($L(body_var), MediaType.get($S(media_type)));
632 }
633 })
634 .expect("raw request body builds")
635}
636
637fn response_decode_expr(tr: &TypedResponse) -> String {
638 match tr.decoding {
639 ResponseDecoding::Json => {
640 let type_token = format!("new TypeToken<{}>() {{}}.getType()", tr.java_type);
641 format!("gson.fromJson(responseText.isEmpty() ? \"null\" : responseText, {type_token})")
642 }
643 ResponseDecoding::Text => "responseText".to_string(),
644 ResponseDecoding::Bytes => "responseBytes".to_string(),
645 }
646}
647
648fn java_response_decode_assignment(tr: &TypedResponse) -> CodeBlock {
649 let exact_status = tr.status.parse::<u16>().ok();
650 let has_exact_status = exact_status.is_some();
651 let status_code = exact_status.unwrap_or_default().to_string();
652 let guard = wildcard_status_guard_java(&tr.status);
653 let assignment = format!("{} = {}", tr.field_name, response_decode_expr(tr));
654 sigil_quote!(Java {
655 $if(has_exact_status) {
656 if (response.code() == $L(status_code.as_str())) {
657 $L(assignment.as_str());
658 }
659 } $else {
660 if ($L(guard.as_str())) {
661 $L(assignment.as_str());
662 }
663 }
664 })
665 .expect("Java response decode assignment builds")
666}
667
668fn emit_required_multipart_part(
669 cb: &mut sigil_stitch::code_block::CodeBlockBuilder,
670 part: &MultipartPart,
671 access: &str,
672) {
673 cb.add_code(java_multipart_part(part, access));
674}
675
676fn java_multipart_part(part: &MultipartPart, access: &str) -> CodeBlock {
677 let wire_name = part.wire_name.as_str();
678 let content_type = part.content_type.as_str();
679 sigil_quote!(Java {
680 $if(part.is_binary) {
681 multipartBuilder.addFormDataPart($S(wire_name), $L(access).filenameOrDefault($S(wire_name)), RequestBody.create($L(access).getData(), MediaType.get($S(content_type))));
682 } $else_if(part.value_encoding == MultipartValueEncoding::Json) {
683 multipartBuilder.addFormDataPart($S(wire_name), null, RequestBody.create(gson.toJson($L(access)), MediaType.get($S(content_type))));
684 } $else_if(part.value_encoding == MultipartValueEncoding::Unsupported) {
685 throw new IllegalArgumentException($S("unsupported multipart part content type"));
686 } $else {
687 multipartBuilder.addFormDataPart($S(wire_name), null, RequestBody.create(String.valueOf($L(access)), MediaType.get($S(content_type))));
688 }
689 })
690 .expect("multipart part block builds")
691}
692
693struct OpPlan<'a> {
698 op: &'a IrOperation,
699 method_name: String,
700 response_type: String,
701 path_params: Vec<ParamBinding<'a>>,
702 query_params: Vec<ParamBinding<'a>>,
703 header_params: Vec<ParamBinding<'a>>,
704 body: Option<BodyBinding>,
705 typed_responses: Vec<TypedResponse>,
706}
707
708struct ParamBinding<'a> {
709 param: &'a IrParameter,
710 var_name: String,
711 java_type: String,
712}
713
714struct BodyBinding {
715 var_name: String,
716 java_type: String,
717 media_type: String,
718 required: bool,
719 encoding: BodyEncoding,
720 multipart_parts: Option<Vec<MultipartPart>>,
721}
722
723struct MultipartPart {
724 wire_name: String,
725 field_name: String,
726 is_binary: bool,
727 required: bool,
728 content_type: String,
729 value_encoding: MultipartValueEncoding,
730}
731
732#[derive(Clone, Copy, PartialEq, Eq)]
733enum BodyEncoding {
734 Json,
735 Multipart,
736 FormUrlEncoded,
737 Xml,
738 TextPlain,
739 OctetStream,
740 Other,
741}
742
743#[derive(Clone, Copy, PartialEq, Eq)]
744enum ResponseDecoding {
745 Json,
746 Text,
747 Bytes,
748}
749
750struct TypedResponse {
751 status: String,
752 field_name: String,
753 java_type: String,
754 decoding: ResponseDecoding,
755}
756
757fn plan_operation<'a>(
758 op: &'a IrOperation,
759 ir: &IrSpec,
760 request_inputs: &RequestInputPlan,
761) -> OpPlan<'a> {
762 let op_id = sanitize_operation_id(&op.operation_id, &op.method, &op.path);
763 let method_name = op_id.to_lower_camel_case();
764 let response_type = format!("{}Response", op_id.to_pascal_case());
765
766 let mut used_names: HashSet<String> = HashSet::new();
767
768 let mut path_params = Vec::new();
769 let mut query_params = Vec::new();
770 let mut header_params = Vec::new();
771 for p in &op.parameters {
772 let var_name = unique_name(&java_ident(&p.name), &mut used_names);
773 let java_type = if p.required {
774 java_type_str(&p.type_expr)
775 } else {
776 java_boxed_type_str(&p.type_expr)
777 };
778 let binding = ParamBinding {
779 param: p,
780 var_name,
781 java_type,
782 };
783 match p.location {
784 ParameterLocation::Path => path_params.push(binding),
785 ParameterLocation::Query => query_params.push(binding),
786 ParameterLocation::Header => header_params.push(binding),
787 ParameterLocation::Cookie => header_params.push(binding),
788 }
789 }
790
791 let body = op
792 .request_body
793 .as_ref()
794 .and_then(|b| plan_body(op, b, ir, request_inputs, &mut used_names));
795
796 let typed_responses = op.responses.iter().filter_map(plan_response).collect();
797
798 OpPlan {
799 op,
800 method_name,
801 response_type,
802 path_params,
803 query_params,
804 header_params,
805 body,
806 typed_responses,
807 }
808}
809
810fn plan_body(
811 op: &IrOperation,
812 b: &IrRequestBody,
813 ir: &IrSpec,
814 request_inputs: &RequestInputPlan,
815 used_names: &mut HashSet<String>,
816) -> Option<BodyBinding> {
817 let (media_type, t) = pick_body_content(b)?;
818 let encoding = body_encoding(&media_type);
819 let java_type = match encoding {
820 BodyEncoding::TextPlain => "String".to_string(),
821 BodyEncoding::OctetStream => "byte[]".to_string(),
822 BodyEncoding::Multipart => request_input_for_operation(request_inputs, op, &media_type)
823 .map(|input| input.name.to_pascal_case())
824 .unwrap_or_else(|| java_type_str(&t)),
825 _ => java_type_str(&t),
826 };
827 let var_name = unique_name("body", used_names);
828 let multipart_parts = if media_type_base(&media_type) == "multipart/form-data" {
829 multipart_parts_for(b, &media_type, ir)
830 } else {
831 None
832 };
833 Some(BodyBinding {
834 var_name,
835 java_type,
836 media_type,
837 required: b.required,
838 encoding,
839 multipart_parts,
840 })
841}
842
843fn plan_response(r: &IrResponse) -> Option<TypedResponse> {
844 let (media_type, t) = pick_response_content(r)?;
845 let decoding = response_decoding(&media_type);
846 let java_type = match decoding {
847 ResponseDecoding::Json => java_type_str(&t),
848 ResponseDecoding::Text => "String".to_string(),
849 ResponseDecoding::Bytes => "byte[]".to_string(),
850 };
851 Some(TypedResponse {
852 status: r.status.clone(),
853 field_name: response_field_name(&r.status),
854 java_type,
855 decoding,
856 })
857}
858
859fn response_field_name(status: &str) -> String {
860 if status == "default" {
861 "default_".to_string()
862 } else if let Ok(code) = status.parse::<u16>() {
863 format!("status{code}")
864 } else {
865 format!("status{}", status.to_lowercase())
866 }
867}
868
869fn wildcard_status_guard_java(status: &str) -> String {
870 let upper = status.to_uppercase();
871 if upper == "4XX" {
872 "response.code() >= 400 && response.code() < 500".to_string()
873 } else if upper == "5XX" {
874 "response.code() >= 500 && response.code() < 600".to_string()
875 } else {
876 "true".to_string()
878 }
879}
880
881fn body_encoding(media_type: &str) -> BodyEncoding {
882 let base = media_type_base(media_type);
883 if base == "multipart/form-data" {
884 BodyEncoding::Multipart
885 } else if is_json_media_type(media_type) {
886 BodyEncoding::Json
887 } else if base == "application/x-www-form-urlencoded" {
888 BodyEncoding::FormUrlEncoded
889 } else if is_xml_media_type(media_type) {
890 BodyEncoding::Xml
891 } else if base == "text/plain" {
892 BodyEncoding::TextPlain
893 } else if base == "application/octet-stream" {
894 BodyEncoding::OctetStream
895 } else {
896 BodyEncoding::Other
897 }
898}
899
900fn response_decoding(media_type: &str) -> ResponseDecoding {
901 let base = media_type_base(media_type);
902 if is_json_media_type(media_type) {
903 ResponseDecoding::Json
904 } else if base == "text/plain" || is_xml_media_type(media_type) {
905 ResponseDecoding::Text
906 } else {
907 ResponseDecoding::Bytes
908 }
909}
910
911fn pick_body_content(body: &IrRequestBody) -> Option<(String, IrTypeExpr)> {
912 pick_media_type(&body.content, |media_type| {
913 media_type_base(media_type) == "application/json"
914 })
915 .or_else(|| pick_media_type(&body.content, is_json_media_type))
916 .or_else(|| {
917 pick_media_type(&body.content, |media_type| {
918 media_type_base(media_type) == "multipart/form-data"
919 })
920 })
921 .or_else(|| {
922 pick_media_type(&body.content, |media_type| {
923 media_type_base(media_type) == "application/x-www-form-urlencoded"
924 })
925 })
926 .or_else(|| pick_media_type(&body.content, is_xml_media_type))
927 .or_else(|| {
928 pick_media_type(&body.content, |media_type| {
929 media_type_base(media_type) == "text/plain"
930 })
931 })
932 .or_else(|| {
933 pick_media_type(&body.content, |media_type| {
934 media_type_base(media_type) == "application/octet-stream"
935 })
936 })
937 .or_else(|| pick_first_content(&body.content))
938}
939
940fn pick_response_content(r: &IrResponse) -> Option<(String, IrTypeExpr)> {
941 pick_media_type(&r.content, |media_type| {
942 media_type_base(media_type) == "application/json"
943 })
944 .or_else(|| pick_media_type(&r.content, is_json_media_type))
945 .or_else(|| {
946 pick_media_type(&r.content, |media_type| {
947 media_type_base(media_type) == "application/octet-stream"
948 })
949 })
950 .or_else(|| {
951 pick_media_type(&r.content, |media_type| {
952 media_type_base(media_type) == "text/plain"
953 })
954 })
955 .or_else(|| pick_media_type(&r.content, is_xml_media_type))
956 .or_else(|| pick_first_content(&r.content))
957}
958
959fn pick_media_type(
960 content: &indexmap::IndexMap<String, IrTypeExpr>,
961 predicate: impl Fn(&str) -> bool,
962) -> Option<(String, IrTypeExpr)> {
963 content
964 .iter()
965 .find(|(media_type, _)| predicate(media_type))
966 .map(|(media_type, t)| (media_type.clone(), t.clone()))
967}
968
969fn pick_first_content(
970 content: &indexmap::IndexMap<String, IrTypeExpr>,
971) -> Option<(String, IrTypeExpr)> {
972 content
973 .iter()
974 .next()
975 .map(|(media_type, t)| (media_type.clone(), t.clone()))
976}
977
978fn media_type_base(media_type: &str) -> String {
979 media_type
980 .split(';')
981 .next()
982 .unwrap_or(media_type)
983 .trim()
984 .to_ascii_lowercase()
985}
986
987fn is_json_media_type(media_type: &str) -> bool {
988 let base = media_type_base(media_type);
989 base == "application/json" || base.ends_with("+json")
990}
991
992fn is_xml_media_type(media_type: &str) -> bool {
993 let base = media_type_base(media_type);
994 base == "application/xml" || base == "text/xml" || base.ends_with("+xml")
995}
996
997fn multipart_parts_for(
998 body: &IrRequestBody,
999 media_type: &str,
1000 ir: &IrSpec,
1001) -> Option<Vec<MultipartPart>> {
1002 multipart_parts_for_request_body(body, media_type, ir).map(|parts| {
1003 parts
1004 .into_iter()
1005 .map(|part| MultipartPart {
1006 field_name: java_field_name(&part.wire_name),
1007 wire_name: part.wire_name,
1008 is_binary: part.is_binary,
1009 required: part.required,
1010 content_type: part.content_type,
1011 value_encoding: part.value_encoding,
1012 })
1013 .collect()
1014 })
1015}