From f20ec73ee3059eabdf8cce843ce8f9a5eabb722a Mon Sep 17 00:00:00 2001 From: Adam Basfop Cavendish Date: Wed, 17 Jun 2026 05:47:18 +0800 Subject: [PATCH 1/2] refactor: quote OkHttp request body emitters - Migrate Java OkHttp request and multipart body setup to sigil_quote - Migrate Kotlin OkHttp request and multipart body setup to sigil_quote - Preserve generated Java and Kotlin golden output --- src/generators/java/okhttp/sigil_emit_api.rs | 109 +++++++++------- .../kotlin/okhttp/sigil_emit_api.rs | 116 +++++++++++------- 2 files changed, 137 insertions(+), 88 deletions(-) diff --git a/src/generators/java/okhttp/sigil_emit_api.rs b/src/generators/java/okhttp/sigil_emit_api.rs index 774712be..9dd300a2 100644 --- a/src/generators/java/okhttp/sigil_emit_api.rs +++ b/src/generators/java/okhttp/sigil_emit_api.rs @@ -511,13 +511,20 @@ fn emit_multipart_body( parts: &[MultipartPart], ) { if !body.required { - cb.add_statement( - "RequestBody multipartBody = RequestBody.create(new byte[0], null)", - (), + cb.add_code( + sigil_quote!(Java { + RequestBody multipartBody = RequestBody.create(new byte[0], null); + }) + .expect("default multipart body builds"), ); cb.begin_control_flow(&format!("if ({} != null)", body.var_name), ()); } - cb.add_statement("MultipartBody.Builder multipartBuilder = new MultipartBody.Builder().setType(MultipartBody.FORM)", ()); + cb.add_code( + sigil_quote!(Java { + MultipartBody.Builder multipartBuilder = new MultipartBody.Builder().setType(MultipartBody.FORM); + }) + .expect("multipart builder builds"), + ); for part in parts { let access = format!( "{}.get{}()", @@ -533,61 +540,81 @@ fn emit_multipart_body( } } if body.required { - cb.add_statement("RequestBody multipartBody = multipartBuilder.build()", ()); + cb.add_code( + sigil_quote!(Java { + RequestBody multipartBody = multipartBuilder.build(); + }) + .expect("required multipart body builds"), + ); } else { - cb.add_statement("multipartBody = multipartBuilder.build()", ()); + cb.add_code( + sigil_quote!(Java { + multipartBody = multipartBuilder.build(); + }) + .expect("optional multipart body builds"), + ); cb.end_control_flow(); } } fn emit_request_body(cb: &mut sigil_stitch::code_block::CodeBlockBuilder, body: &BodyBinding) { if !body.required { - cb.add_statement( - "RequestBody requestBody = RequestBody.create(new byte[0], null)", - (), + cb.add_code( + sigil_quote!(Java { + RequestBody requestBody = RequestBody.create(new byte[0], null); + }) + .expect("default request body builds"), ); cb.begin_control_flow(&format!("if ({} != null)", body.var_name), ()); } match body.encoding { BodyEncoding::Json => { - cb.add_statement( - &format!("String jsonBody = gson.toJson({})", body.var_name), - (), - ); - let prefix = if body.required { - "RequestBody requestBody =" + let body_var = body.var_name.as_str(); + let media_type = body.media_type.as_str(); + if body.required { + cb.add_code( + sigil_quote!(Java { + String jsonBody = gson.toJson($L(body_var)); + RequestBody requestBody = RequestBody.create(jsonBody, MediaType.get($S(media_type))); + }) + .expect("required json request body builds"), + ); } else { - "requestBody =" - }; - cb.add_statement( - &format!( - "{prefix} RequestBody.create(jsonBody, MediaType.get(\"{}\"))", - body.media_type - ), - (), - ); + cb.add_code( + sigil_quote!(Java { + String jsonBody = gson.toJson($L(body_var)); + requestBody = RequestBody.create(jsonBody, MediaType.get($S(media_type))); + }) + .expect("optional json request body builds"), + ); + } } BodyEncoding::TextPlain | BodyEncoding::OctetStream => { - let prefix = if body.required { - "RequestBody requestBody =" + let body_var = body.var_name.as_str(); + let media_type = body.media_type.as_str(); + if body.required { + cb.add_code( + sigil_quote!(Java { + RequestBody requestBody = RequestBody.create($L(body_var), MediaType.get($S(media_type))); + }) + .expect("required raw request body builds"), + ); } else { - "requestBody =" - }; - cb.add_statement( - &format!( - "{prefix} RequestBody.create({}, MediaType.get(\"{}\"))", - body.var_name, body.media_type - ), - (), - ); + cb.add_code( + sigil_quote!(Java { + requestBody = RequestBody.create($L(body_var), MediaType.get($S(media_type))); + }) + .expect("optional raw request body builds"), + ); + } } BodyEncoding::FormUrlEncoded | BodyEncoding::Xml | BodyEncoding::Other => { - cb.add_statement( - &format!( - "throw new IllegalArgumentException(\"unsupported request body media type: {}\")", - body.media_type - ), - (), + let message = format!("unsupported request body media type: {}", body.media_type); + cb.add_code( + sigil_quote!(Java { + throw new IllegalArgumentException($S(message)); + }) + .expect("unsupported request body builds"), ); } BodyEncoding::Multipart => unreachable!("multipart handled separately"), diff --git a/src/generators/kotlin/okhttp/sigil_emit_api.rs b/src/generators/kotlin/okhttp/sigil_emit_api.rs index 64ba7c47..82b63762 100644 --- a/src/generators/kotlin/okhttp/sigil_emit_api.rs +++ b/src/generators/kotlin/okhttp/sigil_emit_api.rs @@ -443,15 +443,20 @@ fn emit_multipart_body( parts: &[MultipartPart], ) { if !body.required { - cb.add("var multipartBody = ByteArray(0).toRequestBody(null)", ()); - cb.add_line(); + cb.add_code( + sigil_quote!(Kotlin { + var multipartBody = ByteArray(0).toRequestBody(null) + }) + .expect("default multipart body builds"), + ); cb.begin_control_flow(&format!("if ({} != null)", body.var_name), ()); } - cb.add( - "val multipartBuilder = MultipartBody.Builder().setType(MultipartBody.FORM)", - (), + cb.add_code( + sigil_quote!(Kotlin { + val multipartBuilder = MultipartBody.Builder().setType(MultipartBody.FORM) + }) + .expect("multipart builder builds"), ); - cb.add_line(); for part in parts { let access = format!("{}.{}", body.var_name, part.field_name); if part.required { @@ -463,11 +468,20 @@ fn emit_multipart_body( } } if body.required { - cb.add("val multipartBody = multipartBuilder.build()", ()); + cb.add_code( + sigil_quote!(Kotlin { + val multipartBody = multipartBuilder.build() + }) + .expect("required multipart body builds"), + ); } else { - cb.add("multipartBody = multipartBuilder.build()", ()); + cb.add_code( + sigil_quote!(Kotlin { + multipartBody = multipartBuilder.build() + }) + .expect("optional multipart body builds"), + ); } - cb.add_line(); if !body.required { cb.end_control_flow(); } @@ -475,55 +489,63 @@ fn emit_multipart_body( fn emit_request_body(cb: &mut sigil_stitch::code_block::CodeBlockBuilder, body: &BodyBinding) { if !body.required { - cb.add("var requestBody = ByteArray(0).toRequestBody(null)", ()); - cb.add_line(); + cb.add_code( + sigil_quote!(Kotlin { + var requestBody = ByteArray(0).toRequestBody(null) + }) + .expect("default request body builds"), + ); cb.begin_control_flow(&format!("if ({} != null)", body.var_name), ()); } match body.encoding { BodyEncoding::Json => { - cb.add( - &format!("val jsonBody = gson.toJson({})", body.var_name), - (), - ); - cb.add_line(); - let prefix = if body.required { - "val requestBody =" + let body_var = body.var_name.as_str(); + let media_type = body.media_type.as_str(); + if body.required { + cb.add_code( + sigil_quote!(Kotlin { + val jsonBody = gson.toJson($L(body_var)) + val requestBody = jsonBody.toRequestBody($S(media_type).toMediaType()) + }) + .expect("required json request body builds"), + ); } else { - "requestBody =" - }; - cb.add( - &format!( - "{prefix} jsonBody.toRequestBody(\"{}\".toMediaType())", - body.media_type - ), - (), - ); - cb.add_line(); + cb.add_code( + sigil_quote!(Kotlin { + val jsonBody = gson.toJson($L(body_var)) + requestBody = jsonBody.toRequestBody($S(media_type).toMediaType()) + }) + .expect("optional json request body builds"), + ); + } } BodyEncoding::TextPlain | BodyEncoding::OctetStream => { - let prefix = if body.required { - "val requestBody =" + let body_var = body.var_name.as_str(); + let media_type = body.media_type.as_str(); + if body.required { + cb.add_code( + sigil_quote!(Kotlin { + val requestBody = $L(body_var).toRequestBody($S(media_type).toMediaType()) + }) + .expect("required raw request body builds"), + ); } else { - "requestBody =" - }; - cb.add( - &format!( - "{prefix} {}.toRequestBody(\"{}\".toMediaType())", - body.var_name, body.media_type - ), - (), - ); - cb.add_line(); + cb.add_code( + sigil_quote!(Kotlin { + requestBody = $L(body_var).toRequestBody($S(media_type).toMediaType()) + }) + .expect("optional raw request body builds"), + ); + } } BodyEncoding::FormUrlEncoded | BodyEncoding::Xml | BodyEncoding::Other => { - cb.add( - &format!( - "throw IllegalArgumentException(\"unsupported request body media type: {}\")", - body.media_type - ), - (), + let message = format!("unsupported request body media type: {}", body.media_type); + cb.add_code( + sigil_quote!(Kotlin { + throw IllegalArgumentException($S(message)) + }) + .expect("unsupported request body builds"), ); - cb.add_line(); } BodyEncoding::Multipart => unreachable!("multipart handled separately"), } From e52fb4443bfd21872dab258cf3c07b48b6b8a85f Mon Sep 17 00:00:00 2001 From: Adam Basfop Cavendish Date: Wed, 17 Jun 2026 15:11:11 +0800 Subject: [PATCH 2/2] refactor: migrate generator emitters to sigil_quote - Migrate Rust, Go, Java, Kotlin, Python, and TypeScript emitter fragments to sigil_quote helpers - Move request setup, body handling, multipart parts, responses, and guard selection into quoted target-code blocks - Preserve generated golden output while keeping language-specific formatting stable - Keep remaining complex Python indentation and ureq array-query cases out of this migration --- src/generators/go/http/sigil_emit_api.rs | 622 ++++++++++++------ src/generators/java/okhttp/sigil_emit_api.rs | 305 +++++---- .../kotlin/okhttp/sigil_emit_api.rs | 330 +++++----- src/generators/python/httpx/emit_api.rs | 62 +- src/generators/python/requests/emit_api.rs | 62 +- src/generators/rust/aioduct/sigil_emit_api.rs | 288 +++++--- src/generators/rust/reqwest/sigil_emit_api.rs | 287 +++++--- src/generators/rust/ureq/sigil_emit_api.rs | 284 +++++--- .../typescript/fetch/sigil_emit_api.rs | 163 ++--- 9 files changed, 1513 insertions(+), 890 deletions(-) diff --git a/src/generators/go/http/sigil_emit_api.rs b/src/generators/go/http/sigil_emit_api.rs index 3f4414ff..56897c62 100644 --- a/src/generators/go/http/sigil_emit_api.rs +++ b/src/generators/go/http/sigil_emit_api.rs @@ -646,171 +646,118 @@ fn emit_method_body(plan: &OpPlan<'_>) -> CodeBlock { let mut cb = CodeBlock::builder(); // Path. - cb.add(&format!("path := \"{}\"", op.path), ()); - cb.add_line(); + cb.add_code(path_init(&op.path)); for p in path_params { let placeholder = format!("{{{}}}", p.param.name); let value_expr = deref_if_pointer(&p.var_name, p.is_pointer); let stringified = render_value_as_string(&value_expr, &p.param.type_expr); - cb.add( - &format!("path = strings.Replace(path, \"{placeholder}\", {stringified}, 1)"), - (), - ); - cb.add_line(); + cb.add_code(path_replace(&placeholder, &stringified)); } // Query. let has_query = !query_params.is_empty(); if has_query { - cb.add("query := url.Values{}", ()); - cb.add_line(); + cb.add_code(query_init()); for p in query_params { let value_expr = deref_if_pointer(&p.var_name, p.is_pointer); let stringified = render_value_as_string(&value_expr, &p.param.type_expr); - let set_line = format!("query.Set(\"{}\", {stringified})", p.param.name); - if p.param.required || !p.is_pointer { - cb.add(&set_line, ()); - cb.add_line(); - } else { - cb.begin_control_flow(&format!("if {} != nil", p.var_name), ()); - cb.add(&set_line, ()); - cb.add_line(); - cb.end_control_flow(); - } + cb.add_code(query_set_guarded( + p.param.required || !p.is_pointer, + &p.var_name, + &p.param.name, + &stringified, + )); } } // Body. if let Some(body) = body { - cb.add("var bodyReader io.Reader", ()); - cb.add_line(); + cb.add_code(body_reader_decl()); match body.encoding { BodyEncoding::Multipart => { - cb.add("var multipartContentType string", ()); - cb.add_line(); + cb.add_code(multipart_content_type_decl()); if let Some(parts) = &body.multipart_parts { emit_multipart_body(&mut cb, body, parts); } else { - cb.add( - "return nil, fmt.Errorf(\"unsupported multipart request body: schema must be object-shaped\")", - (), - ); - cb.add_line(); + cb.add_code(return_unsupported_multipart_request_body()); } } BodyEncoding::Json => { cb.begin_control_flow(&format!("if {} != nil", body.var_name), ()); - cb.add(&format!("buf, err := json.Marshal({})", body.var_name), ()); - cb.add_line(); + cb.add_code(json_marshal_body(&body.var_name)); cb.begin_control_flow("if err != nil", ()); - cb.add("return nil, fmt.Errorf(\"marshal body: %%w\", err)", ()); - cb.add_line(); + cb.add_code(return_marshal_body_error()); cb.end_control_flow(); - cb.add("bodyReader = bytes.NewReader(buf)", ()); - cb.add_line(); + cb.add_code(body_reader_bytes_buffer()); cb.end_control_flow(); } BodyEncoding::TextPlain => { cb.begin_control_flow(&format!("if {} != nil", body.var_name), ()); - cb.add( - &format!("bodyReader = strings.NewReader(*{})", body.var_name), - (), - ); - cb.add_line(); + cb.add_code(body_reader_string_pointer(&body.var_name)); cb.end_control_flow(); } BodyEncoding::OctetStream => { cb.begin_control_flow(&format!("if {} != nil", body.var_name), ()); - cb.add( - &format!("bodyReader = bytes.NewReader({})", body.var_name), - (), - ); - cb.add_line(); + cb.add_code(body_reader_bytes(&body.var_name)); cb.end_control_flow(); } BodyEncoding::FormUrlEncoded | BodyEncoding::Xml | BodyEncoding::Other => { - cb.add( - &format!( - "return nil, fmt.Errorf(\"unsupported request body media type: {}\")", - body.media_type - ), - (), - ); - cb.add_line(); + cb.add_code(return_unsupported_request_body_media_type(&body.media_type)); } } } // Build request. - let query_arg = if has_query { "query" } else { "nil" }; - let body_arg = if body.is_some() { "bodyReader" } else { "nil" }; - cb.add( - &format!( - "req, err := a.client.NewRequest(ctx, \"{}\", path, {query_arg}, {body_arg})", - op.method.to_uppercase(), - ), - (), - ); - cb.add_line(); + cb.add_code(new_request_stmt( + &op.method.to_uppercase(), + has_query, + body.is_some(), + )); cb.begin_control_flow("if err != nil", ()); - cb.add("return nil, err", ()); - cb.add_line(); + cb.add_code(return_nil_err()); cb.end_control_flow(); // Headers. for p in header_params { let value_expr = deref_if_pointer(&p.var_name, p.is_pointer); let stringified = render_value_as_string(&value_expr, &p.param.type_expr); - if p.param.required || !p.is_pointer { - cb.add( - &format!("req.Header.Set(\"{}\", {stringified})", p.param.name), - (), - ); - cb.add_line(); - } else { - cb.begin_control_flow(&format!("if {} != nil", p.var_name), ()); - cb.add( - &format!("req.Header.Set(\"{}\", {stringified})", p.param.name), - (), - ); - cb.add_line(); - cb.end_control_flow(); - } + cb.add_code(set_header_guarded( + p.param.required || !p.is_pointer, + &p.var_name, + &go_string_literal(&p.param.name), + &stringified, + )); } if let Some(body) = body { if body.encoding == BodyEncoding::Multipart { cb.begin_control_flow("if multipartContentType != \"\"", ()); - cb.add("req.Header.Set(\"Content-Type\", multipartContentType)", ()); - cb.add_line(); + cb.add_code(set_header( + &go_string_literal("Content-Type"), + "multipartContentType", + )); cb.end_control_flow(); } else { - cb.add( - &format!("req.Header.Set(\"Content-Type\", \"{}\")", body.media_type), - (), - ); - cb.add_line(); + cb.add_code(set_header( + &go_string_literal("Content-Type"), + &go_string_literal(&body.media_type), + )); } } - cb.add("req.Header.Set(\"Accept\", \"application/json\")", ()); - cb.add_line(); + cb.add_code(set_header( + &go_string_literal("Accept"), + &go_string_literal("application/json"), + )); // Dispatch. - cb.add("httpResp, err := a.client.Do(req)", ()); - cb.add_line(); + cb.add_code(do_request()); cb.begin_control_flow("if err != nil", ()); - cb.add("return nil, err", ()); - cb.add_line(); + cb.add_code(return_nil_err()); cb.end_control_flow(); - cb.add("defer httpResp.Body.Close()", ()); - cb.add_line(); + cb.add_code(defer_body_close()); cb.add_line(); - cb.add( - &format!("resp := &{response_type}{{StatusCode: httpResp.StatusCode, Raw: httpResp}}"), - (), - ); - cb.add_line(); + cb.add_code(response_init(response_type)); if !plan.typed_responses.is_empty() { let mut numeric_responses: Vec<&TypedResponse> = Vec::new(); @@ -849,76 +796,319 @@ fn emit_method_body(plan: &OpPlan<'_>) -> CodeBlock { "%L", emit_decode_into(&tr.field_name, &tr.go_type, tr.decoding), ); - cb.add( - "return resp, &runtime.APIError{StatusCode: httpResp.StatusCode, Status: httpResp.Status}", - (), - ); - cb.add_line(); + cb.add_code(return_status_api_error()); cb.end_control_flow(); } cb.begin_control_flow("if httpResp.StatusCode >= 400", ()); - cb.add("body, _ := io.ReadAll(httpResp.Body)", ()); - cb.add_line(); - cb.add( - "return nil, &runtime.APIError{StatusCode: httpResp.StatusCode, Status: httpResp.Status, Body: body}", - (), - ); - cb.add_line(); + cb.add_code(error_body_read()); + cb.add_code(return_body_api_error()); cb.end_control_flow(); cb.add("%<", ()); cb.end_control_flow(); } else { cb.begin_control_flow("if httpResp.StatusCode >= 400", ()); - cb.add("body, _ := io.ReadAll(httpResp.Body)", ()); - cb.add_line(); - cb.add( - "return nil, &runtime.APIError{StatusCode: httpResp.StatusCode, Status: httpResp.Status, Body: body}", - (), - ); - cb.add_line(); + cb.add_code(error_body_read()); + cb.add_code(return_body_api_error()); cb.end_control_flow(); } - cb.add("return resp, nil", ()); + cb.add_code(return_resp_nil()); cb.build().expect("method body builds") } +fn path_init(path: &str) -> CodeBlock { + let stmt = format!("path := {}", go_string_literal(path)); + sigil_quote!(GoLang { + $L(stmt.as_str()) + }) + .expect("path init builds") +} + +fn path_replace(placeholder: &str, value_expr: &str) -> CodeBlock { + let stmt = format!( + "path = strings.Replace(path, {}, {value_expr}, 1)", + go_string_literal(placeholder) + ); + sigil_quote!(GoLang { + $L(stmt.as_str()) + }) + .expect("path replace builds") +} + +fn query_init() -> CodeBlock { + let stmt = "query := url.Values{}"; + sigil_quote!(GoLang { + $L(stmt) + }) + .expect("query init builds") +} + +fn query_set_guarded(always_set: bool, var_name: &str, name: &str, value_expr: &str) -> CodeBlock { + let stmt = format!("query.Set({}, {value_expr})", go_string_literal(name)); + let guard = format!("{var_name} != nil"); + sigil_quote!(GoLang { + $if(always_set) { + $L(stmt.as_str()) + } $else { + if $L(guard.as_str()) { + $L(stmt.as_str()) + } + } + }) + .expect("guarded query set builds") +} + +fn body_reader_decl() -> CodeBlock { + sigil_quote!(GoLang { + var bodyReader io.Reader + }) + .expect("body reader declaration builds") +} + +fn multipart_content_type_decl() -> CodeBlock { + sigil_quote!(GoLang { + var multipartContentType string + }) + .expect("multipart content type declaration builds") +} + +fn return_unsupported_multipart_request_body() -> CodeBlock { + sigil_quote!(GoLang { + return nil, fmt.Errorf("unsupported multipart request body: schema must be object-shaped") + }) + .expect("unsupported multipart request body builds") +} + +fn json_marshal_body(body_var: &str) -> CodeBlock { + let stmt = format!("buf, err := json.Marshal({body_var})"); + sigil_quote!(GoLang { + $L(stmt.as_str()) + }) + .expect("JSON marshal body builds") +} + +fn return_marshal_body_error() -> CodeBlock { + sigil_quote!(GoLang { + return nil, fmt.Errorf("marshal body: %w", err) + }) + .expect("marshal body error builds") +} + +fn body_reader_bytes_buffer() -> CodeBlock { + sigil_quote!(GoLang { + bodyReader = bytes.NewReader(buf) + }) + .expect("body reader bytes buffer builds") +} + +fn body_reader_string_pointer(body_var: &str) -> CodeBlock { + let stmt = format!("bodyReader = strings.NewReader(*{body_var})"); + sigil_quote!(GoLang { + $L(stmt.as_str()) + }) + .expect("body reader string pointer builds") +} + +fn body_reader_bytes(body_var: &str) -> CodeBlock { + let stmt = format!("bodyReader = bytes.NewReader({body_var})"); + sigil_quote!(GoLang { + $L(stmt.as_str()) + }) + .expect("body reader bytes builds") +} + +fn return_unsupported_request_body_media_type(media_type: &str) -> CodeBlock { + let message = format!("unsupported request body media type: {media_type}"); + sigil_quote!(GoLang { + return nil, fmt.Errorf($S(message.as_str())) + }) + .expect("unsupported request body media type builds") +} + +fn new_request_stmt(method: &str, has_query: bool, has_body: bool) -> CodeBlock { + let query_body_stmt = + format!("req, err := a.client.NewRequest(ctx, {method:?}, path, query, bodyReader)"); + let query_no_body_stmt = + format!("req, err := a.client.NewRequest(ctx, {method:?}, path, query, nil)"); + let no_query_body_stmt = + format!("req, err := a.client.NewRequest(ctx, {method:?}, path, nil, bodyReader)"); + let no_query_no_body_stmt = + format!("req, err := a.client.NewRequest(ctx, {method:?}, path, nil, nil)"); + sigil_quote!(GoLang { + $if(has_query && has_body) { + $L(query_body_stmt.as_str()) + } $else_if(has_query) { + $L(query_no_body_stmt.as_str()) + } $else_if(has_body) { + $L(no_query_body_stmt.as_str()) + } $else { + $L(no_query_no_body_stmt.as_str()) + } + }) + .expect("new request statement builds") +} + +fn return_nil_err() -> CodeBlock { + sigil_quote!(GoLang { + return nil, err + }) + .expect("return nil err builds") +} + +fn set_header(name_expr: &str, value_expr: &str) -> CodeBlock { + let stmt = format!("req.Header.Set({name_expr}, {value_expr})"); + sigil_quote!(GoLang { + $L(stmt.as_str()) + }) + .expect("set header builds") +} + +fn set_header_guarded( + always_set: bool, + var_name: &str, + name_expr: &str, + value_expr: &str, +) -> CodeBlock { + let stmt = format!("req.Header.Set({name_expr}, {value_expr})"); + let guard = format!("{var_name} != nil"); + sigil_quote!(GoLang { + $if(always_set) { + $L(stmt.as_str()) + } $else { + if $L(guard.as_str()) { + $L(stmt.as_str()) + } + } + }) + .expect("guarded header set builds") +} + +fn do_request() -> CodeBlock { + sigil_quote!(GoLang { + httpResp, err := a.client.Do(req) + }) + .expect("do request builds") +} + +fn defer_body_close() -> CodeBlock { + sigil_quote!(GoLang { + defer httpResp.Body.Close() + }) + .expect("defer body close builds") +} + +fn response_init(response_type: &str) -> CodeBlock { + let stmt = + format!("resp := &{response_type}{{StatusCode: httpResp.StatusCode, Raw: httpResp}}"); + sigil_quote!(GoLang { + $L(stmt.as_str()) + }) + .expect("response init builds") +} + +fn return_status_api_error() -> CodeBlock { + let stmt = + "return resp, &runtime.APIError{StatusCode: httpResp.StatusCode, Status: httpResp.Status}"; + sigil_quote!(GoLang { + $L(stmt) + }) + .expect("return status API error builds") +} + +fn error_body_read() -> CodeBlock { + sigil_quote!(GoLang { + body, _ := io.ReadAll(httpResp.Body) + }) + .expect("error body read builds") +} + +fn return_body_api_error() -> CodeBlock { + let stmt = "return nil, &runtime.APIError{StatusCode: httpResp.StatusCode, Status: httpResp.Status, Body: body}"; + sigil_quote!(GoLang { + $L(stmt) + }) + .expect("return body API error builds") +} + +fn return_resp_nil() -> CodeBlock { + sigil_quote!(GoLang { + return resp, nil + }) + .expect("return response builds") +} + fn emit_multipart_body( cb: &mut sigil_stitch::code_block::CodeBlockBuilder, body: &BodyBinding, parts: &[MultipartPart], ) { cb.begin_control_flow(&format!("if {} != nil", body.var_name), ()); - cb.add("buf := &bytes.Buffer{}", ()); - cb.add_line(); - cb.add("writer := multipart.NewWriter(buf)", ()); - cb.add_line(); + cb.add_code(multipart_buffer_init()); + cb.add_code(multipart_writer_init()); for part in parts { let value_expr = format!("{}.{}", body.var_name, part.field_name); if part.required { emit_required_multipart_part(cb, part, &value_expr); } else { cb.begin_control_flow(&format!("if {value_expr} != nil"), ()); - cb.add(&format!("value := *{value_expr}"), ()); - cb.add_line(); + cb.add_code(optional_multipart_value(&value_expr)); emit_required_multipart_part(cb, part, "value"); cb.end_control_flow(); } } cb.begin_control_flow("if err := writer.Close(); err != nil", ()); - cb.add( - "return nil, fmt.Errorf(\"close multipart writer: %%w\", err)", - (), - ); - cb.add_line(); + cb.add_code(return_close_multipart_writer_error()); cb.end_control_flow(); - cb.add("bodyReader = buf", ()); - cb.add_line(); - cb.add("multipartContentType = writer.FormDataContentType()", ()); - cb.add_line(); + cb.add_code(multipart_body_reader_assign()); + cb.add_code(multipart_content_type_assign()); cb.end_control_flow(); } +fn multipart_buffer_init() -> CodeBlock { + let stmt = "buf := &bytes.Buffer{}"; + sigil_quote!(GoLang { + $L(stmt) + }) + .expect("multipart buffer init builds") +} + +fn multipart_writer_init() -> CodeBlock { + sigil_quote!(GoLang { + writer := multipart.NewWriter(buf) + }) + .expect("multipart writer init builds") +} + +fn optional_multipart_value(value_expr: &str) -> CodeBlock { + let stmt = format!("value := *{value_expr}"); + sigil_quote!(GoLang { + $L(stmt.as_str()) + }) + .expect("optional multipart value builds") +} + +fn return_close_multipart_writer_error() -> CodeBlock { + let stmt = "return nil, fmt.Errorf(\"close multipart writer: %w\", err)"; + sigil_quote!(GoLang { + $L(stmt) + }) + .expect("close multipart writer error builds") +} + +fn multipart_body_reader_assign() -> CodeBlock { + sigil_quote!(GoLang { + bodyReader = buf + }) + .expect("multipart body reader assign builds") +} + +fn multipart_content_type_assign() -> CodeBlock { + sigil_quote!(GoLang { + multipartContentType = writer.FormDataContentType() + }) + .expect("multipart content type assign builds") +} + fn emit_required_multipart_part( cb: &mut sigil_stitch::code_block::CodeBlockBuilder, part: &MultipartPart, @@ -926,82 +1116,48 @@ fn emit_required_multipart_part( ) { cb.add("{", ()); cb.add_line(); - cb.add("partHeader := textproto.MIMEHeader{}", ()); - cb.add_line(); + cb.add_code(multipart_part_header_init()); if part.is_binary { - cb.add( - &format!( - "disposition := mime.FormatMediaType(\"form-data\", map[string]string{{\"name\": {}, \"filename\": {value_expr}.FilenameOrDefault({})}})", - go_string_literal(&part.wire_name), - go_string_literal(&part.wire_name) - ), - (), - ); - cb.add_line(); - cb.add("partHeader.Set(\"Content-Disposition\", disposition)", ()); - cb.add_line(); + cb.add_code(multipart_binary_disposition( + value_expr, + &go_string_literal(&part.wire_name), + )); + cb.add_code(multipart_part_header_set( + &go_string_literal("Content-Disposition"), + "disposition", + )); } else { let disposition = format!("form-data; name={}", go_string_literal(&part.wire_name)); - cb.add( - &format!( - "partHeader.Set(\"Content-Disposition\", {})", - go_string_literal(&disposition) - ), - (), - ); - cb.add_line(); + cb.add_code(multipart_part_header_set( + &go_string_literal("Content-Disposition"), + &go_string_literal(&disposition), + )); } - cb.add( - &format!( - "partHeader.Set(\"Content-Type\", {})", - go_string_literal(&part.content_type) - ), - (), - ); - cb.add_line(); - cb.add("partWriter, err := writer.CreatePart(partHeader)", ()); - cb.add_line(); + cb.add_code(multipart_part_header_set( + &go_string_literal("Content-Type"), + &go_string_literal(&part.content_type), + )); + cb.add_code(multipart_part_writer_create()); cb.begin_control_flow("if err != nil", ()); - cb.add( - "return nil, fmt.Errorf(\"create multipart part: %%w\", err)", - (), - ); - cb.add_line(); + cb.add_code(return_create_multipart_part_error()); cb.end_control_flow(); if part.is_binary { cb.begin_control_flow( &format!("if _, err := partWriter.Write({value_expr}.Data); err != nil"), (), ); - cb.add( - "return nil, fmt.Errorf(\"write multipart file: %%w\", err)", - (), - ); - cb.add_line(); + cb.add_code(return_write_multipart_file_error()); cb.end_control_flow(); } else if part.value_encoding == MultipartValueEncoding::Json { - cb.add(&format!("partValue, err := json.Marshal({value_expr})"), ()); - cb.add_line(); + cb.add_code(multipart_json_value(value_expr)); cb.begin_control_flow("if err != nil", ()); - cb.add( - "return nil, fmt.Errorf(\"marshal multipart field: %%w\", err)", - (), - ); - cb.add_line(); + cb.add_code(return_marshal_multipart_field_error()); cb.end_control_flow(); cb.begin_control_flow("if _, err := partWriter.Write(partValue); err != nil", ()); - cb.add( - "return nil, fmt.Errorf(\"write multipart field: %%w\", err)", - (), - ); - cb.add_line(); + cb.add_code(return_write_multipart_field_error()); cb.end_control_flow(); } else if part.value_encoding == MultipartValueEncoding::Unsupported { - cb.add( - "return nil, fmt.Errorf(\"unsupported multipart part content type\")", - (), - ); - cb.add_line(); + cb.add_code(return_unsupported_multipart_part_error()); } else { cb.begin_control_flow( &format!( @@ -1010,17 +1166,89 @@ fn emit_required_multipart_part( ), (), ); - cb.add( - "return nil, fmt.Errorf(\"write multipart field: %%w\", err)", - (), - ); - cb.add_line(); + cb.add_code(return_write_multipart_field_error()); cb.end_control_flow(); } cb.add("}", ()); cb.add_line(); } +fn multipart_part_header_init() -> CodeBlock { + let stmt = "partHeader := textproto.MIMEHeader{}"; + sigil_quote!(GoLang { + $L(stmt) + }) + .expect("multipart part header init builds") +} + +fn multipart_binary_disposition(value_expr: &str, wire_name: &str) -> CodeBlock { + let stmt = format!( + "disposition := mime.FormatMediaType(\"form-data\", map[string]string{{\"name\": {wire_name}, \"filename\": {value_expr}.FilenameOrDefault({wire_name})}})" + ); + sigil_quote!(GoLang { + $L(stmt.as_str()) + }) + .expect("multipart binary disposition builds") +} + +fn multipart_part_header_set(name_expr: &str, value_expr: &str) -> CodeBlock { + let stmt = format!("partHeader.Set({name_expr}, {value_expr})"); + sigil_quote!(GoLang { + $L(stmt.as_str()) + }) + .expect("multipart part header set builds") +} + +fn multipart_part_writer_create() -> CodeBlock { + sigil_quote!(GoLang { + partWriter, err := writer.CreatePart(partHeader) + }) + .expect("multipart part writer create builds") +} + +fn return_create_multipart_part_error() -> CodeBlock { + sigil_quote!(GoLang { + return nil, fmt.Errorf("create multipart part: %w", err) + }) + .expect("create multipart part error builds") +} + +fn return_write_multipart_file_error() -> CodeBlock { + sigil_quote!(GoLang { + return nil, fmt.Errorf("write multipart file: %w", err) + }) + .expect("write multipart file error builds") +} + +fn multipart_json_value(value_expr: &str) -> CodeBlock { + let stmt = format!("partValue, err := json.Marshal({value_expr})"); + sigil_quote!(GoLang { + $L(stmt.as_str()) + }) + .expect("multipart JSON value builds") +} + +fn return_marshal_multipart_field_error() -> CodeBlock { + sigil_quote!(GoLang { + return nil, fmt.Errorf("marshal multipart field: %w", err) + }) + .expect("marshal multipart field error builds") +} + +fn return_write_multipart_field_error() -> CodeBlock { + sigil_quote!(GoLang { + return nil, fmt.Errorf("write multipart field: %w", err) + }) + .expect("write multipart field error builds") +} + +fn return_unsupported_multipart_part_error() -> CodeBlock { + sigil_quote!(GoLang { + return nil, fmt.Errorf("unsupported multipart part content type") + }) + .expect("unsupported multipart part error builds") +} + fn emit_decode_into(field: &str, go_ty: &str, decoding: ResponseDecoding) -> CodeBlock { let (elem_ty, assignment) = if go_ty.starts_with('[') || go_ty.starts_with("map[") { (go_ty.to_string(), format!("resp.{field} = payload")) diff --git a/src/generators/java/okhttp/sigil_emit_api.rs b/src/generators/java/okhttp/sigil_emit_api.rs index 9dd300a2..a176c4d0 100644 --- a/src/generators/java/okhttp/sigil_emit_api.rs +++ b/src/generators/java/okhttp/sigil_emit_api.rs @@ -344,36 +344,27 @@ fn emit_method_body(plan: &OpPlan<'_>) -> CodeBlock { cb.add_statement("Map query = new HashMap<>()", ()); for p in &plan.query_params { let stringified = render_value_as_string(&p.var_name, &p.param.type_expr); - if p.param.required { - cb.add_statement( - &format!("query.put(\"{}\", {})", p.param.name, stringified), - (), - ); - } else { - cb.begin_control_flow(&format!("if ({} != null)", p.var_name), ()); - cb.add_statement( - &format!("query.put(\"{}\", {})", p.param.name, stringified), - (), - ); - cb.end_control_flow(); - } + cb.add_code(java_query_param_put( + p.param.required, + &p.var_name, + &p.param.name, + &stringified, + )); } } // Build request - let query_arg = if has_query { "query" } else { "null" }; let method = plan.op.method.to_uppercase(); if let Some(body) = &plan.body { cb.add_statement("Request request", ()); if body.encoding == BodyEncoding::Multipart { if let Some(parts) = &body.multipart_parts { emit_multipart_body(&mut cb, body, parts); - cb.add_statement( - &format!( - "request = client.newRequestWithBody(\"{method}\", path, {query_arg}, multipartBody)" - ), - (), - ); + cb.add_code(java_new_request_with_body( + &method, + has_query, + "multipartBody", + )); } else { cb.add_statement( "throw new IllegalArgumentException(\"unsupported multipart request body: schema must be object-shaped\")", @@ -382,42 +373,25 @@ fn emit_method_body(plan: &OpPlan<'_>) -> CodeBlock { } } else { emit_request_body(&mut cb, body); - cb.add_statement( - &format!( - "request = client.newRequestWithBody(\"{method}\", path, {query_arg}, requestBody)" - ), - (), - ); + cb.add_code(java_new_request_with_body( + &method, + has_query, + "requestBody", + )); } } else { - cb.add_statement( - &format!("Request request = client.newRequest(\"{method}\", path, {query_arg}, null)"), - (), - ); + cb.add_code(java_new_request(&method, has_query)); } // Headers for p in &plan.header_params { let stringified = render_value_as_string(&p.var_name, &p.param.type_expr); - if p.param.required { - cb.add_statement( - &format!( - "request = request.newBuilder().header(\"{}\", {stringified}).build()", - p.param.name - ), - (), - ); - } else { - cb.begin_control_flow(&format!("if ({} != null)", p.var_name), ()); - cb.add_statement( - &format!( - "request = request.newBuilder().header(\"{}\", {stringified}).build()", - p.param.name - ), - (), - ); - cb.end_control_flow(); - } + cb.add_code(java_header_param_set( + p.param.required, + &p.var_name, + &p.param.name, + &stringified, + )); } // Execute @@ -452,23 +426,7 @@ fn emit_method_body(plan: &OpPlan<'_>) -> CodeBlock { continue; } cb.add_statement(&format!("{} {} = null", tr.java_type, tr.field_name), ()); - if let Ok(code) = tr.status.parse::() { - cb.begin_control_flow(&format!("if (response.code() == {code})"), ()); - cb.add_statement( - &format!("{} = {}", tr.field_name, response_decode_expr(tr)), - (), - ); - cb.end_control_flow(); - } else { - // Wildcard status ("4XX", "5XX", "default"): guard by range - let guard = wildcard_status_guard_java(&tr.status); - cb.begin_control_flow(&format!("if ({guard})"), ()); - cb.add_statement( - &format!("{} = {}", tr.field_name, response_decode_expr(tr)), - (), - ); - cb.end_control_flow(); - } + cb.add_code(java_response_decode_assignment(tr)); } // Return with typed fields @@ -505,6 +463,72 @@ fn emit_method_body(plan: &OpPlan<'_>) -> CodeBlock { cb.build().expect("method body builds") } +fn java_new_request(method: &str, has_query: bool) -> CodeBlock { + let with_query = + format!("Request request = client.newRequest(\"{method}\", path, query, null);"); + let without_query = + format!("Request request = client.newRequest(\"{method}\", path, null, null);"); + sigil_quote!(Java { + $if(has_query) { + $L(with_query.as_str()) + } $else { + $L(without_query.as_str()) + } + }) + .expect("Java request construction builds") +} + +fn java_new_request_with_body(method: &str, has_query: bool, body_expr: &str) -> CodeBlock { + let with_query = + format!("request = client.newRequestWithBody(\"{method}\", path, query, {body_expr});"); + let without_query = + format!("request = client.newRequestWithBody(\"{method}\", path, null, {body_expr});"); + sigil_quote!(Java { + $if(has_query) { + $L(with_query.as_str()) + } $else { + $L(without_query.as_str()) + } + }) + .expect("Java request body construction builds") +} + +fn java_query_param_put( + required: bool, + var_name: &str, + param_name: &str, + value_expr: &str, +) -> CodeBlock { + sigil_quote!(Java { + $if(required) { + query.put($S(param_name), $L(value_expr)); + } $else { + if ($L(var_name) != null) { + query.put($S(param_name), $L(value_expr)); + } + } + }) + .expect("Java query param put builds") +} + +fn java_header_param_set( + required: bool, + var_name: &str, + param_name: &str, + value_expr: &str, +) -> CodeBlock { + sigil_quote!(Java { + $if(required) { + request = request.newBuilder().header($S(param_name), $L(value_expr)).build(); + } $else { + if ($L(var_name) != null) { + request = request.newBuilder().header($S(param_name), $L(value_expr)).build(); + } + } + }) + .expect("Java header param set builds") +} + fn emit_multipart_body( cb: &mut sigil_stitch::code_block::CodeBlockBuilder, body: &BodyBinding, @@ -539,24 +563,23 @@ fn emit_multipart_body( cb.end_control_flow(); } } - if body.required { - cb.add_code( - sigil_quote!(Java { - RequestBody multipartBody = multipartBuilder.build(); - }) - .expect("required multipart body builds"), - ); - } else { - cb.add_code( - sigil_quote!(Java { - multipartBody = multipartBuilder.build(); - }) - .expect("optional multipart body builds"), - ); + cb.add_code(java_multipart_body_finish(body.required)); + if !body.required { cb.end_control_flow(); } } +fn java_multipart_body_finish(body_required: bool) -> CodeBlock { + sigil_quote!(Java { + $if(body_required) { + RequestBody multipartBody = multipartBuilder.build(); + } $else { + multipartBody = multipartBuilder.build(); + } + }) + .expect("multipart body finish builds") +} + fn emit_request_body(cb: &mut sigil_stitch::code_block::CodeBlockBuilder, body: &BodyBinding) { if !body.required { cb.add_code( @@ -571,42 +594,12 @@ fn emit_request_body(cb: &mut sigil_stitch::code_block::CodeBlockBuilder, body: BodyEncoding::Json => { let body_var = body.var_name.as_str(); let media_type = body.media_type.as_str(); - if body.required { - cb.add_code( - sigil_quote!(Java { - String jsonBody = gson.toJson($L(body_var)); - RequestBody requestBody = RequestBody.create(jsonBody, MediaType.get($S(media_type))); - }) - .expect("required json request body builds"), - ); - } else { - cb.add_code( - sigil_quote!(Java { - String jsonBody = gson.toJson($L(body_var)); - requestBody = RequestBody.create(jsonBody, MediaType.get($S(media_type))); - }) - .expect("optional json request body builds"), - ); - } + cb.add_code(java_json_request_body(body.required, body_var, media_type)); } BodyEncoding::TextPlain | BodyEncoding::OctetStream => { let body_var = body.var_name.as_str(); let media_type = body.media_type.as_str(); - if body.required { - cb.add_code( - sigil_quote!(Java { - RequestBody requestBody = RequestBody.create($L(body_var), MediaType.get($S(media_type))); - }) - .expect("required raw request body builds"), - ); - } else { - cb.add_code( - sigil_quote!(Java { - requestBody = RequestBody.create($L(body_var), MediaType.get($S(media_type))); - }) - .expect("optional raw request body builds"), - ); - } + cb.add_code(java_raw_request_body(body.required, body_var, media_type)); } BodyEncoding::FormUrlEncoded | BodyEncoding::Xml | BodyEncoding::Other => { let message = format!("unsupported request body media type: {}", body.media_type); @@ -624,6 +617,29 @@ fn emit_request_body(cb: &mut sigil_stitch::code_block::CodeBlockBuilder, body: } } +fn java_json_request_body(body_required: bool, body_var: &str, media_type: &str) -> CodeBlock { + sigil_quote!(Java { + String jsonBody = gson.toJson($L(body_var)); + $if(body_required) { + RequestBody requestBody = RequestBody.create(jsonBody, MediaType.get($S(media_type))); + } $else { + requestBody = RequestBody.create(jsonBody, MediaType.get($S(media_type))); + } + }) + .expect("json request body builds") +} + +fn java_raw_request_body(body_required: bool, body_var: &str, media_type: &str) -> CodeBlock { + sigil_quote!(Java { + $if(body_required) { + RequestBody requestBody = RequestBody.create($L(body_var), MediaType.get($S(media_type))); + } $else { + requestBody = RequestBody.create($L(body_var), MediaType.get($S(media_type))); + } + }) + .expect("raw request body builds") +} + fn response_decode_expr(tr: &TypedResponse) -> String { match tr.decoding { ResponseDecoding::Json => { @@ -635,42 +651,49 @@ fn response_decode_expr(tr: &TypedResponse) -> String { } } +fn java_response_decode_assignment(tr: &TypedResponse) -> CodeBlock { + let exact_status = tr.status.parse::().ok(); + let has_exact_status = exact_status.is_some(); + let status_code = exact_status.unwrap_or_default().to_string(); + let guard = wildcard_status_guard_java(&tr.status); + let assignment = format!("{} = {}", tr.field_name, response_decode_expr(tr)); + sigil_quote!(Java { + $if(has_exact_status) { + if (response.code() == $L(status_code.as_str())) { + $L(assignment.as_str()); + } + } $else { + if ($L(guard.as_str())) { + $L(assignment.as_str()); + } + } + }) + .expect("Java response decode assignment builds") +} + fn emit_required_multipart_part( cb: &mut sigil_stitch::code_block::CodeBlockBuilder, part: &MultipartPart, access: &str, ) { + cb.add_code(java_multipart_part(part, access)); +} + +fn java_multipart_part(part: &MultipartPart, access: &str) -> CodeBlock { let wire_name = part.wire_name.as_str(); let content_type = part.content_type.as_str(); - if part.is_binary { - cb.add_code( - sigil_quote!(Java { - multipartBuilder.addFormDataPart($S(wire_name), $L(access).filenameOrDefault($S(wire_name)), RequestBody.create($L(access).getData(), MediaType.get($S(content_type)))); - }) - .expect("binary multipart part block builds"), - ); - } else if part.value_encoding == MultipartValueEncoding::Json { - cb.add_code( - sigil_quote!(Java { - multipartBuilder.addFormDataPart($S(wire_name), null, RequestBody.create(gson.toJson($L(access)), MediaType.get($S(content_type)))); - }) - .expect("json multipart part block builds"), - ); - } else if part.value_encoding == MultipartValueEncoding::Unsupported { - cb.add_code( - sigil_quote!(Java { - throw new IllegalArgumentException($S("unsupported multipart part content type")); - }) - .expect("unsupported multipart part block builds"), - ); - } else { - cb.add_code( - sigil_quote!(Java { - multipartBuilder.addFormDataPart($S(wire_name), null, RequestBody.create(String.valueOf($L(access)), MediaType.get($S(content_type)))); - }) - .expect("text multipart part block builds"), - ); - } + sigil_quote!(Java { + $if(part.is_binary) { + multipartBuilder.addFormDataPart($S(wire_name), $L(access).filenameOrDefault($S(wire_name)), RequestBody.create($L(access).getData(), MediaType.get($S(content_type)))); + } $else_if(part.value_encoding == MultipartValueEncoding::Json) { + multipartBuilder.addFormDataPart($S(wire_name), null, RequestBody.create(gson.toJson($L(access)), MediaType.get($S(content_type)))); + } $else_if(part.value_encoding == MultipartValueEncoding::Unsupported) { + throw new IllegalArgumentException($S("unsupported multipart part content type")); + } $else { + multipartBuilder.addFormDataPart($S(wire_name), null, RequestBody.create(String.valueOf($L(access)), MediaType.get($S(content_type)))); + } + }) + .expect("multipart part block builds") } // --------------------------------------------------------------------------- diff --git a/src/generators/kotlin/okhttp/sigil_emit_api.rs b/src/generators/kotlin/okhttp/sigil_emit_api.rs index 82b63762..210fe8fe 100644 --- a/src/generators/kotlin/okhttp/sigil_emit_api.rs +++ b/src/generators/kotlin/okhttp/sigil_emit_api.rs @@ -260,32 +260,26 @@ fn emit_method_body(plan: &OpPlan<'_>) -> CodeBlock { cb.add_line(); for p in &plan.query_params { let stringified = render_value_as_string(&p.var_name, &p.param.type_expr); - if p.param.required { - cb.add(&format!("query[\"{}\"] = {stringified}", p.param.name), ()); - cb.add_line(); - } else { - cb.begin_control_flow(&format!("if ({} != null)", p.var_name), ()); - cb.add(&format!("query[\"{}\"] = {stringified}", p.param.name), ()); - cb.add_line(); - cb.end_control_flow(); - } + cb.add_code(kotlin_query_param_put( + p.param.required, + &p.var_name, + &p.param.name, + &stringified, + )); } } // Build request - let query_arg = if has_query { "query" } else { "null" }; let method = plan.op.method.to_uppercase(); if let Some(body) = &plan.body { if body.encoding == BodyEncoding::Multipart { if let Some(parts) = &body.multipart_parts { emit_multipart_body(&mut cb, body, parts); - cb.add( - &format!( - "val request = client.newRequestWithBody(\"{method}\", path, {query_arg}, multipartBody)" - ), - (), - ); - cb.add_line(); + cb.add_code(kotlin_new_request_with_body( + &method, + has_query, + "multipartBody", + )); } else { cb.add( "val request: okhttp3.Request = throw IllegalArgumentException(\"unsupported multipart request body: schema must be object-shaped\")", @@ -295,20 +289,14 @@ fn emit_method_body(plan: &OpPlan<'_>) -> CodeBlock { } } else { emit_request_body(&mut cb, body); - cb.add( - &format!( - "val request = client.newRequestWithBody(\"{method}\", path, {query_arg}, requestBody)" - ), - (), - ); - cb.add_line(); + cb.add_code(kotlin_new_request_with_body( + &method, + has_query, + "requestBody", + )); } } else { - cb.add( - &format!("val request = client.newRequest(\"{method}\", path, {query_arg}, null)"), - (), - ); - cb.add_line(); + cb.add_code(kotlin_new_request(&method, has_query)); } // Headers @@ -317,38 +305,17 @@ fn emit_method_body(plan: &OpPlan<'_>) -> CodeBlock { cb.add_line(); for p in &plan.header_params { let stringified = render_value_as_string(&p.var_name, &p.param.type_expr); - if p.param.required { - cb.add( - &format!( - "finalRequest = finalRequest.newBuilder().header(\"{}\", {stringified}).build()", - p.param.name - ), - (), - ); - cb.add_line(); - } else { - cb.begin_control_flow(&format!("if ({} != null)", p.var_name), ()); - cb.add( - &format!( - "finalRequest = finalRequest.newBuilder().header(\"{}\", {stringified}).build()", - p.param.name - ), - (), - ); - cb.add_line(); - cb.end_control_flow(); - } + cb.add_code(kotlin_header_param_set( + p.param.required, + &p.var_name, + &p.param.name, + &stringified, + )); } } // Execute - let request_var = if plan.header_params.is_empty() { - "request" - } else { - "finalRequest" - }; - cb.add(&format!("val response = client.execute({request_var})"), ()); - cb.add_line(); + cb.add_code(kotlin_execute_request(plan.header_params.is_empty())); cb.add_line(); // Error handling @@ -379,27 +346,7 @@ fn emit_method_body(plan: &OpPlan<'_>) -> CodeBlock { continue; } cb.add_line(); - let deserialize_expr = response_decode_expr(tr); - if let Ok(code) = tr.status.parse::() { - cb.add( - &format!( - "val {} = if (response.code == {code}) {} else null", - tr.field_name, deserialize_expr - ), - (), - ); - } else { - // Wildcard status ("4XX", "5XX", "default"): guard by range - let guard = wildcard_status_guard(&tr.status); - cb.add( - &format!( - "val {} = if ({}) {} else null", - tr.field_name, guard, deserialize_expr - ), - (), - ); - } - cb.add_line(); + cb.add_code(kotlin_response_decode_assignment(tr)); } // Return with typed fields @@ -437,6 +384,70 @@ fn emit_method_body(plan: &OpPlan<'_>) -> CodeBlock { cb.build().expect("method body builds") } +fn kotlin_new_request(method: &str, has_query: bool) -> CodeBlock { + let with_query = format!("val request = client.newRequest(\"{method}\", path, query, null)"); + let without_query = format!("val request = client.newRequest(\"{method}\", path, null, null)"); + sigil_quote!(Kotlin { + $if(has_query) { + $L(with_query.as_str()) + } $else { + $L(without_query.as_str()) + } + }) + .expect("Kotlin request construction builds") +} + +fn kotlin_new_request_with_body(method: &str, has_query: bool, body_expr: &str) -> CodeBlock { + let with_query = + format!("val request = client.newRequestWithBody(\"{method}\", path, query, {body_expr})"); + let without_query = + format!("val request = client.newRequestWithBody(\"{method}\", path, null, {body_expr})"); + sigil_quote!(Kotlin { + $if(has_query) { + $L(with_query.as_str()) + } $else { + $L(without_query.as_str()) + } + }) + .expect("Kotlin request body construction builds") +} + +fn kotlin_query_param_put( + required: bool, + var_name: &str, + param_name: &str, + value_expr: &str, +) -> CodeBlock { + sigil_quote!(Kotlin { + $if(required) { + query[$S(param_name)] = $L(value_expr) + } $else { + if ($L(var_name) != null) { + query[$S(param_name)] = $L(value_expr) + } + } + }) + .expect("Kotlin query param put builds") +} + +fn kotlin_header_param_set( + required: bool, + var_name: &str, + param_name: &str, + value_expr: &str, +) -> CodeBlock { + sigil_quote!(Kotlin { + $if(required) { + finalRequest = finalRequest.newBuilder().header($S(param_name), $L(value_expr)).build() + } $else { + if ($L(var_name) != null) { + finalRequest = finalRequest.newBuilder().header($S(param_name), $L(value_expr)).build() + } + } + }) + .expect("Kotlin header param set builds") +} + fn emit_multipart_body( cb: &mut sigil_stitch::code_block::CodeBlockBuilder, body: &BodyBinding, @@ -467,26 +478,34 @@ fn emit_multipart_body( cb.end_control_flow(); } } - if body.required { - cb.add_code( - sigil_quote!(Kotlin { - val multipartBody = multipartBuilder.build() - }) - .expect("required multipart body builds"), - ); - } else { - cb.add_code( - sigil_quote!(Kotlin { - multipartBody = multipartBuilder.build() - }) - .expect("optional multipart body builds"), - ); - } + cb.add_code(kotlin_multipart_body_finish(body.required)); if !body.required { cb.end_control_flow(); } } +fn kotlin_execute_request(use_request: bool) -> CodeBlock { + sigil_quote!(Kotlin { + $if(use_request) { + val response = client.execute(request) + } $else { + val response = client.execute(finalRequest) + } + }) + .expect("execute request builds") +} + +fn kotlin_multipart_body_finish(body_required: bool) -> CodeBlock { + sigil_quote!(Kotlin { + $if(body_required) { + val multipartBody = multipartBuilder.build() + } $else { + multipartBody = multipartBuilder.build() + } + }) + .expect("multipart body finish builds") +} + fn emit_request_body(cb: &mut sigil_stitch::code_block::CodeBlockBuilder, body: &BodyBinding) { if !body.required { cb.add_code( @@ -501,42 +520,16 @@ fn emit_request_body(cb: &mut sigil_stitch::code_block::CodeBlockBuilder, body: BodyEncoding::Json => { let body_var = body.var_name.as_str(); let media_type = body.media_type.as_str(); - if body.required { - cb.add_code( - sigil_quote!(Kotlin { - val jsonBody = gson.toJson($L(body_var)) - val requestBody = jsonBody.toRequestBody($S(media_type).toMediaType()) - }) - .expect("required json request body builds"), - ); - } else { - cb.add_code( - sigil_quote!(Kotlin { - val jsonBody = gson.toJson($L(body_var)) - requestBody = jsonBody.toRequestBody($S(media_type).toMediaType()) - }) - .expect("optional json request body builds"), - ); - } + cb.add_code(kotlin_json_request_body( + body.required, + body_var, + media_type, + )); } BodyEncoding::TextPlain | BodyEncoding::OctetStream => { let body_var = body.var_name.as_str(); let media_type = body.media_type.as_str(); - if body.required { - cb.add_code( - sigil_quote!(Kotlin { - val requestBody = $L(body_var).toRequestBody($S(media_type).toMediaType()) - }) - .expect("required raw request body builds"), - ); - } else { - cb.add_code( - sigil_quote!(Kotlin { - requestBody = $L(body_var).toRequestBody($S(media_type).toMediaType()) - }) - .expect("optional raw request body builds"), - ); - } + cb.add_code(kotlin_raw_request_body(body.required, body_var, media_type)); } BodyEncoding::FormUrlEncoded | BodyEncoding::Xml | BodyEncoding::Other => { let message = format!("unsupported request body media type: {}", body.media_type); @@ -554,6 +547,29 @@ fn emit_request_body(cb: &mut sigil_stitch::code_block::CodeBlockBuilder, body: } } +fn kotlin_json_request_body(body_required: bool, body_var: &str, media_type: &str) -> CodeBlock { + sigil_quote!(Kotlin { + val jsonBody = gson.toJson($L(body_var)) + $if(body_required) { + val requestBody = jsonBody.toRequestBody($S(media_type).toMediaType()) + } $else { + requestBody = jsonBody.toRequestBody($S(media_type).toMediaType()) + } + }) + .expect("json request body builds") +} + +fn kotlin_raw_request_body(body_required: bool, body_var: &str, media_type: &str) -> CodeBlock { + sigil_quote!(Kotlin { + $if(body_required) { + val requestBody = $L(body_var).toRequestBody($S(media_type).toMediaType()) + } $else { + requestBody = $L(body_var).toRequestBody($S(media_type).toMediaType()) + } + }) + .expect("raw request body builds") +} + fn response_decode_expr(tr: &TypedResponse) -> String { match tr.decoding { ResponseDecoding::Json => format!( @@ -565,43 +581,47 @@ fn response_decode_expr(tr: &TypedResponse) -> String { } } +fn kotlin_response_decode_assignment(tr: &TypedResponse) -> CodeBlock { + let exact_status = tr.status.parse::().ok(); + let has_exact_status = exact_status.is_some(); + let status_code = exact_status.unwrap_or_default().to_string(); + let guard = wildcard_status_guard(&tr.status); + let field_name = tr.field_name.as_str(); + let deserialize_expr = response_decode_expr(tr); + sigil_quote!(Kotlin { + $if(has_exact_status) { + val $L(field_name) = if (response.code == $L(status_code.as_str())) $L(deserialize_expr.as_str()) else null + } $else { + val $L(field_name) = if ($L(guard.as_str())) $L(deserialize_expr.as_str()) else null + } + }) + .expect("Kotlin response decode assignment builds") +} + fn emit_required_multipart_part( cb: &mut sigil_stitch::code_block::CodeBlockBuilder, part: &MultipartPart, access: &str, ) { + cb.add_code(kotlin_multipart_part(part, access)); + cb.add_line(); +} + +fn kotlin_multipart_part(part: &MultipartPart, access: &str) -> CodeBlock { let wire_name = part.wire_name.as_str(); let content_type = part.content_type.as_str(); - if part.is_binary { - cb.add_code( - sigil_quote!(Kotlin { - multipartBuilder.addFormDataPart($S(wire_name), $L(access).filenameOrDefault($S(wire_name)), $L(access).data.toRequestBody($S(content_type).toMediaType())) - }) - .expect("binary multipart part block builds"), - ); - } else if part.value_encoding == MultipartValueEncoding::Json { - cb.add_code( - sigil_quote!(Kotlin { - multipartBuilder.addFormDataPart($S(wire_name), null, gson.toJson($L(access)).toRequestBody($S(content_type).toMediaType())) - }) - .expect("json multipart part block builds"), - ); - } else if part.value_encoding == MultipartValueEncoding::Unsupported { - cb.add_code( - sigil_quote!(Kotlin { - throw IllegalArgumentException($S("unsupported multipart part content type")) - }) - .expect("unsupported multipart part block builds"), - ); - } else { - cb.add_code( - sigil_quote!(Kotlin { - multipartBuilder.addFormDataPart($S(wire_name), null, $L(access).toString().toRequestBody($S(content_type).toMediaType())) - }) - .expect("text multipart part block builds"), - ); - } - cb.add_line(); + sigil_quote!(Kotlin { + $if(part.is_binary) { + multipartBuilder.addFormDataPart($S(wire_name), $L(access).filenameOrDefault($S(wire_name)), $L(access).data.toRequestBody($S(content_type).toMediaType())) + } $else_if(part.value_encoding == MultipartValueEncoding::Json) { + multipartBuilder.addFormDataPart($S(wire_name), null, gson.toJson($L(access)).toRequestBody($S(content_type).toMediaType())) + } $else_if(part.value_encoding == MultipartValueEncoding::Unsupported) { + throw IllegalArgumentException($S("unsupported multipart part content type")) + } $else { + multipartBuilder.addFormDataPart($S(wire_name), null, $L(access).toString().toRequestBody($S(content_type).toMediaType())) + } + }) + .expect("multipart part block builds") } // --------------------------------------------------------------------------- diff --git a/src/generators/python/httpx/emit_api.rs b/src/generators/python/httpx/emit_api.rs index c191bf60..a823cc7b 100644 --- a/src/generators/python/httpx/emit_api.rs +++ b/src/generators/python/httpx/emit_api.rs @@ -391,37 +391,37 @@ fn emit_required_multipart_part( access: &str, ir: &IrSpec, ) { - if part.is_binary { - cb.add_statement( - &format!( - "files[\"{}\"] = ({}.filename_or_default(\"{}\"), {}.data, \"{}\")", - part.wire_name, access, part.wire_name, access, part.content_type - ), - (), - ); - } else if part.value_encoding == MultipartValueEncoding::Json { - let json_value = render_multipart_json_value(access, &part.type_expr, ir); - cb.add_statement( - &format!( - "files[\"{}\"] = (None, json.dumps({json_value}), \"{}\")", - part.wire_name, part.content_type - ), - (), - ); - } else if part.value_encoding == MultipartValueEncoding::Unsupported { - cb.add_statement( - "raise ValueError(\"unsupported multipart part content type\")", - (), - ); - } else { - cb.add_statement( - &format!( - "files[\"{}\"] = (None, str({access}), \"{}\")", - part.wire_name, part.content_type - ), - (), - ); - } + cb.add_code(multipart_part_assignment(part, access, ir)); +} + +fn multipart_part_assignment(part: &MultipartPart, access: &str, ir: &IrSpec) -> CodeBlock { + let binary_stmt = format!( + "files[\"{}\"] = ({}.filename_or_default(\"{}\"), {}.data, \"{}\")", + part.wire_name, access, part.wire_name, access, part.content_type + ); + let json_value = render_multipart_json_value(access, &part.type_expr, ir); + let json_stmt = format!( + "files[\"{}\"] = (None, json.dumps({json_value}), \"{}\")", + part.wire_name, part.content_type + ); + let unsupported_stmt = "raise ValueError(\"unsupported multipart part content type\")"; + let scalar_stmt = format!( + "files[\"{}\"] = (None, str({access}), \"{}\")", + part.wire_name, part.content_type + ); + + sigil_quote!(Python { + $if(part.is_binary) { + $L(binary_stmt.as_str()) + } $else_if(part.value_encoding == MultipartValueEncoding::Json) { + $L(json_stmt.as_str()) + } $else_if(part.value_encoding == MultipartValueEncoding::Unsupported) { + $L(unsupported_stmt) + } $else { + $L(scalar_stmt.as_str()) + } + }) + .expect("multipart part assignment builds") } fn render_multipart_json_value(access: &str, expr: &IrTypeExpr, ir: &IrSpec) -> String { diff --git a/src/generators/python/requests/emit_api.rs b/src/generators/python/requests/emit_api.rs index 1a7956e7..5be4fef7 100644 --- a/src/generators/python/requests/emit_api.rs +++ b/src/generators/python/requests/emit_api.rs @@ -390,37 +390,37 @@ fn emit_required_multipart_part( access: &str, ir: &IrSpec, ) { - if part.is_binary { - cb.add_statement( - &format!( - "files[\"{}\"] = ({}.filename_or_default(\"{}\"), {}.data, \"{}\")", - part.wire_name, access, part.wire_name, access, part.content_type - ), - (), - ); - } else if part.value_encoding == MultipartValueEncoding::Json { - let json_value = render_multipart_json_value(access, &part.type_expr, ir); - cb.add_statement( - &format!( - "files[\"{}\"] = (None, json.dumps({json_value}), \"{}\")", - part.wire_name, part.content_type - ), - (), - ); - } else if part.value_encoding == MultipartValueEncoding::Unsupported { - cb.add_statement( - "raise ValueError(\"unsupported multipart part content type\")", - (), - ); - } else { - cb.add_statement( - &format!( - "files[\"{}\"] = (None, str({access}), \"{}\")", - part.wire_name, part.content_type - ), - (), - ); - } + cb.add_code(multipart_part_assignment(part, access, ir)); +} + +fn multipart_part_assignment(part: &MultipartPart, access: &str, ir: &IrSpec) -> CodeBlock { + let binary_stmt = format!( + "files[\"{}\"] = ({}.filename_or_default(\"{}\"), {}.data, \"{}\")", + part.wire_name, access, part.wire_name, access, part.content_type + ); + let json_value = render_multipart_json_value(access, &part.type_expr, ir); + let json_stmt = format!( + "files[\"{}\"] = (None, json.dumps({json_value}), \"{}\")", + part.wire_name, part.content_type + ); + let unsupported_stmt = "raise ValueError(\"unsupported multipart part content type\")"; + let scalar_stmt = format!( + "files[\"{}\"] = (None, str({access}), \"{}\")", + part.wire_name, part.content_type + ); + + sigil_quote!(Python { + $if(part.is_binary) { + $L(binary_stmt.as_str()) + } $else_if(part.value_encoding == MultipartValueEncoding::Json) { + $L(json_stmt.as_str()) + } $else_if(part.value_encoding == MultipartValueEncoding::Unsupported) { + $L(unsupported_stmt) + } $else { + $L(scalar_stmt.as_str()) + } + }) + .expect("multipart part assignment builds") } fn render_multipart_json_value(access: &str, expr: &IrTypeExpr, ir: &IrSpec) -> String { diff --git a/src/generators/rust/aioduct/sigil_emit_api.rs b/src/generators/rust/aioduct/sigil_emit_api.rs index aa47c59a..ac07884d 100644 --- a/src/generators/rust/aioduct/sigil_emit_api.rs +++ b/src/generators/rust/aioduct/sigil_emit_api.rs @@ -73,21 +73,17 @@ pub fn emit_method_body(plan: &OpPlan<'_>) -> CodeBlock { (), ); for p in query_params { - if p.is_optional { - let value_expr = render_to_string("v", &p.param.type_expr, false); - b.begin_control_flow(&format!("if let Some(v) = &{}", p.var_name), ()); - b.add( - &format!("query_parts.push((\"{}\", {value_expr}));\n", p.param.name), - (), - ); - b.end_control_flow(); + let value_expr = if p.is_optional { + render_to_string("v", &p.param.type_expr, false) } else { - let value_expr = render_to_string(&p.var_name, &p.param.type_expr, false); - b.add( - &format!("query_parts.push((\"{}\", {value_expr}));\n", p.param.name), - (), - ); - } + render_to_string(&p.var_name, &p.param.type_expr, false) + }; + b.add_code(rust_query_part( + p.is_optional, + &p.var_name, + &p.param.name, + &value_expr, + )); } b.begin_control_flow("if !query_parts.is_empty()", ()); b.add( @@ -101,36 +97,22 @@ pub fn emit_method_body(plan: &OpPlan<'_>) -> CodeBlock { // Build request (aioduct uses method helpers that return Result) let method = op.method.to_lowercase(); let needs_mut_req = !header_params.is_empty() || body.is_some(); - let req_let = if needs_mut_req { - "let mut req" - } else { - "let req" - }; - b.add(&format!("{req_let} = self.client.{method}(&path)?;\n"), ()); + b.add_code(aioduct_request(needs_mut_req, &method)); // Headers for p in header_params { - if p.is_optional { - let value_expr = render_to_string("v", &p.param.type_expr, false); - b.begin_control_flow(&format!("if let Some(v) = &{}", p.var_name), ()); - b.add( - &format!( - "req = req.header_str(\"{}\", &{value_expr})?;\n", - p.param.name - ), - (), - ); - b.end_control_flow(); + let header_name = rust_string_literal(&p.param.name); + let value_expr = if p.is_optional { + render_to_string("v", &p.param.type_expr, false) } else { - let value_expr = render_to_string(&p.var_name, &p.param.type_expr, false); - b.add( - &format!( - "req = req.header_str(\"{}\", &{value_expr})?;\n", - p.param.name - ), - (), - ); - } + render_to_string(&p.var_name, &p.param.type_expr, false) + }; + b.add_code(aioduct_header_guarded( + p.is_optional, + &p.var_name, + &header_name, + &value_expr, + )); } // Body @@ -152,24 +134,112 @@ pub fn emit_method_body(plan: &OpPlan<'_>) -> CodeBlock { } // Send - b.add("let resp = req.send().await?;\n", ()); - b.add("let status_code = resp.status().as_u16();\n", ()); + b.add_code(aioduct_send()); + b.add_code(status_code_init()); // Parse response if typed_responses.is_empty() { - b.add(&format!("Ok({response_type} {{ status_code }})\n"), ()); + b.add_code(empty_response(response_type)); } else { - b.add("let body_bytes = resp.bytes().await?;\n", ()); + b.add_code(aioduct_body_bytes_init()); emit_result_init(&mut b, response_type, typed_responses); emit_response_match(&mut b, typed_responses, &|tr| { response_value_expr(tr, "&body_bytes") }); - b.add("Ok(result)\n", ()); + b.add_code(ok_result()); } b.build().unwrap() } +fn aioduct_request(needs_mut_req: bool, method: &str) -> CodeBlock { + let mutable_request_stmt = format!("let mut req = self.client.{method}(&path)?;"); + let request_stmt = format!("let req = self.client.{method}(&path)?;"); + sigil_quote!(RustLang { + $if(needs_mut_req) { + $L(mutable_request_stmt.as_str()) + } $else { + $L(request_stmt.as_str()) + } + }) + .expect("aioduct request builds") +} + +fn rust_query_part( + is_optional: bool, + var_name: &str, + param_name: &str, + value_expr: &str, +) -> CodeBlock { + let param_name = rust_string_literal(param_name); + sigil_quote!(RustLang { + $if(is_optional) { + if let Some(v) = &$L(var_name) { + query_parts.push(($L(param_name.as_str()), $L(value_expr))); + } + } $else { + query_parts.push(($L(param_name.as_str()), $L(value_expr))); + } + }) + .expect("Rust query part builds") +} + +fn aioduct_header_guarded( + is_optional: bool, + var_name: &str, + header_name: &str, + value_expr: &str, +) -> CodeBlock { + sigil_quote!(RustLang { + $if(is_optional) { + if let Some(v) = &$L(var_name) { + req = req.header_str($L(header_name), &$L(value_expr))?; + } + } $else { + req = req.header_str($L(header_name), &$L(value_expr))?; + } + }) + .expect("guarded aioduct header builds") +} + +fn aioduct_send() -> CodeBlock { + let send_stmt = "let resp = req.send().await?;"; + sigil_quote!(RustLang { + $L(send_stmt) + }) + .expect("aioduct send builds") +} + +fn status_code_init() -> CodeBlock { + sigil_quote!(RustLang { + let status_code = resp.status().as_u16(); + }) + .expect("status code init builds") +} + +fn empty_response(response_type: &str) -> CodeBlock { + let response_expr = format!("{response_type} {{ status_code }}"); + sigil_quote!(RustLang { + Ok($L(response_expr.as_str())) + }) + .expect("empty response builds") +} + +fn aioduct_body_bytes_init() -> CodeBlock { + let body_bytes_stmt = "let body_bytes = resp.bytes().await?;"; + sigil_quote!(RustLang { + $L(body_bytes_stmt) + }) + .expect("aioduct body bytes init builds") +} + +fn ok_result() -> CodeBlock { + sigil_quote!(RustLang { + Ok(result) + }) + .expect("ok result builds") +} + fn emit_body( b: &mut sigil_stitch::code_block::CodeBlockBuilder, body: &crate::generators::rust::common::emit_api::BodyBinding, @@ -260,69 +330,115 @@ fn emit_multipart_body( body_var: &str, parts: &[MultipartPart], ) { - b.add( - "let mut multipart = aioduct::multipart::Multipart::new();\n", - (), - ); + b.add_code(multipart_init()); for part in parts { let wire_name = rust_string_literal(&part.wire_name); let content_type = rust_string_literal(&part.content_type); if part.value_encoding == MultipartValueEncoding::Unsupported { if part.required { - b.add("return Err(Error::Unsupported(\"unsupported multipart part content type\"));\n", ()); + b.add_code(unsupported_multipart_part()); } else { let field_name = rust_field_name(&part.wire_name); b.begin_control_flow(&format!("if {body_var}.{field_name}.is_some()"), ()); - b.add("return Err(Error::Unsupported(\"unsupported multipart part content type\"));\n", ()); + b.add_code(unsupported_multipart_part()); b.end_control_flow(); } continue; } if part.required { - if part.is_binary { - b.add( - &format!( - "multipart = multipart.file({wire_name}, {}, {content_type}, {});\n", - binary_filename_expr(body_var, part), - binary_field_expr(body_var, part), - ), - (), - ); + let binary_value_expr = if part.is_binary { + binary_field_expr(body_var, part) } else { - b.add( - &format!( - "multipart = multipart.part(aioduct::multipart::Part::text({wire_name}, {}).mime_str({content_type}));\n", - text_field_expr(body_var, part), - ), - (), - ); - } + String::new() + }; + let filename_expr = if part.is_binary { + binary_filename_expr(body_var, part) + } else { + String::new() + }; + let text_value_expr = if part.is_binary { + String::new() + } else { + text_field_expr(body_var, part) + }; + b.add_code(aioduct_multipart_part( + part.is_binary, + &wire_name, + &filename_expr, + &content_type, + &binary_value_expr, + &text_value_expr, + )); } else { let field_name = rust_field_name(&part.wire_name); b.begin_control_flow( &format!("if let Some(value) = &{body_var}.{field_name}"), (), ); - if part.is_binary { - b.add( - &format!( - "multipart = multipart.file({wire_name}, {}, {content_type}, {});\n", - optional_binary_filename_expr("value", part), - optional_binary_field_expr("value"), - ), - (), - ); + let binary_value_expr = if part.is_binary { + optional_binary_field_expr("value") } else { - b.add( - &format!( - "multipart = multipart.part(aioduct::multipart::Part::text({wire_name}, {}).mime_str({content_type}));\n", - optional_text_field_expr("value", part), - ), - (), - ); - } + String::new() + }; + let filename_expr = if part.is_binary { + optional_binary_filename_expr("value", part) + } else { + String::new() + }; + let text_value_expr = if part.is_binary { + String::new() + } else { + optional_text_field_expr("value", part) + }; + b.add_code(aioduct_multipart_part( + part.is_binary, + &wire_name, + &filename_expr, + &content_type, + &binary_value_expr, + &text_value_expr, + )); b.end_control_flow(); } } - b.add("req = req.multipart(multipart);\n", ()); + b.add_code(multipart_finish()); +} + +fn multipart_init() -> CodeBlock { + sigil_quote!(RustLang { + let mut multipart = aioduct::multipart::Multipart::new(); + }) + .expect("multipart init builds") +} + +fn unsupported_multipart_part() -> CodeBlock { + sigil_quote!(RustLang { + return Err(Error::Unsupported($S("unsupported multipart part content type"))); + }) + .expect("unsupported multipart part builds") +} + +fn aioduct_multipart_part( + is_binary: bool, + wire_name: &str, + filename_expr: &str, + content_type: &str, + binary_value_expr: &str, + text_value_expr: &str, +) -> CodeBlock { + sigil_quote!(RustLang { + $if(is_binary) { + multipart = multipart.file($L(wire_name), $L(filename_expr), $L(content_type), $L(binary_value_expr)); + } $else { + multipart = multipart.part(aioduct::multipart::Part::text($L(wire_name), $L(text_value_expr)).mime_str($L(content_type))); + } + }) + .expect("multipart part builds") +} + +fn multipart_finish() -> CodeBlock { + sigil_quote!(RustLang { + req = req.multipart(multipart); + }) + .expect("multipart finish builds") } diff --git a/src/generators/rust/reqwest/sigil_emit_api.rs b/src/generators/rust/reqwest/sigil_emit_api.rs index cea23563..1cbf02e0 100644 --- a/src/generators/rust/reqwest/sigil_emit_api.rs +++ b/src/generators/rust/reqwest/sigil_emit_api.rs @@ -73,21 +73,17 @@ pub fn emit_method_body(plan: &OpPlan<'_>) -> CodeBlock { (), ); for p in query_params { - if p.is_optional { - let value_expr = render_to_string("v", &p.param.type_expr, false); - b.begin_control_flow(&format!("if let Some(v) = &{}", p.var_name), ()); - b.add( - &format!("query_parts.push((\"{}\", {value_expr}));\n", p.param.name), - (), - ); - b.end_control_flow(); + let value_expr = if p.is_optional { + render_to_string("v", &p.param.type_expr, false) } else { - let value_expr = render_to_string(&p.var_name, &p.param.type_expr, false); - b.add( - &format!("query_parts.push((\"{}\", {value_expr}));\n", p.param.name), - (), - ); - } + render_to_string(&p.var_name, &p.param.type_expr, false) + }; + b.add_code(rust_query_part( + p.is_optional, + &p.var_name, + &p.param.name, + &value_expr, + )); } b.begin_control_flow("if !query_parts.is_empty()", ()); b.add( @@ -101,33 +97,22 @@ pub fn emit_method_body(plan: &OpPlan<'_>) -> CodeBlock { // Build request let method = op.method.to_uppercase(); let needs_mut_req = !header_params.is_empty() || body.is_some(); - let req_let = if needs_mut_req { - "let mut req" - } else { - "let req" - }; - b.add( - &format!("{req_let} = self.client.request(reqwest::Method::{method}, &path).await?;\n"), - (), - ); + b.add_code(reqwest_request(needs_mut_req, &method)); // Headers for p in header_params { - if p.is_optional { - let value_expr = render_to_string("v", &p.param.type_expr, false); - b.begin_control_flow(&format!("if let Some(v) = &{}", p.var_name), ()); - b.add( - &format!("req = req.header(\"{}\", {value_expr});\n", p.param.name), - (), - ); - b.end_control_flow(); + let header_name = rust_string_literal(&p.param.name); + let value_expr = if p.is_optional { + render_to_string("v", &p.param.type_expr, false) } else { - let value_expr = render_to_string(&p.var_name, &p.param.type_expr, false); - b.add( - &format!("req = req.header(\"{}\", {value_expr});\n", p.param.name), - (), - ); - } + render_to_string(&p.var_name, &p.param.type_expr, false) + }; + b.add_code(reqwest_header_guarded( + p.is_optional, + &p.var_name, + &header_name, + &value_expr, + )); } // Body @@ -149,27 +134,114 @@ pub fn emit_method_body(plan: &OpPlan<'_>) -> CodeBlock { } // Send - b.add("let resp = self.client.send(req).await?;\n", ()); - b.add("let status_code = resp.status().as_u16();\n", ()); + b.add_code(reqwest_send()); + b.add_code(status_code_init()); // Parse response if typed_responses.is_empty() { - b.add(&format!("Ok({response_type} {{ status_code }})\n"), ()); + b.add_code(empty_response(response_type)); } else { - b.add( - "let body_bytes = resp.bytes().await.map_err(Error::Network)?;\n", - (), - ); + b.add_code(reqwest_body_bytes_init()); emit_result_init(&mut b, response_type, typed_responses); emit_response_match(&mut b, typed_responses, &|tr| { response_value_expr(tr, "&body_bytes") }); - b.add("Ok(result)\n", ()); + b.add_code(ok_result()); } b.build().unwrap() } +fn reqwest_request(needs_mut_req: bool, method: &str) -> CodeBlock { + let mutable_request_stmt = + format!("let mut req = self.client.request(reqwest::Method::{method}, &path).await?;"); + let request_stmt = + format!("let req = self.client.request(reqwest::Method::{method}, &path).await?;"); + sigil_quote!(RustLang { + $if(needs_mut_req) { + $L(mutable_request_stmt.as_str()) + } $else { + $L(request_stmt.as_str()) + } + }) + .expect("reqwest request builds") +} + +fn rust_query_part( + is_optional: bool, + var_name: &str, + param_name: &str, + value_expr: &str, +) -> CodeBlock { + let param_name = rust_string_literal(param_name); + sigil_quote!(RustLang { + $if(is_optional) { + if let Some(v) = &$L(var_name) { + query_parts.push(($L(param_name.as_str()), $L(value_expr))); + } + } $else { + query_parts.push(($L(param_name.as_str()), $L(value_expr))); + } + }) + .expect("Rust query part builds") +} + +fn reqwest_header_guarded( + is_optional: bool, + var_name: &str, + header_name: &str, + value_expr: &str, +) -> CodeBlock { + sigil_quote!(RustLang { + $if(is_optional) { + if let Some(v) = &$L(var_name) { + req = req.header($L(header_name), $L(value_expr)); + } + } $else { + req = req.header($L(header_name), $L(value_expr)); + } + }) + .expect("guarded reqwest header builds") +} + +fn reqwest_send() -> CodeBlock { + let send_stmt = "let resp = self.client.send(req).await?;"; + sigil_quote!(RustLang { + $L(send_stmt) + }) + .expect("reqwest send builds") +} + +fn status_code_init() -> CodeBlock { + sigil_quote!(RustLang { + let status_code = resp.status().as_u16(); + }) + .expect("status code init builds") +} + +fn empty_response(response_type: &str) -> CodeBlock { + let response_expr = format!("{response_type} {{ status_code }}"); + sigil_quote!(RustLang { + Ok($L(response_expr.as_str())) + }) + .expect("empty response builds") +} + +fn reqwest_body_bytes_init() -> CodeBlock { + let body_bytes_stmt = "let body_bytes = resp.bytes().await.map_err(Error::Network)?;"; + sigil_quote!(RustLang { + $L(body_bytes_stmt) + }) + .expect("reqwest body bytes init builds") +} + +fn ok_result() -> CodeBlock { + sigil_quote!(RustLang { + Ok(result) + }) + .expect("ok result builds") +} + fn emit_body( b: &mut sigil_stitch::code_block::CodeBlockBuilder, body: &crate::generators::rust::common::emit_api::BodyBinding, @@ -259,66 +331,115 @@ fn emit_multipart_body( body_var: &str, parts: &[MultipartPart], ) { - b.add("let mut multipart = reqwest::multipart::Form::new();\n", ()); + b.add_code(multipart_init()); for part in parts { let wire_name = rust_string_literal(&part.wire_name); let content_type = rust_string_literal(&part.content_type); if part.value_encoding == MultipartValueEncoding::Unsupported { if part.required { - b.add("return Err(Error::Unsupported(\"unsupported multipart part content type\"));\n", ()); + b.add_code(unsupported_multipart_part()); } else { let field_name = rust_field_name(&part.wire_name); b.begin_control_flow(&format!("if {body_var}.{field_name}.is_some()"), ()); - b.add("return Err(Error::Unsupported(\"unsupported multipart part content type\"));\n", ()); + b.add_code(unsupported_multipart_part()); b.end_control_flow(); } continue; } if part.required { - if part.is_binary { - b.add( - &format!( - "multipart = multipart.part({wire_name}, reqwest::multipart::Part::bytes({}).file_name({}).mime_str({content_type}).map_err(Error::Network)?);\n", - binary_field_expr(body_var, part), - binary_filename_expr(body_var, part), - ), - (), - ); + let binary_value_expr = if part.is_binary { + binary_field_expr(body_var, part) } else { - b.add( - &format!( - "multipart = multipart.part({wire_name}, reqwest::multipart::Part::text({}).mime_str({content_type}).map_err(Error::Network)?);\n", - text_field_expr(body_var, part), - ), - (), - ); - } + String::new() + }; + let filename_expr = if part.is_binary { + binary_filename_expr(body_var, part) + } else { + String::new() + }; + let text_value_expr = if part.is_binary { + String::new() + } else { + text_field_expr(body_var, part) + }; + b.add_code(reqwest_multipart_part( + part.is_binary, + &wire_name, + &binary_value_expr, + &filename_expr, + &text_value_expr, + &content_type, + )); } else { let field_name = rust_field_name(&part.wire_name); b.begin_control_flow( &format!("if let Some(value) = &{body_var}.{field_name}"), (), ); - if part.is_binary { - b.add( - &format!( - "multipart = multipart.part({wire_name}, reqwest::multipart::Part::bytes({}).file_name({}).mime_str({content_type}).map_err(Error::Network)?);\n", - optional_binary_field_expr("value"), - optional_binary_filename_expr("value", part), - ), - (), - ); + let binary_value_expr = if part.is_binary { + optional_binary_field_expr("value") } else { - b.add( - &format!( - "multipart = multipart.part({wire_name}, reqwest::multipart::Part::text({}).mime_str({content_type}).map_err(Error::Network)?);\n", - optional_text_field_expr("value", part), - ), - (), - ); - } + String::new() + }; + let filename_expr = if part.is_binary { + optional_binary_filename_expr("value", part) + } else { + String::new() + }; + let text_value_expr = if part.is_binary { + String::new() + } else { + optional_text_field_expr("value", part) + }; + b.add_code(reqwest_multipart_part( + part.is_binary, + &wire_name, + &binary_value_expr, + &filename_expr, + &text_value_expr, + &content_type, + )); b.end_control_flow(); } } - b.add("req = req.multipart(multipart);\n", ()); + b.add_code(multipart_finish()); +} + +fn multipart_init() -> CodeBlock { + sigil_quote!(RustLang { + let mut multipart = reqwest::multipart::Form::new(); + }) + .expect("multipart init builds") +} + +fn unsupported_multipart_part() -> CodeBlock { + sigil_quote!(RustLang { + return Err(Error::Unsupported($S("unsupported multipart part content type"))); + }) + .expect("unsupported multipart part builds") +} + +fn reqwest_multipart_part( + is_binary: bool, + wire_name: &str, + binary_value_expr: &str, + filename_expr: &str, + text_value_expr: &str, + content_type: &str, +) -> CodeBlock { + sigil_quote!(RustLang { + $if(is_binary) { + multipart = multipart.part($L(wire_name), reqwest::multipart::Part::bytes($L(binary_value_expr)).file_name($L(filename_expr)).mime_str($L(content_type)).map_err(Error::Network)?); + } $else { + multipart = multipart.part($L(wire_name), reqwest::multipart::Part::text($L(text_value_expr)).mime_str($L(content_type)).map_err(Error::Network)?); + } + }) + .expect("multipart part builds") +} + +fn multipart_finish() -> CodeBlock { + sigil_quote!(RustLang { + req = req.multipart(multipart); + }) + .expect("multipart finish builds") } diff --git a/src/generators/rust/ureq/sigil_emit_api.rs b/src/generators/rust/ureq/sigil_emit_api.rs index 7f6c8c68..7ab13bfb 100644 --- a/src/generators/rust/ureq/sigil_emit_api.rs +++ b/src/generators/rust/ureq/sigil_emit_api.rs @@ -81,12 +81,7 @@ pub fn emit_method_body(plan: &OpPlan<'_>) -> CodeBlock { }); let needs_mut_req = !query_params.is_empty() || !header_params.is_empty() || body_requires_mut_req; - let req_let = if needs_mut_req { - "let mut req" - } else { - "let req" - }; - b.add(&format!("{req_let} = self.client.{method}(&path);\n"), ()); + b.add_code(ureq_request(needs_mut_req, &method)); // Query params via ureq's built-in .query() for p in query_params { @@ -154,7 +149,7 @@ pub fn emit_method_body(plan: &OpPlan<'_>) -> CodeBlock { if body.required { emit_body_send(&mut b, body, "let resp ="); } else { - b.add("let resp;\n", ()); + b.add_code(resp_decl()); b.begin_control_flow( &format!("if let Some({}) = {}", body.var_name, body.var_name), (), @@ -162,30 +157,30 @@ pub fn emit_method_body(plan: &OpPlan<'_>) -> CodeBlock { emit_body_send(&mut b, body, "resp ="); b.end_control_flow(); b.begin_control_flow("else", ()); - b.add("resp = req.send_empty()?;\n", ()); + b.add_code(assign_send_empty()); b.end_control_flow(); } } else { - b.add("let resp = req.send_empty()?;\n", ()); + b.add_code(let_send_empty()); } } else { - b.add("let resp = req.send_empty()?;\n", ()); + b.add_code(let_send_empty()); } } else { - b.add("let resp = req.call()?;\n", ()); + b.add_code(let_call()); } - b.add("let status_code = resp.status().as_u16();\n", ()); + b.add_code(status_code_init()); // Parse response if typed_responses.is_empty() { - b.add(&format!("Ok({response_type} {{ status_code }})\n"), ()); + b.add_code(empty_response(response_type)); } else { - b.add("let body_bytes = resp.into_body().read_to_vec()?;\n", ()); + b.add_code(body_bytes_init()); emit_result_init(&mut b, response_type, typed_responses); emit_response_match(&mut b, typed_responses, &|tr| { response_value_expr(tr, "body_bytes.as_slice()") }); - b.add("Ok(result)\n", ()); + b.add_code(ok_result()); } b.build().unwrap() @@ -227,6 +222,19 @@ fn emit_body_send( } } +fn ureq_request(needs_mut_req: bool, method: &str) -> CodeBlock { + let mutable_request_stmt = format!("let mut req = self.client.{method}(&path);"); + let request_stmt = format!("let req = self.client.{method}(&path);"); + sigil_quote!(RustLang { + $if(needs_mut_req) { + $L(mutable_request_stmt.as_str()) + } $else { + $L(request_stmt.as_str()) + } + }) + .expect("ureq request builds") +} + fn unsupported_multipart_body(body_var: &str) -> CodeBlock { sigil_quote!(RustLang { let _ = self.client; @@ -296,10 +304,7 @@ fn emit_multipart_body( body_var: &str, parts: &[MultipartPart], ) { - b.add( - "let boundary = format!(\"openapi-nexus-{}\", std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH).map(|duration| duration.as_nanos()).unwrap_or(0));\n", - (), - ); + b.add_code(multipart_boundary_init()); b.add_code(multipart_body_init()); for part in parts { emit_multipart_part(b, body_var, part); @@ -312,16 +317,93 @@ fn emit_form_body(b: &mut sigil_stitch::code_block::CodeBlockBuilder, body_var: b.begin_control_flow("if let serde_json::Value::Object(fields) = form_value", ()); b.begin_control_flow("for (key, value) in fields", ()); b.begin_control_flow("if !value.is_null()", ()); - b.add( - "let value = match value { serde_json::Value::String(s) => s, other => other.to_string() };\n", - (), - ); - b.add("form_pairs.push((key, value));\n", ()); + b.add_code(form_value_to_string()); + b.add_code(form_pairs_push()); b.end_control_flow(); b.end_control_flow(); b.end_control_flow(); } +fn resp_decl() -> CodeBlock { + sigil_quote!(RustLang { + let resp; + }) + .expect("response declaration builds") +} + +fn assign_send_empty() -> CodeBlock { + sigil_quote!(RustLang { + resp = req.send_empty()?; + }) + .expect("assign send_empty builds") +} + +fn let_send_empty() -> CodeBlock { + sigil_quote!(RustLang { + let resp = req.send_empty()?; + }) + .expect("let send_empty builds") +} + +fn let_call() -> CodeBlock { + sigil_quote!(RustLang { + let resp = req.call()?; + }) + .expect("let call builds") +} + +fn status_code_init() -> CodeBlock { + sigil_quote!(RustLang { + let status_code = resp.status().as_u16(); + }) + .expect("status code init builds") +} + +fn empty_response(response_type: &str) -> CodeBlock { + let response_expr = format!("{response_type} {{ status_code }}"); + sigil_quote!(RustLang { + Ok($L(response_expr.as_str())) + }) + .expect("empty response builds") +} + +fn body_bytes_init() -> CodeBlock { + sigil_quote!(RustLang { + let body_bytes = resp.into_body().read_to_vec()?; + }) + .expect("body bytes init builds") +} + +fn ok_result() -> CodeBlock { + sigil_quote!(RustLang { + Ok(result) + }) + .expect("ok result builds") +} + +fn form_value_to_string() -> CodeBlock { + let value_expr = "let value = match value { serde_json::Value::String(s) => s, other => other.to_string() };"; + sigil_quote!(RustLang { + $L(value_expr) + }) + .expect("form value conversion builds") +} + +fn form_pairs_push() -> CodeBlock { + sigil_quote!(RustLang { + form_pairs.push((key, value)); + }) + .expect("form pairs push builds") +} + +fn multipart_boundary_init() -> CodeBlock { + let timestamp_expr = "std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH).map(|duration| duration.as_nanos()).unwrap_or(0)"; + sigil_quote!(RustLang { + let boundary = format!("openapi-nexus-{}", $L(timestamp_expr)); + }) + .expect("multipart boundary init builds") +} + fn multipart_body_init() -> CodeBlock { sigil_quote!(RustLang { let mut multipart_body: Vec = Vec::new(); @@ -354,17 +436,11 @@ fn emit_multipart_part( let content_type = rust_string_literal(&part.content_type); if part.value_encoding == MultipartValueEncoding::Unsupported { if part.required { - b.add( - "return Err(Error::Unsupported(\"unsupported multipart part content type\"));\n", - (), - ); + b.add_code(unsupported_multipart_part()); } else { let field_name = rust_field_name(&part.wire_name); b.begin_control_flow(&format!("if {body_var}.{field_name}.is_some()"), ()); - b.add( - "return Err(Error::Unsupported(\"unsupported multipart part content type\"));\n", - (), - ); + b.add_code(unsupported_multipart_part()); b.end_control_flow(); } return; @@ -378,24 +454,22 @@ fn emit_multipart_part( &content_type, filename_expr.as_deref(), ); - if part.is_binary { - b.add( - &format!( - "multipart_body.extend_from_slice(&{});\n", - binary_field_expr(body_var, part), - ), - (), - ); + let binary_value_expr = if part.is_binary { + binary_field_expr(body_var, part) } else { - b.add( - &format!( - "multipart_body.extend_from_slice({}.as_bytes());\n", - text_field_expr(body_var, part), - ), - (), - ); - } - b.add("multipart_body.extend_from_slice(b\"\\r\\n\");\n", ()); + String::new() + }; + let text_value_expr = if part.is_binary { + String::new() + } else { + text_field_expr(body_var, part) + }; + b.add_code(multipart_part_value( + part.is_binary, + &binary_value_expr, + &text_value_expr, + )); + b.add_code(multipart_part_crlf()); } else { let field_name = rust_field_name(&part.wire_name); b.begin_control_flow( @@ -412,28 +486,55 @@ fn emit_multipart_part( &content_type, filename_expr.as_deref(), ); - if part.is_binary { - b.add( - &format!( - "multipart_body.extend_from_slice(&{});\n", - optional_binary_field_expr("value"), - ), - (), - ); + let binary_value_expr = if part.is_binary { + optional_binary_field_expr("value") } else { - b.add( - &format!( - "multipart_body.extend_from_slice({}.as_bytes());\n", - optional_text_field_expr("value", part), - ), - (), - ); - } - b.add("multipart_body.extend_from_slice(b\"\\r\\n\");\n", ()); + String::new() + }; + let text_value_expr = if part.is_binary { + String::new() + } else { + optional_text_field_expr("value", part) + }; + b.add_code(multipart_part_value( + part.is_binary, + &binary_value_expr, + &text_value_expr, + )); + b.add_code(multipart_part_crlf()); b.end_control_flow(); } } +fn unsupported_multipart_part() -> CodeBlock { + sigil_quote!(RustLang { + return Err(Error::Unsupported($S("unsupported multipart part content type"))); + }) + .expect("unsupported multipart part builds") +} + +fn multipart_part_value( + is_binary: bool, + binary_value_expr: &str, + text_value_expr: &str, +) -> CodeBlock { + sigil_quote!(RustLang { + $if(is_binary) { + multipart_body.extend_from_slice(&$L(binary_value_expr)); + } $else { + multipart_body.extend_from_slice($L(text_value_expr).as_bytes()); + } + }) + .expect("multipart part value builds") +} + +fn multipart_part_crlf() -> CodeBlock { + sigil_quote!(RustLang { + multipart_body.extend_from_slice(b"\r\n"); + }) + .expect("multipart part CRLF builds") +} + fn emit_part_prefix( b: &mut sigil_stitch::code_block::CodeBlockBuilder, wire_name: &str, @@ -441,24 +542,35 @@ fn emit_part_prefix( content_type: &str, filename_expr: Option<&str>, ) { - b.add( - "multipart_body.extend_from_slice(format!(\"--{}\\r\\n\", boundary).as_bytes());\n", - (), - ); - if is_binary { - let filename_expr = filename_expr.expect("binary multipart part filename"); - b.add( - &format!( - "multipart_body.extend_from_slice(format!(\"Content-Disposition: form-data; name=\\\"{{}}\\\"; filename=\\\"{{}}\\\"\\r\\nContent-Type: {{}}\\r\\n\\r\\n\", crate::runtime::multipart_header_value({wire_name}), crate::runtime::multipart_header_value(&{filename_expr}), {content_type}).as_bytes());\n", - ), - (), - ); - } else { - b.add( - &format!( - "multipart_body.extend_from_slice(format!(\"Content-Disposition: form-data; name=\\\"{{}}\\\"\\r\\nContent-Type: {{}}\\r\\n\\r\\n\", crate::runtime::multipart_header_value({wire_name}), {content_type}).as_bytes());\n", - ), - (), - ); - } + b.add_code(multipart_part_boundary()); + let filename_expr = filename_expr.unwrap_or_default(); + b.add_code(multipart_part_headers( + is_binary, + wire_name, + filename_expr, + content_type, + )); +} + +fn multipart_part_boundary() -> CodeBlock { + sigil_quote!(RustLang { + multipart_body.extend_from_slice(format!("--{}\r\n", boundary).as_bytes()); + }) + .expect("multipart part boundary builds") +} + +fn multipart_part_headers( + is_binary: bool, + wire_name: &str, + filename_expr: &str, + content_type: &str, +) -> CodeBlock { + sigil_quote!(RustLang { + $if(is_binary) { + multipart_body.extend_from_slice(format!("Content-Disposition: form-data; name=\"{}\"; filename=\"{}\"\r\nContent-Type: {}\r\n\r\n", crate::runtime::multipart_header_value($L(wire_name)), crate::runtime::multipart_header_value(&$L(filename_expr)), $L(content_type)).as_bytes()); + } $else { + multipart_body.extend_from_slice(format!("Content-Disposition: form-data; name=\"{}\"\r\nContent-Type: {}\r\n\r\n", crate::runtime::multipart_header_value($L(wire_name)), $L(content_type)).as_bytes()); + } + }) + .expect("multipart part headers build") } diff --git a/src/generators/typescript/fetch/sigil_emit_api.rs b/src/generators/typescript/fetch/sigil_emit_api.rs index d1dde54d..bd56338d 100644 --- a/src/generators/typescript/fetch/sigil_emit_api.rs +++ b/src/generators/typescript/fetch/sigil_emit_api.rs @@ -607,10 +607,16 @@ fn emit_url_path(cb: &mut sigil_stitch::code_block::CodeBlockBuilder, op: &IrOpe .parameters .iter() .any(|p| matches!(p.location, IrParameterLocation::Path)); - let binding = if has_path_params { "let" } else { "const" }; - cb.add( - &format!("{binding} urlPath = %V;\n"), - vec![Arg::VerbatimStr(op.path.clone())], + let path = op.path.as_str(); + cb.add_code( + sigil_quote!(TypeScript { + $if(has_path_params) { + let urlPath = $V(path); + } $else { + const urlPath = $V(path); + } + }) + .expect("url path declaration builds"), ); let names = resolve_param_names(op); @@ -643,12 +649,14 @@ fn emit_query_params(cb: &mut sigil_stitch::code_block::CodeBlockBuilder, op: &I { let resolved = resolved_param(&names, p); let access = request_parameters_access(&resolved); - cb.add( - &format!( - "if ({0} !== undefined) {{\n queryParameters['{1}'] = {0};\n}}\n", - access, p.name - ), - vec![], + let key = format!("'{}'", p.name); + cb.add_code( + sigil_quote!(TypeScript { + if ($L(access.as_str()) !== undefined) { + queryParameters[$L(key.as_str())] = $L(access.as_str()); + } + }) + .expect("query parameter guard builds"), ); } } @@ -674,12 +682,14 @@ fn emit_headers(cb: &mut sigil_stitch::code_block::CodeBlockBuilder, op: &IrOper { let resolved = resolved_param(&names, p); let access = request_parameters_access(&resolved); - cb.add( - &format!( - "if ({0} !== undefined) {{\n headerParameters['{1}'] = String({0});\n}}\n", - access, p.name - ), - vec![], + let key = format!("'{}'", p.name); + cb.add_code( + sigil_quote!(TypeScript { + if ($L(access.as_str()) !== undefined) { + headerParameters[$L(key.as_str())] = String($L(access.as_str())); + } + }) + .expect("header parameter guard builds"), ); } } @@ -781,21 +791,16 @@ fn emit_unsupported_ts_body( required: bool, message: &str, ) { - if required { - cb.add_code( - sigil_quote!(TypeScript { + cb.add_code( + sigil_quote!(TypeScript { + $if(required) { const requestBody = (() => { throw new Error($S(message)); })(); - }) - .expect("unsupported request body block builds"), - ); - } else { - cb.add_code( - sigil_quote!(TypeScript { + } $else { const requestBody = $L(access) === undefined || $L(access) === null ? undefined : (() => { throw new Error($S(message)); })(); - }) - .expect("optional unsupported request body block builds"), - ); - } + } + }) + .expect("unsupported request body block builds"), + ); } fn is_unsupported_ts_request_media_type(media_type: &str) -> bool { @@ -816,23 +821,17 @@ fn emit_multipart_blob_setup(cb: &mut sigil_stitch::code_block::CodeBlockBuilder fn emit_multipart_blob_finish(cb: &mut sigil_stitch::code_block::CodeBlockBuilder, required: bool) { let closing_boundary_tail = ts_string_literal("--\r\n"); - if required { - cb.add_code( - sigil_quote!(TypeScript { - multipartChunks.push($S("--") + multipartBoundary + $L(closing_boundary_tail)); + cb.add_code( + sigil_quote!(TypeScript { + multipartChunks.push($S("--") + multipartBoundary + $L(closing_boundary_tail)); + $if(required) { const requestBody = new Blob(multipartChunks); - }) - .expect("required multipart finish block builds"), - ); - } else { - cb.add_code( - sigil_quote!(TypeScript { - multipartChunks.push($S("--") + multipartBoundary + $L(closing_boundary_tail)); + } $else { requestBody = new Blob(multipartChunks); - }) - .expect("optional multipart finish block builds"), - ); - } + } + }) + .expect("multipart finish block builds"), + ); } fn emit_multipart_blob_part( @@ -931,15 +930,26 @@ fn emit_make_request( has_body: bool, ) { let method = op.method.to_uppercase(); - let body_expr = if has_body { "requestBody" } else { "undefined" }; + let request_with_body = format!( + "const response = await this.request({{\n path: urlPath,\n method: '{}',\n headers: headerParameters,\n query: queryParameters,\n body: requestBody,\n}}, initOverrides);", + method + ); + let request_without_body = format!( + "const response = await this.request({{\n path: urlPath,\n method: '{}',\n headers: headerParameters,\n query: queryParameters,\n body: undefined,\n}}, initOverrides);", + method + ); cb.add("// Make request\n", vec![]); - cb.add( - &format!( - "const response = await this.request({{\n path: urlPath,\n method: '{}',\n headers: headerParameters,\n query: queryParameters,\n body: {},\n}}, initOverrides);\n\n", - method, body_expr - ), - vec![], + cb.add_code( + sigil_quote!(TypeScript { + $if(has_body) { + $L(request_with_body) + } $else { + $L(request_without_body) + } + }) + .expect("make request block builds"), ); + cb.add_line(); } fn emit_response_handler( @@ -1094,24 +1104,21 @@ fn emit_fallback_return( any_body: bool, _inside_block: bool, ) { - if any_body { - cb.add( - "return new %T(response) as %T<%T> & { status: number };\n", - vec![ - Arg::TypeName(rt_value("JSONApiResponse")), - Arg::TypeName(rt_value("JSONApiResponse")), - Arg::TypeName(TypeName::primitive("unknown")), - ], - ); - } else { - cb.add( - "return new %T(response) as %T & { status: number };\n", - vec![ - Arg::TypeName(rt_value("VoidApiResponse")), - Arg::TypeName(rt_value("VoidApiResponse")), - ], - ); - } + let json_response = rt_value("JSONApiResponse"); + let void_response = rt_value("VoidApiResponse"); + let unknown = TypeName::primitive("unknown"); + let json_status_shape = TypeName::raw(" { status: number }"); + let void_status_shape = TypeName::raw("{ status: number }"); + cb.add_code( + sigil_quote!(TypeScript { + $if(any_body) { + return new $T(json_response.clone())(response) as $T(json_response)<$T(unknown)> & $T(json_status_shape); + } $else { + return new $T(void_response.clone())(response) as $T(void_response) & $T(void_status_shape); + } + }) + .expect("fallback response return block builds"), + ); } // ============================================================================ @@ -1136,19 +1143,15 @@ fn build_convenience_method(op: &IrOperation) -> Result { let args_list = raw_call_args(op); let is_void = is_void_type(&body_ty); let call_expr = format!("this.{raw_name}({args_list})"); - let body = if is_void { - sigil_quote!(TypeScript { - const response = await $L(call_expr.clone()); + let body = sigil_quote!(TypeScript { + const response = await $L(call_expr); + $if(is_void) { return await response.value(); - }) - .expect("CodeBlock builds") - } else { - sigil_quote!(TypeScript { - const response = await $L(call_expr); + } $else { return await response.value() as $T(body_ty); - }) - .expect("CodeBlock builds") - }; + } + }) + .expect("CodeBlock builds"); fb = fb.body(body); fb.build()