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