Packages
oaspec
0.48.0
0.68.0
0.67.0
0.66.0
0.65.0
0.64.0
0.63.0
0.62.0
0.61.0
0.60.0
0.59.0
0.58.1
0.58.0
0.57.0
0.56.0
0.55.0
0.54.0
0.53.0
0.52.0
0.51.0
0.50.0
0.49.0
0.48.0
0.47.0
0.46.0
0.45.0
0.44.0
0.43.0
0.42.0
0.41.0
0.40.0
0.39.0
0.38.0
0.37.0
0.36.0
0.35.0
0.34.0
0.33.0
0.32.0
0.31.0
0.30.0
0.29.0
0.28.0
0.27.0
0.26.0
0.25.0
0.24.0
0.23.0
0.22.0
0.21.0
0.20.0
0.19.0
0.18.0
0.17.0
0.16.0
0.15.0
0.14.0
0.13.0
0.12.0
0.11.0
0.10.0
0.9.0
0.8.0
0.7.0
0.6.3
0.6.1
0.6.0
0.5.0
0.4.0
0.3.0
0.1.3
Generate Gleam code from OpenAPI 3.x specifications
Current section
Files
Jump to
Current section
Files
src/oaspec/internal/codegen/client.gleam
import gleam/bool
import gleam/dict
import gleam/list
import gleam/option.{None, Some}
import gleam/string
import oaspec/config
import oaspec/internal/codegen/client_ir
import oaspec/internal/codegen/client_request
import oaspec/internal/codegen/client_response
import oaspec/internal/codegen/client_security
import oaspec/internal/codegen/context.{
type Context, type GeneratedFile, GeneratedFile,
}
import oaspec/internal/codegen/guards
import oaspec/internal/codegen/ir_build
import oaspec/internal/codegen/runtime_snippets
import oaspec/internal/openapi/schema
import oaspec/internal/openapi/spec.{type Resolved, Value}
import oaspec/internal/util/http
import oaspec/internal/util/naming
import oaspec/internal/util/string_extra as se
/// Generate client SDK files.
pub fn generate(ctx: Context) -> List(GeneratedFile) {
let client_content = generate_client(ctx)
[
GeneratedFile(
path: "client.gleam",
content: client_content,
target: context.ClientTarget,
write_mode: context.Overwrite,
),
]
}
/// Generate the client module with functions for each operation.
fn generate_client(ctx: Context) -> String {
let operations = context.operations(ctx)
let requirements = client_ir.analyze(ctx)
let imports = client_ir.imports(requirements, ctx)
let sb =
se.file_header(context.version)
|> se.imports(imports)
// ClientError type
let sb =
sb
|> se.doc_comment("HTTP client errors.")
|> se.line("pub type ClientError {")
|> se.indent(1, "TransportError(error: transport.TransportError)")
|> se.indent(1, "DecodeFailure(detail: String)")
|> se.indent(1, "InvalidResponse(detail: String)")
|> se.indent(1, "UnexpectedStatus(")
|> se.indent(2, "status: Int,")
|> se.indent(2, "headers: List(#(String, String)),")
|> se.indent(2, "body: transport.Body,")
|> se.indent(1, ")")
let sb = case requirements.needs_guards {
True ->
sb
|> se.indent(1, "ValidationError(errors: List(guards.ValidationFailure))")
False -> sb
}
let sb =
sb
|> se.line("}")
|> se.blank_line()
// default_base_url
let sb = generate_default_base_url(sb, ctx)
// text_body / bytes_body helpers
let sb = case requirements.needs_text_helper {
True -> generate_text_body_helper(sb)
False -> sb
}
let sb = case requirements.needs_bytes_helper {
True -> generate_bytes_body_helper(sb)
False -> sb
}
let sb = case operations {
[] -> sb
_ -> generate_async_response_helper(sb)
}
// Operation functions
let sb =
list.fold(operations, sb, fn(sb, op) {
let #(op_id, operation, path, method) = op
generate_client_function(sb, op_id, operation, path, method, ctx)
})
se.to_string(sb)
}
/// Substitute server variable placeholders.
fn substitute_server_variables(
url: String,
variables: List(#(String, spec.ServerVariable)),
) -> String {
list.fold(variables, url, fn(acc, entry) {
let #(name, variable) = entry
string.replace(acc, "{" <> name <> "}", variable.default)
})
}
/// Generate the default_base_url function (only when servers are declared).
fn generate_default_base_url(
sb: se.StringBuilder,
ctx: Context,
) -> se.StringBuilder {
case context.spec(ctx).servers {
[first_server, ..] -> {
let variables = ir_build.sorted_entries(first_server.variables)
let resolved_url =
substitute_server_variables(first_server.url, variables)
let defaults_doc = case variables {
[] -> ""
_ ->
"Defaults: "
<> string.join(
list.map(variables, fn(entry) {
let #(name, variable) = entry
name <> " = \"" <> variable.default <> "\""
}),
", ",
)
}
let sb = case defaults_doc {
"" ->
sb
|> se.doc_comment("Default base URL declared by the OpenAPI spec.")
doc ->
sb
|> se.doc_comment("Default base URL declared by the OpenAPI spec.")
|> se.doc_comment(doc)
}
sb
|> se.line("pub fn default_base_url() -> String {")
|> se.indent(1, "\"" <> resolved_url <> "\"")
|> se.line("}")
|> se.blank_line()
}
[] -> sb
}
}
/// Emit the text_body helper that extracts a UTF-8 string from a
/// `transport.Body`, surfacing `InvalidResponse` for non-text bodies.
fn generate_text_body_helper(sb: se.StringBuilder) -> se.StringBuilder {
se.raw(sb, runtime_snippets.text_body)
}
/// Emit the bytes_body helper for binary response bodies.
fn generate_bytes_body_helper(sb: se.StringBuilder) -> se.StringBuilder {
se.raw(sb, runtime_snippets.bytes_body)
}
/// Emit the async helper that maps an async transport result into the
/// generated response type.
fn generate_async_response_helper(sb: se.StringBuilder) -> se.StringBuilder {
se.raw(sb, runtime_snippets.await_response)
}
fn emit_simple_query_param(
sb: se.StringBuilder,
p: spec.Parameter(Resolved),
param_name: String,
ctx: Context,
) -> se.StringBuilder {
case p.required {
True -> {
let to_str = client_request.param_to_string_expr(p, param_name, ctx)
sb
|> se.indent(
1,
"let query = [#(\"" <> p.name <> "\", " <> to_str <> "), ..query]",
)
}
False -> {
let to_str = client_request.to_str_for_optional_value(p, ctx)
sb
|> se.indent(1, "let query = case " <> param_name <> " {")
|> se.indent(
2,
"Some(v) -> [#(\"" <> p.name <> "\", " <> to_str <> "), ..query]",
)
|> se.indent(2, "None -> query")
|> se.indent(1, "}")
}
}
}
fn http_method_to_transport(method: spec.HttpMethod) -> String {
case method {
spec.Get -> "transport.Get"
spec.Post -> "transport.Post"
spec.Put -> "transport.Put"
spec.Delete -> "transport.Delete"
spec.Patch -> "transport.Patch"
spec.Head -> "transport.Head"
spec.Options -> "transport.Options"
spec.Trace -> "transport.Trace"
}
}
/// Generate the client functions for a single operation: sync and async call
/// helpers, request builder, response decoder, and request-record wrappers.
fn generate_client_function(
sb: se.StringBuilder,
op_id: String,
operation: spec.Operation(Resolved),
path: String,
method: spec.HttpMethod,
ctx: Context,
) -> se.StringBuilder {
let fn_name = naming.operation_to_function_name(op_id)
let build_fn = "build_" <> fn_name <> "_request"
let decode_fn = "decode_" <> fn_name <> "_response"
let response_type = naming.schema_to_type_name(op_id) <> "Response"
let unwrapped_params =
list.filter_map(operation.parameters, fn(ref_p) {
case ref_p {
Value(p) -> Ok(p)
_ -> Error(Nil)
}
})
let path_params =
list.filter(unwrapped_params, fn(p) { p.in_ == spec.InPath })
let query_params =
list.filter(unwrapped_params, fn(p) { p.in_ == spec.InQuery })
let header_params =
list.filter(unwrapped_params, fn(p) { p.in_ == spec.InHeader })
let cookie_params =
list.filter(unwrapped_params, fn(p) { p.in_ == spec.InCookie })
let params_signature =
client_request.build_param_list(
path_params,
query_params,
header_params,
cookie_params,
operation,
op_id,
ctx,
)
let call_args =
build_call_args(
path_params,
query_params,
header_params,
cookie_params,
operation,
ctx,
)
// ------------------------------------------------------------
// 1. The send-first composition function
// ------------------------------------------------------------
let sb = case operation.summary {
Some(summary) -> sb |> se.doc_comment(summary)
_ -> sb
}
let sb = case operation.description {
Some(desc) -> sb |> se.doc_comment(desc)
_ -> sb
}
let sb = case operation.request_body {
Some(Value(rb)) -> {
let content_entries = ir_build.sorted_entries(rb.content)
case content_entries {
[_, _, ..] -> {
let ct_names =
list.map(content_entries, fn(e) { e.0 })
|> string.join(", ")
sb
|> se.doc_comment("Supported content types: " <> ct_names)
}
_ -> sb
}
}
_ -> sb
}
let sb =
sb
|> se.line(
"pub fn "
<> fn_name
<> "(send send: transport.Send"
<> params_signature
<> ") -> Result(response_types."
<> response_type
<> ", ClientError) {",
)
// Validation is handled inside build_<op>_request; here we just wire
// build → send → decode.
let sb =
sb
|> se.indent(1, "use req <- result.try(" <> build_fn <> call_args <> ")")
|> se.indent(1, "use resp <- result.try(")
|> se.indent(2, "send(req)")
|> se.indent(2, "|> result.map_error(TransportError),")
|> se.indent(1, ")")
|> se.indent(1, decode_fn <> "(resp)")
|> se.line("}")
|> se.blank_line()
let sb =
sb
|> se.doc_comment(
"Async transport variant of "
<> fn_name
<> ". Resolves to the typed response or a client error.",
)
|> se.line(
"pub fn "
<> fn_name
<> "_async(async_send async_send: transport.AsyncSend"
<> params_signature
<> ") -> transport.Async(Result(response_types."
<> response_type
<> ", ClientError)) {",
)
|> se.indent(1, "case " <> build_fn <> call_args <> " {")
|> se.indent(
2,
"Ok(req) -> await_response(async_send(req), " <> decode_fn <> ")",
)
|> se.indent(2, "Error(error) -> transport.resolve(Error(error))")
|> se.indent(1, "}")
|> se.line("}")
|> se.blank_line()
// ------------------------------------------------------------
// 2. build_<op>_request
// ------------------------------------------------------------
let sb =
sb
|> se.doc_comment(
"Build the transport request for "
<> fn_name
<> " without sending it. Useful for testing and for adding custom transport middleware.",
)
|> se.line(
"pub fn "
<> build_fn
<> "("
<> trim_leading_comma(params_signature)
<> ") -> Result(transport.Request, ClientError) {",
)
let sb =
generate_build_body(
sb,
op_id,
operation,
path,
method,
path_params,
query_params,
header_params,
cookie_params,
ctx,
)
let sb = sb |> se.line("}") |> se.blank_line()
// ------------------------------------------------------------
// 3. decode_<op>_response
// ------------------------------------------------------------
let sb =
sb
|> se.doc_comment(
"Decode a transport response into the typed response variant for "
<> fn_name
<> ".",
)
|> se.line(
"pub fn "
<> decode_fn
<> "(resp: transport.Response) -> Result(response_types."
<> response_type
<> ", ClientError) {",
)
|> se.indent(1, "case resp.status {")
let responses = http.sort_response_entries(dict.to_list(operation.responses))
let sb =
list.fold(responses, sb, fn(sb, entry) {
let #(status_code, ref_or) = entry
case ref_or {
Value(response) -> {
let variant_name =
"response_types."
<> naming.schema_to_type_name(op_id)
<> "Response"
<> http.status_code_suffix(status_code)
let content_entries = ir_build.sorted_entries(response.content)
// Issue #387: when the response declares headers, the
// response variant constructor takes an extra typed headers
// record. Build the headers descriptor here and thread it
// through to every variant-emitting branch so each branch
// can emit the per-field extraction (let / use chain) and
// pass `decoded, headers` in the right order.
let headers_record =
client_response.build_headers_record(op_id, status_code, response)
case content_entries {
[] ->
client_response.generate_empty_content_response(
sb,
status_code,
variant_name,
headers_record,
)
[#(single_ct, single_mt)] ->
client_response.generate_single_content_response(
sb,
status_code,
variant_name,
single_ct,
single_mt,
op_id,
ctx,
headers_record,
)
multiple ->
client_response.generate_multi_content_response(
sb,
status_code,
variant_name,
multiple,
op_id,
ctx,
headers_record,
)
}
}
_ -> sb
}
})
let has_default =
list.any(responses, fn(entry) {
let #(code, _) = entry
code == http.DefaultStatus
})
let sb = case has_default {
True -> sb
False ->
sb
|> se.indent(2, "_ ->")
|> se.indent(3, "Error(UnexpectedStatus(")
|> se.indent(4, "status: resp.status,")
|> se.indent(4, "headers: resp.headers,")
|> se.indent(4, "body: resp.body,")
|> se.indent(3, "))")
}
let sb =
sb
|> se.indent(1, "}")
|> se.line("}")
|> se.blank_line()
// ------------------------------------------------------------
// 4. <op>_with_request wrapper(s)
// ------------------------------------------------------------
case
client_request.build_request_object_call_args(
path_params,
query_params,
header_params,
cookie_params,
operation,
)
{
Some(call_args2) -> {
let request_type = naming.schema_to_type_name(op_id) <> "Request"
let extra = case call_args2 {
"" -> ""
_ -> ", " <> call_args2
}
sb
|> se.doc_comment(
"Request-object wrapper. Delegates to "
<> fn_name
<> " with fields unpacked from the request record.",
)
|> se.line(
"pub fn "
<> fn_name
<> "_with_request(send send: transport.Send, request request: request_types."
<> request_type
<> ") -> Result(response_types."
<> response_type
<> ", ClientError) {",
)
|> se.indent(1, fn_name <> "(send" <> rebind_request_fields(extra) <> ")")
|> se.line("}")
|> se.blank_line()
|> se.doc_comment(
"Async request-object wrapper. Delegates to "
<> fn_name
<> "_async with fields unpacked from the request record.",
)
|> se.line(
"pub fn "
<> fn_name
<> "_with_request_async(async_send async_send: transport.AsyncSend, request request: request_types."
<> request_type
<> ") -> transport.Async(Result(response_types."
<> response_type
<> ", ClientError)) {",
)
|> se.indent(
1,
fn_name <> "_async(async_send" <> rebind_request_fields(extra) <> ")",
)
|> se.line("}")
|> se.blank_line()
}
None -> sb
}
}
/// Build the call-args list passed from `<op>` to `build_<op>_request`.
fn build_call_args(
path_params: List(spec.Parameter(Resolved)),
query_params: List(spec.Parameter(Resolved)),
header_params: List(spec.Parameter(Resolved)),
cookie_params: List(spec.Parameter(Resolved)),
operation: spec.Operation(Resolved),
_ctx: Context,
) -> String {
let all_params =
list.append(path_params, query_params)
|> list.append(header_params)
|> list.append(cookie_params)
let field_names = client_request.build_param_field_names(operation)
let names =
list.map(all_params, fn(p) {
let field = client_request.field_name_for(field_names, p)
field <> ": " <> field
})
let body_args = case operation.request_body {
Some(Value(rb)) -> {
let content_entries = ir_build.sorted_entries(rb.content)
case content_entries {
[_, _, ..] -> ["content_type: content_type", "body: body"]
_ -> ["body: body"]
}
}
_ -> []
}
let all = list.append(names, body_args)
case all {
[] -> "()"
_ -> "(" <> string.join(all, ", ") <> ")"
}
}
/// Replace `req.field` references in the rebuilt call args with the
/// `request.` prefix used by the `_with_request` wrapper. The shared
/// helper builds `req.foo, req.bar`; we keep that token shape but just
/// rename the prefix.
fn rebind_request_fields(extra: String) -> String {
string.replace(extra, "req.", "request.")
}
/// Trim a leading ", " from a parameter list snippet so it can stand on
/// its own as the argument list of `build_<op>_request`.
fn trim_leading_comma(s: String) -> String {
use <- bool.guard(!string.starts_with(s, ", "), s)
string.drop_start(s, 2)
}
/// Body of `build_<op>_request`. Validates request body, builds path /
/// query / headers / body / security, and assembles the
/// `transport.Request` literal.
fn generate_build_body(
sb: se.StringBuilder,
op_id: String,
operation: spec.Operation(Resolved),
path: String,
method: spec.HttpMethod,
path_params: List(spec.Parameter(Resolved)),
query_params: List(spec.Parameter(Resolved)),
header_params: List(spec.Parameter(Resolved)),
cookie_params: List(spec.Parameter(Resolved)),
ctx: Context,
) -> se.StringBuilder {
let field_names = client_request.build_param_field_names(operation)
// Validation guard.
let client_guard_schema = case
config.validate(context.config(ctx)),
operation.request_body
{
True, Some(Value(rb)) -> {
let content_entries = dict.to_list(rb.content)
case content_entries {
[#(_, mt)] ->
case mt.schema {
Some(schema.Reference(name:, ..)) ->
case guards.schema_has_validator(name, ctx) {
True -> Some(#(name, rb.required))
False -> None
}
_ -> None
}
_ -> None
}
}
_, _ -> None
}
let sb = case client_guard_schema {
Some(#(name, True)) -> {
let validate_fn =
"guards.validate_" <> naming.to_snake_case(name) <> "(body)"
sb
|> se.indent(1, "case " <> validate_fn <> " {")
|> se.indent(2, "Error(errors) -> Error(ValidationError(errors:))")
|> se.indent(2, "Ok(_) -> {")
}
Some(#(name, False)) -> {
let validate_fn = "guards.validate_" <> naming.to_snake_case(name)
sb
|> se.indent(1, "let validation_errors = case body {")
|> se.indent(2, "Some(b) -> case " <> validate_fn <> "(b) {")
|> se.indent(3, "Error(errors) -> errors")
|> se.indent(3, "Ok(_) -> []")
|> se.indent(2, "}")
|> se.indent(2, "None -> []")
|> se.indent(1, "}")
|> se.indent(1, "case validation_errors {")
|> se.indent(
2,
"[_, ..] -> Error(ValidationError(errors: validation_errors))",
)
|> se.indent(2, "[] -> {")
}
None -> sb
}
// Determine effective base URL.
let effective_server_url = case operation.servers {
[first_server, ..] -> {
let variables = ir_build.sorted_entries(first_server.variables)
let resolved = substitute_server_variables(first_server.url, variables)
Some(resolved)
}
[] ->
case context.spec(ctx).servers {
[_, ..] -> Some("default_base_url()")
[] -> None
}
}
// Wrap the path-parameter substitution in a let.
let sb = sb |> se.indent(1, "let path = \"" <> path <> "\"")
let sb =
list.fold(path_params, sb, fn(sb, p) {
let param_name = client_request.field_name_for(field_names, p)
let to_string_expr =
client_request.param_to_string_expr(p, param_name, ctx)
sb
|> se.indent(
1,
"let path = string.replace(path, \"{"
<> p.name
<> "}\", uri.percent_encode("
<> to_string_expr
<> "))",
)
})
// Query as List(#(String, String)) — raw values, adapter encodes.
let sb = case list.is_empty(query_params) {
True -> sb |> se.indent(1, "let query = []")
False -> {
let sb = sb |> se.indent(1, "let query = []")
let sb =
list.fold(query_params, sb, fn(sb, p) {
let param_name = client_request.field_name_for(field_names, p)
case client_request.is_exploded_array_param(p, ctx) {
True ->
client_request.generate_exploded_array_query_param(
sb,
p,
param_name,
ctx,
)
False ->
case client_request.is_delimited_array_param(p, ctx) {
Some(joiner) ->
client_request.generate_delimited_array_query_param(
sb,
p,
param_name,
joiner,
ctx,
)
None -> emit_simple_query_param(sb, p, param_name, ctx)
}
}
})
sb |> se.indent(1, "let query = list.reverse(query)")
}
}
// Headers.
let sb = sb |> se.indent(1, "let headers = []")
let sb =
list.fold(header_params, sb, fn(sb, p) {
let param_name = client_request.field_name_for(field_names, p)
let header_name = string.lowercase(p.name)
case p.required {
True -> {
let to_str = client_request.param_to_string_expr(p, param_name, ctx)
sb
|> se.indent(
1,
"let headers = [#(\""
<> header_name
<> "\", "
<> to_str
<> "), ..headers]",
)
}
False -> {
let to_str = client_request.to_str_for_optional_value(p, ctx)
sb
|> se.indent(1, "let headers = case " <> param_name <> " {")
|> se.indent(
2,
"Some(v) -> [#(\""
<> header_name
<> "\", "
<> to_str
<> "), ..headers]",
)
|> se.indent(2, "None -> headers")
|> se.indent(1, "}")
}
}
})
// Cookies bundled into a single "cookie" header.
let sb = case list.is_empty(cookie_params) {
True -> sb
False -> {
let sb = sb |> se.indent(1, "let cookie_parts = []")
let sb =
list.fold(cookie_params, sb, fn(sb, p) {
let param_name = client_request.field_name_for(field_names, p)
case p.required {
True -> {
let to_str =
client_request.param_to_string_expr(p, param_name, ctx)
sb
|> se.indent(
1,
"let cookie_parts = [\""
<> p.name
<> "=\" <> uri.percent_encode("
<> to_str
<> "), ..cookie_parts]",
)
}
False -> {
let to_str = client_request.to_str_for_optional_value(p, ctx)
sb
|> se.indent(1, "let cookie_parts = case " <> param_name <> " {")
|> se.indent(
2,
"Some(v) -> [\""
<> p.name
<> "=\" <> uri.percent_encode("
<> to_str
<> "), ..cookie_parts]",
)
|> se.indent(2, "None -> cookie_parts")
|> se.indent(1, "}")
}
}
})
sb
|> se.indent(1, "let headers = case cookie_parts {")
|> se.indent(2, "[] -> headers")
|> se.indent(
2,
"_ -> [#(\"cookie\", string.join(cookie_parts, \"; \")), ..headers]",
)
|> se.indent(1, "}")
}
}
// Reverse only when something was prepended; an empty list is a
// no-op for `list.reverse` but still requires the import.
let needs_reverse =
!list.is_empty(header_params) || !list.is_empty(cookie_params)
let sb = case needs_reverse {
True -> sb |> se.indent(1, "let headers = list.reverse(headers)")
False -> sb
}
// Body.
let sb = case operation.request_body {
Some(Value(rb)) -> generate_body_emission(sb, rb, op_id, ctx)
_ -> sb |> se.indent(1, "let body = transport.EmptyBody")
}
// Body content-type header.
let sb = case operation.request_body {
Some(Value(rb)) -> {
let content_entries = ir_build.sorted_entries(rb.content)
let ct_expr = case content_entries {
// Multi-content: user-provided value via `content_type:` arg.
[_, _, ..] -> "content_type"
// multipart/form-urlencoded: helper left `body_content_type`
// in scope (boundary-augmented for multipart).
[#("multipart/form-data", _)] -> "body_content_type"
[#("application/x-www-form-urlencoded", _)] -> "body_content_type"
[#(static_ct, _)] -> "\"" <> static_ct <> "\""
[] -> ""
}
case ct_expr {
"" -> sb
ct -> {
let header_line = "[#(\"content-type\", " <> ct <> "), ..headers]"
case rb.required {
True -> sb |> se.indent(1, "let headers = " <> header_line)
False ->
sb
|> se.indent(1, "let headers = case body {")
|> se.indent(2, "transport.EmptyBody -> headers")
|> se.indent(2, "_ -> " <> header_line)
|> se.indent(1, "}")
}
}
}
}
_ -> sb
}
// Base URL.
let base_url_expr = case effective_server_url {
Some("default_base_url()") -> "Some(default_base_url())"
Some(url) -> "Some(\"" <> url <> "\")"
None -> "None"
}
// Security metadata.
let effective_security = case operation.security {
Some(sec) -> sec
None -> context.spec(ctx).security
}
let security_literal =
client_security.render_security_metadata(ctx, effective_security)
let http_method = http_method_to_transport(method)
// Assemble the request literal.
let sb =
sb
|> se.indent(1, "Ok(transport.Request(")
|> se.indent(2, "method: " <> http_method <> ",")
|> se.indent(2, "base_url: " <> base_url_expr <> ",")
|> se.indent(2, "path: path,")
|> se.indent(2, "query: query,")
|> se.indent(2, "headers: headers,")
|> se.indent(2, "body: body,")
|> se.indent(2, "security: " <> security_literal <> ",")
|> se.indent(1, "))")
// Close validation wrapper.
case client_guard_schema {
Some(_) ->
sb
|> se.indent(1, "}")
|> se.indent(1, "}")
None -> sb
}
}
/// Emit the body construction. Sets `body: transport.Body` in the
/// generated scope. Multi-content uses the user-provided `body: String`
/// directly; single-content encodes per its schema.
fn generate_body_emission(
sb: se.StringBuilder,
rb: spec.RequestBody(Resolved),
op_id: String,
ctx: Context,
) -> se.StringBuilder {
let content_entries = ir_build.sorted_entries(rb.content)
case content_entries {
[_, _, ..] -> {
case rb.required {
True -> sb |> se.indent(1, "let body = transport.TextBody(body)")
False ->
sb
|> se.indent(1, "let body = case body {")
|> se.indent(2, "Some(b) -> transport.TextBody(b)")
|> se.indent(2, "None -> transport.EmptyBody")
|> se.indent(1, "}")
}
}
[#(content_type_key, _)] ->
case content_type_key {
"application/octet-stream" ->
case rb.required {
True -> sb |> se.indent(1, "let body = transport.BytesBody(body)")
False ->
sb
|> se.indent(1, "let body = case body {")
|> se.indent(2, "Some(b) -> transport.BytesBody(b)")
|> se.indent(2, "None -> transport.EmptyBody")
|> se.indent(1, "}")
}
"multipart/form-data" ->
generate_multipart_body_emission(sb, rb, op_id, ctx)
"application/x-www-form-urlencoded" ->
generate_form_urlencoded_body_emission(sb, rb, op_id, ctx)
"text/plain" ->
// Plain-text bodies must travel as the raw string the caller
// supplies. The default `_ ->` arm runs the value through the
// schema-aware encoder, which would JSON-quote a string schema —
// that's wrong for text/plain (the wire payload would be
// `"foo"` instead of `foo`).
case rb.required {
True -> sb |> se.indent(1, "let body = transport.TextBody(body)")
False ->
sb
|> se.indent(1, "let body = case body {")
|> se.indent(2, "Some(b) -> transport.TextBody(b)")
|> se.indent(2, "None -> transport.EmptyBody")
|> se.indent(1, "}")
}
_ -> {
let encode_expr = client_request.get_body_encode_expr(rb, op_id, ctx)
case rb.required {
True ->
sb
|> se.indent(
1,
"let body = transport.TextBody(" <> encode_expr <> ")",
)
False ->
sb
|> se.indent(1, "let body = case body {")
|> se.indent(
2,
"Some(body) -> transport.TextBody(" <> encode_expr <> ")",
)
|> se.indent(2, "None -> transport.EmptyBody")
|> se.indent(1, "}")
}
}
}
[] -> sb |> se.indent(1, "let body = transport.EmptyBody")
}
}
fn generate_multipart_body_emission(
sb: se.StringBuilder,
rb: spec.RequestBody(Resolved),
op_id: String,
ctx: Context,
) -> se.StringBuilder {
// Reuse the existing multipart string builder, but redirect its final
// assembly to a `body` value rather than calling `request.set_body`.
let sb = client_request.generate_multipart_body(sb, rb, op_id, ctx)
// The helper leaves `body_str` and `boundary` in scope.
sb
|> se.indent(1, "let body = transport.TextBody(body_str)")
}
fn generate_form_urlencoded_body_emission(
sb: se.StringBuilder,
rb: spec.RequestBody(Resolved),
op_id: String,
ctx: Context,
) -> se.StringBuilder {
let sb = client_request.generate_form_urlencoded_body(sb, rb, op_id, ctx)
sb
|> se.indent(1, "let body = transport.TextBody(body_str)")
}