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::generators::response_headers::{
7 ResponseHeaderPlan, ResponseHeaderValueKind, collect_response_headers,
8 unique_response_header_accessor_names,
9};
10use crate::generators::response_names::{
11 response_entry_name as response_variant_name, response_match_rank,
12};
13use crate::ir::types::{
14 IrOperation, IrParameter, IrRequestBody, IrResponse, IrSpec, IrTypeExpr, ParameterLocation,
15};
16use heck::{ToLowerCamelCase, ToPascalCase};
17use sigil_stitch::lang::java::Java;
18use sigil_stitch::prelude::*;
19
20use super::util::{
21 build_java_getter, java_boxed_type_str, java_field_name, java_ident, java_type_str,
22 render_value_as_string, sanitize_operation_id, unique_name,
23};
24
25const RENDER_WIDTH: usize = 100;
26
27pub fn generate_api_files(
28 ir: &IrSpec,
29 package_name: &str,
30 header: &str,
31 request_inputs: &RequestInputPlan,
32) -> Result<Vec<FileInfo>, String> {
33 let by_tag = group_by_tag(&ir.operations);
34 let mut files = Vec::with_capacity(by_tag.len());
35 let mut support_files_seen = HashSet::new();
36 let has_models = has_java_models(ir, request_inputs);
37 for (tag, ops) in &by_tag {
38 let class_name = format!("{}Api", tag.to_pascal_case());
39 let filename = format!("{class_name}.java");
40 let body = emit_api_file(tag, ops, ir, package_name, request_inputs, has_models);
41 let content = format!("{header}{body}");
42 files.push(FileInfo::api(filename, content));
43 for op in ops {
44 let plan = plan_operation(op, ir, request_inputs);
45 for file in operation_support_files(&plan, package_name, header, has_models) {
46 if support_files_seen.insert(file.filename.clone()) {
47 files.push(file);
48 }
49 }
50 }
51 }
52 Ok(files)
53}
54
55fn group_by_tag(operations: &[IrOperation]) -> BTreeMap<String, Vec<&IrOperation>> {
56 let mut out: BTreeMap<String, Vec<&IrOperation>> = BTreeMap::new();
57 for op in operations {
58 let tags: Vec<String> = if op.tags.is_empty() {
59 vec!["default".to_string()]
60 } else {
61 op.tags.clone()
62 };
63 for tag in tags {
64 out.entry(tag).or_default().push(op);
65 }
66 }
67 out
68}
69
70fn has_java_models(ir: &IrSpec, request_inputs: &RequestInputPlan) -> bool {
71 !ir.schemas.is_empty() || !request_inputs.models().is_empty()
72}
73
74fn emit_api_file(
79 tag: &str,
80 ops: &[&IrOperation],
81 ir: &IrSpec,
82 package_name: &str,
83 request_inputs: &RequestInputPlan,
84 has_models: bool,
85) -> String {
86 let class_name = format!("{}Api", tag.to_pascal_case());
87 let plans: Vec<OpPlan> = ops
88 .iter()
89 .map(|op| plan_operation(op, ir, request_inputs))
90 .collect();
91
92 let filename = format!("{class_name}.java");
93 let mut fb = FileSpec::builder_with(&filename, Java::new())
94 .header(package_header(package_name))
95 .add_import(ImportSpec::named(
96 &format!("{package_name}.runtime"),
97 "ApiClient",
98 ))
99 .add_import(ImportSpec::named(
100 &format!("{package_name}.runtime"),
101 "ApiException",
102 ))
103 .add_import(ImportSpec::named("com.google.gson", "Gson"))
104 .add_import(ImportSpec::named("com.google.gson.reflect", "TypeToken"))
105 .add_import(ImportSpec::named("java.io", "IOException"))
106 .add_import(ImportSpec::named("java.nio.charset", "StandardCharsets"))
107 .add_import(ImportSpec::named("java.util", "HashMap"))
108 .add_import(ImportSpec::named("java.util", "List"))
109 .add_import(ImportSpec::named("java.util", "Map"))
110 .add_import(ImportSpec::named("java.util.stream", "Collectors"))
111 .add_import(ImportSpec::named("okhttp3", "Request"))
112 .add_import(ImportSpec::named("okhttp3", "Response"));
113 if has_models {
114 fb = fb.add_import(ImportSpec::named(&format!("{package_name}.models"), "*"));
115 }
116 let has_supported_multipart_body = plans.iter().any(|plan| {
117 plan.body.as_ref().is_some_and(|body| {
118 media_type_base(&body.media_type) == "multipart/form-data"
119 && body.multipart_parts.is_some()
120 })
121 });
122 let has_raw_request_body = plans.iter().any(|plan| plan.body.is_some());
123 if has_supported_multipart_body {
124 fb = fb.add_import(ImportSpec::named("okhttp3", "MultipartBody"));
125 }
126 if has_raw_request_body {
127 fb = fb.add_import(ImportSpec::named("okhttp3", "RequestBody"));
128 }
129 if has_supported_multipart_body || has_raw_request_body {
130 fb = fb.add_import(ImportSpec::named("okhttp3", "MediaType"));
131 }
132
133 let mut cls = TypeSpec::builder(&class_name, TypeKind::Class).visibility(Visibility::Public);
135 cls = cls.doc(&format!(
136 "{class_name} groups operations under the {tag} tag."
137 ));
138
139 cls = cls.add_field(
141 FieldSpec::builder("client", TypeName::primitive("ApiClient"))
142 .visibility(Visibility::Private)
143 .is_readonly()
144 .build()
145 .expect("client field"),
146 );
147 cls = cls.add_field(
148 FieldSpec::builder("gson", TypeName::primitive("Gson"))
149 .visibility(Visibility::Private)
150 .is_readonly()
151 .initializer(CodeBlock::of("new Gson()", ()).expect("gson init"))
152 .build()
153 .expect("gson field"),
154 );
155
156 let mut ctor = FunSpec::builder(&class_name);
158 ctor = ctor.visibility(Visibility::Public);
159 ctor = ctor.add_param(
160 ParameterSpec::new("ApiClient client", TypeName::primitive("")).expect("client param"),
161 );
162 let ctor_body = sigil_quote!(Java {
163 this.client = client;
164 })
165 .expect("ctor body");
166 ctor = ctor.body(ctor_body);
167 cls = cls.add_method(ctor.build().expect("constructor"));
168
169 for plan in &plans {
171 cls = cls.add_method(build_operation_fun(plan));
172 }
173
174 fb = fb.add_type(cls.build().expect("API class builds"));
175
176 let file = fb.build().expect("FileSpec builds for API file");
177 file.render(RENDER_WIDTH)
178 .expect("FileSpec renders for API file")
179}
180
181fn package_header(package_name: &str) -> CodeBlock {
182 sigil_quote!(Java {
183 package $L(format!("{package_name}.apis"));
184 })
185 .expect("package header builds")
186}
187
188fn operation_support_files(
189 plan: &OpPlan<'_>,
190 package_name: &str,
191 header: &str,
192 has_models: bool,
193) -> Vec<FileInfo> {
194 let mut files = Vec::new();
195 let mut response_imports = vec![
196 ImportSpec::named("java.util", "List"),
197 ImportSpec::named("java.util", "Map"),
198 ImportSpec::named("okhttp3", "Response"),
199 ];
200 if has_models {
201 response_imports.push(ImportSpec::named(&format!("{package_name}.models"), "*"));
202 }
203 files.push(java_type_file(
204 &plan.response_type,
205 package_name,
206 header,
207 response_imports,
208 build_response_class(plan),
209 ));
210
211 let detail_interface = format!("{}Detail", plan.error_type);
212 let mut detail_impl_names = Vec::new();
213 let mut detail_seen = HashSet::new();
214 for response in &plan.error_responses {
215 if !detail_seen.insert(response.field_name.clone()) {
216 continue;
217 }
218 detail_impl_names.push(format!(
219 "{}{}",
220 plan.method_name.to_pascal_case(),
221 response.field_name
222 ));
223 }
224 let unexpected = format!("{}Unexpected", plan.method_name.to_pascal_case());
225 detail_impl_names.push(unexpected.clone());
226 files.push(java_code_file(
227 &detail_interface,
228 package_name,
229 header,
230 Vec::new(),
231 java_error_detail_interface(&detail_interface, &detail_impl_names),
232 ));
233
234 files.push(java_code_file(
235 &plan.error_type,
236 package_name,
237 header,
238 vec![
239 ImportSpec::named(&format!("{package_name}.runtime"), "ApiException"),
240 ImportSpec::named("okhttp3", "Headers"),
241 ],
242 java_error_exception_class(&plan.error_type, &detail_interface, &plan.error_headers),
243 ));
244
245 let mut seen = HashSet::new();
246 for response in &plan.error_responses {
247 if !seen.insert(response.field_name.clone()) {
248 continue;
249 }
250 let class_name = format!(
251 "{}{}",
252 plan.method_name.to_pascal_case(),
253 response.field_name
254 );
255 files.push(java_code_file(
256 &class_name,
257 package_name,
258 header,
259 error_detail_imports(package_name, has_models),
260 java_error_detail_class(&class_name, &detail_interface, response),
261 ));
262 }
263
264 files.push(java_code_file(
265 &unexpected,
266 package_name,
267 header,
268 Vec::new(),
269 java_unexpected_detail_class(&unexpected, &detail_interface),
270 ));
271 files
272}
273
274fn java_type_file(
275 class_name: &str,
276 package_name: &str,
277 header: &str,
278 imports: Vec<ImportSpec>,
279 type_spec: TypeSpec,
280) -> FileInfo {
281 let filename = format!("{class_name}.java");
282 let mut fb =
283 FileSpec::builder_with(&filename, Java::new()).header(package_header(package_name));
284 for import in imports {
285 fb = fb.add_import(import);
286 }
287 fb = fb.add_type(type_spec);
288 let file = fb.build().expect("Java support type file builds");
289 FileInfo::api(
290 filename,
291 format!(
292 "{header}{}",
293 file.render(RENDER_WIDTH)
294 .expect("Java support type file renders")
295 ),
296 )
297}
298
299fn java_code_file(
300 class_name: &str,
301 package_name: &str,
302 header: &str,
303 imports: Vec<ImportSpec>,
304 code: CodeBlock,
305) -> FileInfo {
306 let filename = format!("{class_name}.java");
307 let mut fb =
308 FileSpec::builder_with(&filename, Java::new()).header(package_header(package_name));
309 for import in imports {
310 fb = fb.add_import(import);
311 }
312 fb = fb.add_code(code);
313 let file = fb.build().expect("Java support code file builds");
314 FileInfo::api(
315 filename,
316 format!(
317 "{header}{}",
318 file.render(RENDER_WIDTH)
319 .expect("Java support code file renders")
320 ),
321 )
322}
323
324fn error_detail_imports(package_name: &str, has_models: bool) -> Vec<ImportSpec> {
325 let mut imports = vec![
326 ImportSpec::named("com.google.gson", "Gson"),
327 ImportSpec::named("com.google.gson.reflect", "TypeToken"),
328 ImportSpec::named("java.nio.charset", "StandardCharsets"),
329 ImportSpec::named("java.util", "List"),
330 ImportSpec::named("java.util", "Map"),
331 ];
332 if has_models {
333 imports.push(ImportSpec::named(&format!("{package_name}.models"), "*"));
334 }
335 imports
336}
337
338fn build_response_class(plan: &OpPlan<'_>) -> TypeSpec {
343 let mut tb =
344 TypeSpec::builder(&plan.response_type, TypeKind::Struct).visibility(Visibility::Public);
345 tb = tb.doc(&format!(
346 "{} carries the response from {}.",
347 plan.response_type, plan.method_name
348 ));
349
350 tb = tb.add_field(
352 FieldSpec::builder("statusCode", TypeName::primitive("int"))
353 .visibility(Visibility::Private)
354 .is_readonly()
355 .build()
356 .expect("field"),
357 );
358 tb = tb.add_field(
359 FieldSpec::builder("raw", TypeName::primitive("Response"))
360 .visibility(Visibility::Private)
361 .is_readonly()
362 .build()
363 .expect("field"),
364 );
365
366 let mut seen: HashSet<String> = HashSet::new();
367 for tr in &plan.typed_responses {
368 if !seen.insert(tr.field_name.clone()) {
369 continue;
370 }
371 tb = tb.add_field(
372 FieldSpec::builder(&tr.field_name, TypeName::primitive(&tr.java_type))
373 .visibility(Visibility::Private)
374 .is_readonly()
375 .build()
376 .expect("field"),
377 );
378 }
379
380 let mut ctor = FunSpec::builder(&plan.response_type);
382 ctor = ctor.visibility(Visibility::Public);
383 ctor = ctor
384 .add_param(ParameterSpec::new("int statusCode", TypeName::primitive("")).expect("param"));
385 ctor =
386 ctor.add_param(ParameterSpec::new("Response raw", TypeName::primitive("")).expect("param"));
387 let mut ctor_seen: HashSet<String> = HashSet::new();
388 for tr in &plan.typed_responses {
389 if !ctor_seen.insert(tr.field_name.clone()) {
390 continue;
391 }
392 ctor = ctor.add_param(
393 ParameterSpec::new(
394 &format!("{} {}", tr.java_type, tr.field_name),
395 TypeName::primitive(""),
396 )
397 .expect("param"),
398 );
399 }
400 let mut assignment_fields = Vec::new();
401 let mut body_seen: HashSet<String> = HashSet::new();
402 for tr in &plan.typed_responses {
403 if !body_seen.insert(tr.field_name.clone()) {
404 continue;
405 }
406 assignment_fields.push(tr.field_name.clone());
407 }
408 let ctor_body = sigil_quote!(Java {
409 this.statusCode = statusCode;
410 this.raw = raw;
411 $for(field_name in &assignment_fields) {
412 this.$L(field_name.as_str()) = $L(field_name.as_str());
413 }
414 })
415 .expect("ctor body");
416 ctor = ctor.body(ctor_body);
417 tb = tb.add_method(ctor.build().expect("response ctor"));
418
419 tb = tb.add_method(build_java_getter("getStatusCode", "int", "statusCode"));
421 tb = tb.add_method(build_java_getter("getRaw", "Response", "raw"));
422
423 let mut getter_seen: HashSet<String> = HashSet::new();
424 for tr in &plan.typed_responses {
425 if !getter_seen.insert(tr.field_name.clone()) {
426 continue;
427 }
428 let getter_name = format!("get{}", tr.field_name.to_pascal_case());
429 tb = tb.add_method(build_java_getter(
430 &getter_name,
431 &tr.java_type,
432 &tr.field_name,
433 ));
434 }
435 let header_method_names =
436 unique_response_header_accessor_names(&plan.success_headers, |wire_name| {
437 format!("get{}Header", wire_name.to_pascal_case())
438 });
439 for (header, method_name) in plan.success_headers.iter().zip(header_method_names) {
440 tb = tb.add_method(build_java_header_getter(
441 header,
442 &method_name,
443 "this.raw.header",
444 ));
445 }
446
447 tb.build().expect("response class builds")
448}
449
450fn build_java_header_getter(
451 header: &ResponseHeaderPlan,
452 method_name: &str,
453 lookup: &str,
454) -> FunSpec {
455 let return_type = java_header_type(header.value_kind);
456 FunSpec::builder(method_name)
457 .visibility(Visibility::Public)
458 .returns(TypeName::primitive(return_type))
459 .body(java_header_accessor_body(
460 header.value_kind,
461 lookup,
462 &header.wire_name,
463 ))
464 .build()
465 .expect("Java header getter builds")
466}
467
468fn java_error_detail_interface(detail_interface: &str, permits: &[String]) -> CodeBlock {
469 let permits_clause = permits.join(", ");
470 CodeBlock::of(
471 &format!(
472 "public sealed interface {detail_interface} permits {permits_clause} {{\n int statusCode();\n okhttp3.Headers headers();\n byte[] rawBody();\n}}\n"
473 ),
474 (),
475 )
476 .expect("Java error detail interface builds")
477}
478
479fn java_error_exception_class(
480 error_type: &str,
481 detail_interface: &str,
482 headers: &[ResponseHeaderPlan],
483) -> CodeBlock {
484 let mut cb = CodeBlock::builder();
485 cb.add(
486 &format!(
487 "public final class {error_type} extends ApiException {{\n private final {detail_interface} detail;\n private final Headers headers;\n private final byte[] rawBody;\n\n public {error_type}(int statusCode, String status, String body, Headers headers, byte[] rawBody, {detail_interface} detail) {{\n super(statusCode, status, body);\n this.headers = headers;\n this.rawBody = rawBody.clone();\n this.detail = detail;\n }}\n\n public {detail_interface} detail() {{\n return this.detail;\n }}\n\n public Headers headers() {{\n return this.headers;\n }}\n\n public byte[] rawBody() {{\n return this.rawBody.clone();\n }}\n"
488 ),
489 (),
490 );
491 cb.add("%>", ());
492 let method_names = unique_response_header_accessor_names(headers, |wire_name| {
493 format!("get{}Header", wire_name.to_pascal_case())
494 });
495 for (header, method_name) in headers.iter().zip(method_names) {
496 cb.add_line();
497 cb.add_code(
498 build_java_header_getter(header, &method_name, "this.headers.get")
499 .emit(&Java::new(), DeclarationContext::Member)
500 .expect("Java error header getter emits"),
501 );
502 }
503 cb.add("%<}\n", ());
504 cb.build().expect("Java error exception class builds")
505}
506
507fn java_header_type(kind: ResponseHeaderValueKind) -> &'static str {
508 match kind {
509 ResponseHeaderValueKind::String => "String",
510 ResponseHeaderValueKind::Integer => "Long",
511 ResponseHeaderValueKind::Number => "Double",
512 ResponseHeaderValueKind::Boolean => "Boolean",
513 }
514}
515
516fn java_header_accessor_body(
517 kind: ResponseHeaderValueKind,
518 lookup: &str,
519 wire_name: &str,
520) -> CodeBlock {
521 let integer_pattern = r"[+-]?[0-9]+";
522 let number_pattern = r"[+-]?(?:[0-9]+(?:[.][0-9]*)?|[.][0-9]+)(?:[eE][+-]?[0-9]+)?";
523 match kind {
524 ResponseHeaderValueKind::String => sigil_quote!(Java {
525 return $L(lookup)($S(wire_name));
526 }),
527 ResponseHeaderValueKind::Integer => sigil_quote!(Java {
528 String value = $L(lookup)($S(wire_name));
529 if (value == null || !value.matches($S(integer_pattern))) {
530 return null;
531 }
532 try {
533 return Long.valueOf(value);
534 } catch (NumberFormatException ignored) {
535 return null;
536 }
537 }),
538 ResponseHeaderValueKind::Number => sigil_quote!(Java {
539 String value = $L(lookup)($S(wire_name));
540 if (value == null || !value.matches($S(number_pattern))) {
541 return null;
542 }
543 try {
544 Double parsed = Double.valueOf(value);
545 return Double.isFinite(parsed) ? parsed : null;
546 } catch (NumberFormatException ignored) {
547 return null;
548 }
549 }),
550 ResponseHeaderValueKind::Boolean => sigil_quote!(Java {
551 String value = $L(lookup)($S(wire_name));
552 if ($S("true").equals(value)) {
553 return true;
554 }
555 if ($S("false").equals(value)) {
556 return false;
557 }
558 return null;
559 }),
560 }
561 .expect("Java header getter body builds")
562}
563
564fn java_unexpected_detail_class(unexpected: &str, detail_interface: &str) -> CodeBlock {
565 CodeBlock::of(
566 &format!(
567 "public final class {unexpected} implements {detail_interface} {{\n private final int statusCode;\n private final okhttp3.Headers headers;\n private final byte[] rawBody;\n\n public {unexpected}(int statusCode, okhttp3.Headers headers, byte[] rawBody) {{\n this.statusCode = statusCode;\n this.headers = headers;\n this.rawBody = rawBody.clone();\n }}\n\n public int statusCode() {{ return this.statusCode; }}\n public okhttp3.Headers headers() {{ return this.headers; }}\n public byte[] rawBody() {{ return this.rawBody.clone(); }}\n public byte[] body() {{ return this.rawBody.clone(); }}\n}}\n"
568 ),
569 (),
570 )
571 .expect("Java unexpected detail class builds")
572}
573
574fn java_error_detail_class(
575 class_name: &str,
576 detail_interface: &str,
577 response: &TypedResponse,
578) -> CodeBlock {
579 let body_method = java_error_body_method(response);
580 let text_helper = java_error_text_helper(response);
581 let block = format!(
582 "public final class {class_name} implements {detail_interface} {{\n private final int statusCode;\n private final okhttp3.Headers headers;\n private final byte[] rawBody;\n\n public {class_name}(int statusCode, okhttp3.Headers headers, byte[] rawBody) {{\n this.statusCode = statusCode;\n this.headers = headers;\n this.rawBody = rawBody.clone();\n }}\n\n public int statusCode() {{ return this.statusCode; }}\n public okhttp3.Headers headers() {{ return this.headers; }}\n public byte[] rawBody() {{ return this.rawBody.clone(); }}\n{text_helper}{body_method}\n}}\n"
583 );
584 CodeBlock::of(&block, ()).expect("Java error detail class builds")
585}
586
587fn java_error_text_helper(response: &TypedResponse) -> &'static str {
588 match response.decoding {
589 ResponseDecoding::Json | ResponseDecoding::Text => {
590 "\n private String textBody() {\n String contentTypeHeader = this.headers.get(\"Content-Type\");\n okhttp3.MediaType contentType = contentTypeHeader != null ? okhttp3.MediaType.parse(contentTypeHeader) : null;\n java.nio.charset.Charset charset = contentType != null ? contentType.charset(StandardCharsets.UTF_8) : StandardCharsets.UTF_8;\n return new String(this.rawBody, charset);\n }\n"
591 }
592 ResponseDecoding::Bytes => "",
593 }
594}
595
596fn java_error_body_method(response: &TypedResponse) -> String {
597 match response.decoding {
598 ResponseDecoding::Json => format!(
599 " public {} body() {{\n String text = textBody();\n return new Gson().fromJson(text.isEmpty() ? \"null\" : text, new TypeToken<{}>() {{}}.getType());\n }}",
600 response.java_type, response.java_type
601 ),
602 ResponseDecoding::Text => {
603 " public String body() {\n return textBody();\n }".to_string()
604 }
605 ResponseDecoding::Bytes => {
606 " public byte[] body() {\n return this.rawBody.clone();\n }".to_string()
607 }
608 }
609}
610
611fn build_operation_fun(plan: &OpPlan<'_>) -> FunSpec {
616 let mut fb = FunSpec::builder(&plan.method_name);
617 fb = fb.visibility(Visibility::Public);
618
619 if let Some(summary) = &plan.op.summary {
620 fb = fb.doc(summary);
621 } else {
622 fb = fb.doc(&format!(
623 "{} {} {}.",
624 plan.method_name,
625 plan.op.method.to_uppercase(),
626 plan.op.path,
627 ));
628 }
629
630 for p in plan
632 .path_params
633 .iter()
634 .chain(&plan.query_params)
635 .chain(&plan.header_params)
636 {
637 fb = fb.add_param(
638 ParameterSpec::new(
639 &format!("{} {}", p.java_type, p.var_name),
640 TypeName::primitive(""),
641 )
642 .expect("param"),
643 );
644 }
645 if let Some(body) = &plan.body {
646 fb = fb.add_param(
647 ParameterSpec::new(
648 &format!("{} {}", body.java_type, body.var_name),
649 TypeName::primitive(""),
650 )
651 .expect("body param"),
652 );
653 }
654
655 fb = fb.returns(TypeName::primitive(&plan.response_type));
656 fb = fb.suffix("throws IOException");
657 fb = fb.body(emit_method_body(plan));
658
659 fb.build().expect("operation FunSpec builds")
660}
661
662fn emit_method_body(plan: &OpPlan<'_>) -> CodeBlock {
667 let mut cb = CodeBlock::builder();
668
669 let mut path_expr = format!("\"{}\"", plan.op.path);
671 for p in &plan.path_params {
672 let placeholder = format!("{{{}}}", p.param.name);
673 let stringified = render_value_as_string(&p.var_name, &p.param.type_expr);
674 path_expr = format!("{path_expr}.replace(\"{placeholder}\", {stringified})");
675 }
676 cb.add_statement(&format!("String path = {path_expr}"), ());
677
678 let has_query = !plan.query_params.is_empty();
680 if has_query {
681 cb.add_statement("Map<String, String> query = new HashMap<>()", ());
682 for p in &plan.query_params {
683 let stringified = render_value_as_string(&p.var_name, &p.param.type_expr);
684 cb.add_code(java_query_param_put(
685 p.param.required,
686 &p.var_name,
687 &p.param.name,
688 &stringified,
689 ));
690 }
691 }
692
693 let method = plan.op.method.to_uppercase();
695 if let Some(body) = &plan.body {
696 if let Some(message) = unsupported_request_body_message(body) {
697 cb.add_code(
698 sigil_quote!(Java {
699 throw new IllegalArgumentException($S(message));
700 })
701 .expect("unsupported request body builds"),
702 );
703 return cb.build().expect("method body builds");
704 }
705 cb.add_statement("Request request", ());
706 if body.encoding == BodyEncoding::Multipart {
707 if let Some(parts) = &body.multipart_parts {
708 emit_multipart_body(&mut cb, body, parts);
709 cb.add_code(java_new_request_with_body(
710 &method,
711 has_query,
712 "multipartBody",
713 ));
714 } else {
715 cb.add_statement(
716 "throw new IllegalArgumentException(\"unsupported multipart request body: schema must be object-shaped\")",
717 (),
718 );
719 }
720 } else {
721 emit_request_body(&mut cb, body);
722 cb.add_code(java_new_request_with_body(
723 &method,
724 has_query,
725 "requestBody",
726 ));
727 }
728 } else {
729 cb.add_code(java_new_request(&method, has_query));
730 }
731
732 for p in &plan.header_params {
734 let stringified = render_value_as_string(&p.var_name, &p.param.type_expr);
735 cb.add_code(java_header_param_set(
736 p.param.required,
737 &p.var_name,
738 &p.param.name,
739 &stringified,
740 ));
741 }
742
743 cb.add_statement("Response response = client.execute(request)", ());
745 cb.add_line();
746 cb.add_code(java_error_throw(plan));
747
748 if !plan.typed_responses.is_empty() {
750 cb.add_statement(
751 "byte[] responseBytes = response.body() != null ? response.body().bytes() : new byte[0]",
752 (),
753 );
754 cb.add_statement(
755 "String responseText = new String(responseBytes, StandardCharsets.UTF_8)",
756 (),
757 );
758 let mut seen: HashSet<String> = HashSet::new();
759
760 for tr in &plan.typed_responses {
762 if !seen.insert(tr.field_name.clone()) {
763 continue;
764 }
765 cb.add_statement(&format!("{} {} = null", tr.java_type, tr.field_name), ());
766 }
767 cb.add_code(java_response_decode_assignments(&plan.typed_responses));
768
769 let args: Vec<String> = std::iter::once("response.code()".to_string())
771 .chain(std::iter::once("response".to_string()))
772 .chain(plan.typed_responses.iter().map(|tr| tr.field_name.clone()))
773 .collect();
774 let mut dedup_args: Vec<String> = Vec::new();
776 let mut args_seen: HashSet<String> = HashSet::new();
777 for a in args {
778 if args_seen.insert(a.clone()) {
779 dedup_args.push(a);
780 }
781 }
782 cb.add_code(
783 sigil_quote!(Java {
784 return new $N(plan.response_type.as_str())($for(arg in &dedup_args; separator = ", ") { $L(arg.as_str()) });
785 })
786 .expect("typed response constructor return"),
787 );
788 } else {
789 cb.add_statement(
790 &format!(
791 "return new {}(response.code(), response)",
792 plan.response_type
793 ),
794 (),
795 );
796 }
797
798 cb.build().expect("method body builds")
799}
800
801fn unsupported_request_body_message(body: &BodyBinding) -> Option<String> {
802 if body.encoding == BodyEncoding::Multipart && body.multipart_parts.is_none() {
803 return Some(
804 "unsupported multipart request body: schema must be object-shaped".to_string(),
805 );
806 }
807 match body.encoding {
808 BodyEncoding::FormUrlEncoded | BodyEncoding::Xml | BodyEncoding::Other => Some(format!(
809 "unsupported request body media type: {}",
810 body.media_type
811 )),
812 _ => None,
813 }
814}
815
816fn java_new_request(method: &str, has_query: bool) -> CodeBlock {
817 let with_query =
818 format!("Request request = client.newRequest(\"{method}\", path, query, null);");
819 let without_query =
820 format!("Request request = client.newRequest(\"{method}\", path, null, null);");
821 sigil_quote!(Java {
822 $if(has_query) {
823 $L(with_query.as_str())
824 } $else {
825 $L(without_query.as_str())
826 }
827 })
828 .expect("Java request construction builds")
829}
830
831fn java_new_request_with_body(method: &str, has_query: bool, body_expr: &str) -> CodeBlock {
832 let with_query =
833 format!("request = client.newRequestWithBody(\"{method}\", path, query, {body_expr});");
834 let without_query =
835 format!("request = client.newRequestWithBody(\"{method}\", path, null, {body_expr});");
836 sigil_quote!(Java {
837 $if(has_query) {
838 $L(with_query.as_str())
839 } $else {
840 $L(without_query.as_str())
841 }
842 })
843 .expect("Java request body construction builds")
844}
845
846fn java_query_param_put(
847 required: bool,
848 var_name: &str,
849 param_name: &str,
850 value_expr: &str,
851) -> CodeBlock {
852 sigil_quote!(Java {
853 $if(required) {
854 query.put($S(param_name), $L(value_expr));
855 } $else {
856 if ($L(var_name) != null) {
857 query.put($S(param_name), $L(value_expr));
858 }
859 }
860 })
861 .expect("Java query param put builds")
862}
863
864fn java_header_param_set(
865 required: bool,
866 var_name: &str,
867 param_name: &str,
868 value_expr: &str,
869) -> CodeBlock {
870 sigil_quote!(Java {
871 $if(required) {
872 request = request.newBuilder().header($S(param_name), $L(value_expr)).build();
873 } $else {
874 if ($L(var_name) != null) {
875 request = request.newBuilder().header($S(param_name), $L(value_expr)).build();
876 }
877 }
878 })
879 .expect("Java header param set builds")
880}
881
882fn emit_multipart_body(
883 cb: &mut sigil_stitch::code_block::CodeBlockBuilder,
884 body: &BodyBinding,
885 parts: &[MultipartPart],
886) {
887 if !body.required {
888 cb.add_code(
889 sigil_quote!(Java {
890 RequestBody multipartBody = RequestBody.create(new byte[0], null);
891 })
892 .expect("default multipart body builds"),
893 );
894 cb.begin_control_flow(&format!("if ({} != null)", body.var_name), ());
895 }
896 cb.add_code(
897 sigil_quote!(Java {
898 MultipartBody.Builder multipartBuilder = new MultipartBody.Builder().setType(MultipartBody.FORM);
899 })
900 .expect("multipart builder builds"),
901 );
902 for part in parts {
903 let access = format!(
904 "{}.get{}()",
905 body.var_name,
906 part.field_name.to_pascal_case()
907 );
908 if part.required {
909 emit_required_multipart_part(cb, part, &access);
910 } else {
911 cb.begin_control_flow(&format!("if ({access} != null)"), ());
912 emit_required_multipart_part(cb, part, &access);
913 cb.end_control_flow();
914 }
915 }
916 cb.add_code(java_multipart_body_finish(body.required));
917 if !body.required {
918 cb.end_control_flow();
919 }
920}
921
922fn java_multipart_body_finish(body_required: bool) -> CodeBlock {
923 sigil_quote!(Java {
924 $if(body_required) {
925 RequestBody multipartBody = multipartBuilder.build();
926 } $else {
927 multipartBody = multipartBuilder.build();
928 }
929 })
930 .expect("multipart body finish builds")
931}
932
933fn emit_request_body(cb: &mut sigil_stitch::code_block::CodeBlockBuilder, body: &BodyBinding) {
934 if !body.required {
935 cb.add_code(
936 sigil_quote!(Java {
937 RequestBody requestBody = RequestBody.create(new byte[0], null);
938 })
939 .expect("default request body builds"),
940 );
941 cb.begin_control_flow(&format!("if ({} != null)", body.var_name), ());
942 }
943 match body.encoding {
944 BodyEncoding::Json => {
945 let body_var = body.var_name.as_str();
946 let media_type = body.media_type.as_str();
947 cb.add_code(java_json_request_body(body.required, body_var, media_type));
948 }
949 BodyEncoding::TextPlain | BodyEncoding::OctetStream => {
950 let body_var = body.var_name.as_str();
951 let media_type = body.media_type.as_str();
952 cb.add_code(java_raw_request_body(body.required, body_var, media_type));
953 }
954 BodyEncoding::FormUrlEncoded | BodyEncoding::Xml | BodyEncoding::Other => {
955 let message = format!("unsupported request body media type: {}", body.media_type);
956 cb.add_code(
957 sigil_quote!(Java {
958 throw new IllegalArgumentException($S(message));
959 })
960 .expect("unsupported request body builds"),
961 );
962 }
963 BodyEncoding::Multipart => unreachable!("multipart handled separately"),
964 }
965 if !body.required {
966 cb.end_control_flow();
967 }
968}
969
970fn java_json_request_body(body_required: bool, body_var: &str, media_type: &str) -> CodeBlock {
971 sigil_quote!(Java {
972 String jsonBody = gson.toJson($L(body_var));
973 $if(body_required) {
974 RequestBody requestBody = RequestBody.create(jsonBody, MediaType.get($S(media_type)));
975 } $else {
976 requestBody = RequestBody.create(jsonBody, MediaType.get($S(media_type)));
977 }
978 })
979 .expect("json request body builds")
980}
981
982fn java_raw_request_body(body_required: bool, body_var: &str, media_type: &str) -> CodeBlock {
983 sigil_quote!(Java {
984 $if(body_required) {
985 RequestBody requestBody = RequestBody.create($L(body_var), MediaType.get($S(media_type)));
986 } $else {
987 requestBody = RequestBody.create($L(body_var), MediaType.get($S(media_type)));
988 }
989 })
990 .expect("raw request body builds")
991}
992
993fn response_decode_expr(tr: &TypedResponse) -> String {
994 match tr.decoding {
995 ResponseDecoding::Json => {
996 let type_token = format!("new TypeToken<{}>() {{}}.getType()", tr.java_type);
997 format!("gson.fromJson(responseText.isEmpty() ? \"null\" : responseText, {type_token})")
998 }
999 ResponseDecoding::Text => "responseText".to_string(),
1000 ResponseDecoding::Bytes => "responseBytes".to_string(),
1001 }
1002}
1003
1004fn java_response_decode_assignments(typed_responses: &[TypedResponse]) -> CodeBlock {
1005 let mut cb = CodeBlock::builder();
1006 let mut seen: HashSet<String> = HashSet::new();
1007 let mut emitted_any = false;
1008 for tr in typed_responses {
1009 if !seen.insert(tr.field_name.clone()) {
1010 continue;
1011 }
1012 let keyword = if emitted_any { "else if" } else { "if" };
1013 let guard = response_status_guard_java(&tr.status);
1014 let assignment = format!("{} = {}", tr.field_name, response_decode_expr(tr));
1015 cb.begin_control_flow(&format!("{keyword} ({guard})"), ());
1016 cb.add(&format!("{assignment};\n"), ());
1017 cb.end_control_flow();
1018 emitted_any = true;
1019 }
1020 cb.build().expect("Java response decode assignments build")
1021}
1022
1023fn java_error_throw(plan: &OpPlan<'_>) -> CodeBlock {
1024 let mut cb = CodeBlock::builder();
1025 cb.begin_control_flow("if (!response.isSuccessful())", ());
1026 cb.add_statement("okhttp3.ResponseBody responseBody = response.body()", ());
1027 cb.add_statement(
1028 "java.nio.charset.Charset responseCharset = responseBody != null && responseBody.contentType() != null ? responseBody.contentType().charset(StandardCharsets.UTF_8) : StandardCharsets.UTF_8",
1029 (),
1030 );
1031 cb.add_statement(
1032 "byte[] responseBytes = responseBody != null ? responseBody.bytes() : new byte[0]",
1033 (),
1034 );
1035 cb.add_statement(
1036 "String responseText = new String(responseBytes, responseCharset)",
1037 (),
1038 );
1039 cb.add(&format!("{}Detail detail;\n", plan.error_type), ());
1040 cb.begin_control_flow("switch (response.code())", ());
1041 let mut seen = HashSet::new();
1042 for response in plan
1043 .error_responses
1044 .iter()
1045 .filter(|response| response.status.parse::<u16>().is_ok())
1046 {
1047 if !seen.insert(response.field_name.clone()) {
1048 continue;
1049 }
1050 cb.add(&format!("case {}:\n", response.status), ());
1051 cb.add("%>", ());
1052 cb.add_code(java_error_detail_assign(plan, response));
1053 cb.add("break;\n", ());
1054 cb.add("%<", ());
1055 }
1056 cb.add("default:\n", ());
1057 cb.add("%>", ());
1058 for response in plan
1059 .error_responses
1060 .iter()
1061 .filter(|response| response.status.ends_with("XX"))
1062 {
1063 cb.begin_control_flow(
1064 &format!("if ({})", wildcard_status_guard_java(&response.status)),
1065 (),
1066 );
1067 cb.add_code(java_error_detail_assign(plan, response));
1068 cb.add("break;\n", ());
1069 cb.end_control_flow();
1070 }
1071 if let Some(default) = plan
1072 .error_responses
1073 .iter()
1074 .find(|response| response.status.eq_ignore_ascii_case("default"))
1075 {
1076 cb.add_code(java_error_detail_assign(plan, default));
1077 } else {
1078 let unexpected = format!("{}Unexpected", plan.method_name.to_pascal_case());
1079 cb.add(
1080 &format!(
1081 "detail = new {unexpected}(response.code(), response.headers(), responseBytes);\n"
1082 ),
1083 (),
1084 );
1085 }
1086 cb.add("%<", ());
1087 cb.end_control_flow();
1088 cb.add(
1089 &format!(
1090 "throw new {}(response.code(), response.message(), responseText, response.headers(), responseBytes, detail);\n",
1091 plan.error_type
1092 ),
1093 (),
1094 );
1095 cb.end_control_flow();
1096 cb.build().expect("Java error throw builds")
1097}
1098
1099fn java_error_detail_assign(plan: &OpPlan<'_>, response: &TypedResponse) -> CodeBlock {
1100 let class_name = format!(
1101 "{}{}",
1102 plan.method_name.to_pascal_case(),
1103 response.field_name
1104 );
1105 let stmt =
1106 format!("detail = new {class_name}(response.code(), response.headers(), responseBytes);");
1107 CodeBlock::of(&stmt, ()).expect("Java error detail assign builds")
1108}
1109
1110fn emit_required_multipart_part(
1111 cb: &mut sigil_stitch::code_block::CodeBlockBuilder,
1112 part: &MultipartPart,
1113 access: &str,
1114) {
1115 cb.add_code(java_multipart_part(part, access));
1116}
1117
1118fn java_multipart_part(part: &MultipartPart, access: &str) -> CodeBlock {
1119 let wire_name = part.wire_name.as_str();
1120 let content_type = part.content_type.as_str();
1121 sigil_quote!(Java {
1122 $if(part.is_binary) {
1123 multipartBuilder.addFormDataPart($S(wire_name), $L(access).filenameOrDefault($S(wire_name)), RequestBody.create($L(access).getData(), MediaType.get($S(content_type))));
1124 } $else_if(part.value_encoding == MultipartValueEncoding::Json) {
1125 multipartBuilder.addFormDataPart($S(wire_name), null, RequestBody.create(gson.toJson($L(access)), MediaType.get($S(content_type))));
1126 } $else_if(part.value_encoding == MultipartValueEncoding::Unsupported) {
1127 throw new IllegalArgumentException($S("unsupported multipart part content type"));
1128 } $else {
1129 multipartBuilder.addFormDataPart($S(wire_name), null, RequestBody.create(String.valueOf($L(access)), MediaType.get($S(content_type))));
1130 }
1131 })
1132 .expect("multipart part block builds")
1133}
1134
1135struct OpPlan<'a> {
1140 op: &'a IrOperation,
1141 method_name: String,
1142 response_type: String,
1143 error_type: String,
1144 path_params: Vec<ParamBinding<'a>>,
1145 query_params: Vec<ParamBinding<'a>>,
1146 header_params: Vec<ParamBinding<'a>>,
1147 body: Option<BodyBinding>,
1148 typed_responses: Vec<TypedResponse>,
1149 error_responses: Vec<TypedResponse>,
1150 success_headers: Vec<ResponseHeaderPlan>,
1151 error_headers: Vec<ResponseHeaderPlan>,
1152}
1153
1154struct ParamBinding<'a> {
1155 param: &'a IrParameter,
1156 var_name: String,
1157 java_type: String,
1158}
1159
1160struct BodyBinding {
1161 var_name: String,
1162 java_type: String,
1163 media_type: String,
1164 required: bool,
1165 encoding: BodyEncoding,
1166 multipart_parts: Option<Vec<MultipartPart>>,
1167}
1168
1169struct MultipartPart {
1170 wire_name: String,
1171 field_name: String,
1172 is_binary: bool,
1173 required: bool,
1174 content_type: String,
1175 value_encoding: MultipartValueEncoding,
1176}
1177
1178#[derive(Clone, Copy, PartialEq, Eq)]
1179enum BodyEncoding {
1180 Json,
1181 Multipart,
1182 FormUrlEncoded,
1183 Xml,
1184 TextPlain,
1185 OctetStream,
1186 Other,
1187}
1188
1189#[derive(Clone, Copy, PartialEq, Eq)]
1190enum ResponseDecoding {
1191 Json,
1192 Text,
1193 Bytes,
1194}
1195
1196struct TypedResponse {
1197 status: String,
1198 field_name: String,
1199 java_type: String,
1200 decoding: ResponseDecoding,
1201}
1202
1203fn plan_operation<'a>(
1204 op: &'a IrOperation,
1205 ir: &IrSpec,
1206 request_inputs: &RequestInputPlan,
1207) -> OpPlan<'a> {
1208 let op_id = sanitize_operation_id(&op.operation_id, &op.method, &op.path);
1209 let method_name = op_id.to_lower_camel_case();
1210 let response_type = format!("{}Response", op_id.to_pascal_case());
1211 let error_type = format!("{}Exception", op_id.to_pascal_case());
1212
1213 let mut used_names: HashSet<String> = HashSet::new();
1214
1215 let mut path_params = Vec::new();
1216 let mut query_params = Vec::new();
1217 let mut header_params = Vec::new();
1218 for p in &op.parameters {
1219 let var_name = unique_name(&java_ident(&p.name), &mut used_names);
1220 let java_type = if p.required {
1221 java_type_str(&p.type_expr)
1222 } else {
1223 java_boxed_type_str(&p.type_expr)
1224 };
1225 let binding = ParamBinding {
1226 param: p,
1227 var_name,
1228 java_type,
1229 };
1230 match p.location {
1231 ParameterLocation::Path => path_params.push(binding),
1232 ParameterLocation::Query => query_params.push(binding),
1233 ParameterLocation::Header => header_params.push(binding),
1234 ParameterLocation::Cookie => header_params.push(binding),
1235 }
1236 }
1237
1238 let body = op
1239 .request_body
1240 .as_ref()
1241 .and_then(|b| plan_body(op, b, ir, request_inputs, &mut used_names));
1242
1243 let mut typed_responses: Vec<TypedResponse> = op
1244 .responses
1245 .iter()
1246 .filter(|r| is_success_status(&r.status))
1247 .filter_map(plan_response)
1248 .collect();
1249 typed_responses.sort_by_key(|r| response_match_rank(&r.status));
1250 let error_responses = op
1251 .responses
1252 .iter()
1253 .filter(|r| !is_success_status(&r.status))
1254 .map(plan_error_response)
1255 .collect();
1256 let success_headers = collect_response_headers(
1257 op.responses
1258 .iter()
1259 .filter(|response| is_success_status(&response.status)),
1260 ir,
1261 );
1262 let error_headers = collect_response_headers(
1263 op.responses
1264 .iter()
1265 .filter(|response| !is_success_status(&response.status)),
1266 ir,
1267 );
1268
1269 OpPlan {
1270 op,
1271 method_name,
1272 response_type,
1273 error_type,
1274 path_params,
1275 query_params,
1276 header_params,
1277 body,
1278 typed_responses,
1279 error_responses,
1280 success_headers,
1281 error_headers,
1282 }
1283}
1284
1285fn plan_body(
1286 op: &IrOperation,
1287 b: &IrRequestBody,
1288 ir: &IrSpec,
1289 request_inputs: &RequestInputPlan,
1290 used_names: &mut HashSet<String>,
1291) -> Option<BodyBinding> {
1292 let (media_type, t) = pick_body_content(b)?;
1293 let encoding = body_encoding(&media_type);
1294 let java_type = match encoding {
1295 BodyEncoding::TextPlain => "String".to_string(),
1296 BodyEncoding::OctetStream => "byte[]".to_string(),
1297 BodyEncoding::Multipart => request_input_for_operation(request_inputs, op, &media_type)
1298 .map(|input| input.name.to_pascal_case())
1299 .unwrap_or_else(|| java_type_str(&t)),
1300 _ => java_type_str(&t),
1301 };
1302 let var_name = unique_name("body", used_names);
1303 let multipart_parts = if media_type_base(&media_type) == "multipart/form-data" {
1304 multipart_parts_for(b, &media_type, ir)
1305 } else {
1306 None
1307 };
1308 Some(BodyBinding {
1309 var_name,
1310 java_type,
1311 media_type,
1312 required: b.required,
1313 encoding,
1314 multipart_parts,
1315 })
1316}
1317
1318fn plan_response(r: &IrResponse) -> Option<TypedResponse> {
1319 let (media_type, t) = pick_response_content(r)?;
1320 let decoding = response_decoding(&media_type);
1321 let java_type = match decoding {
1322 ResponseDecoding::Json => java_type_str(&t),
1323 ResponseDecoding::Text => "String".to_string(),
1324 ResponseDecoding::Bytes => "byte[]".to_string(),
1325 };
1326 Some(TypedResponse {
1327 status: r.status.clone(),
1328 field_name: response_field_name(&r.status),
1329 java_type,
1330 decoding,
1331 })
1332}
1333
1334fn plan_error_response(r: &IrResponse) -> TypedResponse {
1335 match pick_response_content(r) {
1336 Some((media_type, t)) => {
1337 let decoding = response_decoding(&media_type);
1338 let java_type = match decoding {
1339 ResponseDecoding::Json => java_type_str(&t),
1340 ResponseDecoding::Text => "String".to_string(),
1341 ResponseDecoding::Bytes => "byte[]".to_string(),
1342 };
1343 TypedResponse {
1344 status: r.status.clone(),
1345 field_name: response_variant_name(&r.status),
1346 java_type,
1347 decoding,
1348 }
1349 }
1350 None => TypedResponse {
1351 status: r.status.clone(),
1352 field_name: response_variant_name(&r.status),
1353 java_type: "byte[]".to_string(),
1354 decoding: ResponseDecoding::Bytes,
1355 },
1356 }
1357}
1358
1359fn is_success_status(status: &str) -> bool {
1360 status
1361 .parse::<u16>()
1362 .is_ok_and(|code| (200..300).contains(&code))
1363 || status.eq_ignore_ascii_case("2XX")
1364}
1365
1366fn response_field_name(status: &str) -> String {
1367 if status == "default" {
1368 "default_".to_string()
1369 } else if let Ok(code) = status.parse::<u16>() {
1370 format!("status{code}")
1371 } else {
1372 format!("status{}", status.to_lowercase())
1373 }
1374}
1375
1376fn wildcard_status_guard_java(status: &str) -> String {
1377 let upper = status.to_uppercase();
1378 if upper == "4XX" {
1379 "response.code() >= 400 && response.code() < 500".to_string()
1380 } else if upper == "5XX" {
1381 "response.code() >= 500 && response.code() < 600".to_string()
1382 } else if upper == "2XX" {
1383 "response.code() >= 200 && response.code() < 300".to_string()
1384 } else {
1385 "true".to_string()
1387 }
1388}
1389
1390fn response_status_guard_java(status: &str) -> String {
1391 status
1392 .parse::<u16>()
1393 .map(|code| format!("response.code() == {code}"))
1394 .unwrap_or_else(|_| wildcard_status_guard_java(status))
1395}
1396
1397fn body_encoding(media_type: &str) -> BodyEncoding {
1398 let base = media_type_base(media_type);
1399 if base == "multipart/form-data" {
1400 BodyEncoding::Multipart
1401 } else if is_json_media_type(media_type) {
1402 BodyEncoding::Json
1403 } else if base == "application/x-www-form-urlencoded" {
1404 BodyEncoding::FormUrlEncoded
1405 } else if is_xml_media_type(media_type) {
1406 BodyEncoding::Xml
1407 } else if base == "text/plain" {
1408 BodyEncoding::TextPlain
1409 } else if base == "application/octet-stream" {
1410 BodyEncoding::OctetStream
1411 } else {
1412 BodyEncoding::Other
1413 }
1414}
1415
1416fn response_decoding(media_type: &str) -> ResponseDecoding {
1417 let base = media_type_base(media_type);
1418 if is_json_media_type(media_type) {
1419 ResponseDecoding::Json
1420 } else if base == "text/plain" || is_xml_media_type(media_type) {
1421 ResponseDecoding::Text
1422 } else {
1423 ResponseDecoding::Bytes
1424 }
1425}
1426
1427fn pick_body_content(body: &IrRequestBody) -> Option<(String, IrTypeExpr)> {
1428 pick_media_type(&body.content, |media_type| {
1429 media_type_base(media_type) == "application/json"
1430 })
1431 .or_else(|| pick_media_type(&body.content, is_json_media_type))
1432 .or_else(|| {
1433 pick_media_type(&body.content, |media_type| {
1434 media_type_base(media_type) == "multipart/form-data"
1435 })
1436 })
1437 .or_else(|| {
1438 pick_media_type(&body.content, |media_type| {
1439 media_type_base(media_type) == "application/x-www-form-urlencoded"
1440 })
1441 })
1442 .or_else(|| pick_media_type(&body.content, is_xml_media_type))
1443 .or_else(|| {
1444 pick_media_type(&body.content, |media_type| {
1445 media_type_base(media_type) == "text/plain"
1446 })
1447 })
1448 .or_else(|| {
1449 pick_media_type(&body.content, |media_type| {
1450 media_type_base(media_type) == "application/octet-stream"
1451 })
1452 })
1453 .or_else(|| pick_first_content(&body.content))
1454}
1455
1456fn pick_response_content(r: &IrResponse) -> Option<(String, IrTypeExpr)> {
1457 pick_media_type(&r.content, |media_type| {
1458 media_type_base(media_type) == "application/json"
1459 })
1460 .or_else(|| pick_media_type(&r.content, is_json_media_type))
1461 .or_else(|| {
1462 pick_media_type(&r.content, |media_type| {
1463 media_type_base(media_type) == "application/octet-stream"
1464 })
1465 })
1466 .or_else(|| {
1467 pick_media_type(&r.content, |media_type| {
1468 media_type_base(media_type) == "text/plain"
1469 })
1470 })
1471 .or_else(|| pick_media_type(&r.content, is_xml_media_type))
1472 .or_else(|| pick_first_content(&r.content))
1473}
1474
1475fn pick_media_type(
1476 content: &indexmap::IndexMap<String, IrTypeExpr>,
1477 predicate: impl Fn(&str) -> bool,
1478) -> Option<(String, IrTypeExpr)> {
1479 content
1480 .iter()
1481 .find(|(media_type, _)| predicate(media_type))
1482 .map(|(media_type, t)| (media_type.clone(), t.clone()))
1483}
1484
1485fn pick_first_content(
1486 content: &indexmap::IndexMap<String, IrTypeExpr>,
1487) -> Option<(String, IrTypeExpr)> {
1488 content
1489 .iter()
1490 .next()
1491 .map(|(media_type, t)| (media_type.clone(), t.clone()))
1492}
1493
1494fn media_type_base(media_type: &str) -> String {
1495 media_type
1496 .split(';')
1497 .next()
1498 .unwrap_or(media_type)
1499 .trim()
1500 .to_ascii_lowercase()
1501}
1502
1503fn is_json_media_type(media_type: &str) -> bool {
1504 let base = media_type_base(media_type);
1505 base == "application/json" || base.ends_with("+json")
1506}
1507
1508fn is_xml_media_type(media_type: &str) -> bool {
1509 let base = media_type_base(media_type);
1510 base == "application/xml" || base == "text/xml" || base.ends_with("+xml")
1511}
1512
1513fn multipart_parts_for(
1514 body: &IrRequestBody,
1515 media_type: &str,
1516 ir: &IrSpec,
1517) -> Option<Vec<MultipartPart>> {
1518 multipart_parts_for_request_body(body, media_type, ir).map(|parts| {
1519 parts
1520 .into_iter()
1521 .map(|part| MultipartPart {
1522 field_name: java_field_name(&part.wire_name),
1523 wire_name: part.wire_name,
1524 is_binary: part.is_binary,
1525 required: part.required,
1526 content_type: part.content_type,
1527 value_encoding: part.value_encoding,
1528 })
1529 .collect()
1530 })
1531}