Current section

Files

Jump to
oaspec src oaspec codegen server_request_decode.gleam
Raw

src/oaspec/codegen/server_request_decode.gleam

import gleam/bool
import gleam/dict
import gleam/list
import gleam/option.{type Option, None, Some}
import gleam/string
import oaspec/codegen/context.{type Context}
import oaspec/codegen/ir_build
import oaspec/codegen/operation_ir
import oaspec/openapi/resolver
import oaspec/openapi/schema.{
type AdditionalProperties, type SchemaRef, Forbidden, Inline, ObjectSchema,
Reference, Typed, Unspecified, Untyped,
}
import oaspec/openapi/spec.{type Resolved, Value}
import oaspec/util/content_type
import oaspec/util/naming
/// Expression that case-insensitively parses a string to Bool.
/// Accepts "true"/"True"/"TRUE" etc. as True, everything else as False.
/// This is compatible with Gleam's bool.to_string which produces "True"/"False".
const bool_parse_expr = "case string.lowercase(v) { \"true\" -> True _ -> False }"
type DeepObjectProperty {
DeepObjectProperty(
name: String,
field_name: String,
schema_ref: SchemaRef,
required: Bool,
)
}
pub type BodyFieldKind {
BodyFieldUnknown
BodyFieldString
BodyFieldInt
BodyFieldFloat
BodyFieldBool
BodyFieldStringArray
BodyFieldIntArray
BodyFieldFloatArray
BodyFieldBoolArray
}
pub fn schema_ref_body_field_kind(
schema_ref: Option(SchemaRef),
ctx: Context,
) -> BodyFieldKind {
case schema_ref {
Some(schema_ref) -> body_field_kind(schema_ref, ctx)
None -> BodyFieldUnknown
}
}
pub fn body_field_kind(schema_ref: SchemaRef, ctx: Context) -> BodyFieldKind {
case schema_ref {
Inline(schema.StringSchema(..)) -> BodyFieldString
Inline(schema.IntegerSchema(..)) -> BodyFieldInt
Inline(schema.NumberSchema(..)) -> BodyFieldFloat
Inline(schema.BooleanSchema(..)) -> BodyFieldBool
Inline(schema.ArraySchema(items:, ..)) ->
case body_field_kind(items, ctx) {
BodyFieldString -> BodyFieldStringArray
BodyFieldInt -> BodyFieldIntArray
BodyFieldFloat -> BodyFieldFloatArray
BodyFieldBool -> BodyFieldBoolArray
_ -> BodyFieldUnknown
}
Reference(..) as schema_ref ->
case resolver.resolve_schema_ref(schema_ref, context.spec(ctx)) {
Ok(schema_obj) -> body_field_kind_from_object(schema_obj, ctx)
// nolint: thrown_away_error -- unresolved refs map to Unknown; the resolver reports the ref error separately
Error(_) -> BodyFieldUnknown
}
_ -> BodyFieldUnknown
}
}
fn body_field_kind_from_object(
schema_obj: schema.SchemaObject,
ctx: Context,
) -> BodyFieldKind {
case schema_obj {
schema.StringSchema(..) -> BodyFieldString
schema.IntegerSchema(..) -> BodyFieldInt
schema.NumberSchema(..) -> BodyFieldFloat
schema.BooleanSchema(..) -> BodyFieldBool
schema.ArraySchema(items:, ..) ->
case body_field_kind(items, ctx) {
BodyFieldString -> BodyFieldStringArray
BodyFieldInt -> BodyFieldIntArray
BodyFieldFloat -> BodyFieldFloatArray
BodyFieldBool -> BodyFieldBoolArray
_ -> BodyFieldUnknown
}
_ -> BodyFieldUnknown
}
}
pub fn body_field_kind_needs_int(kind: BodyFieldKind) -> Bool {
case kind {
BodyFieldInt | BodyFieldIntArray -> True
_ -> False
}
}
pub fn body_field_kind_needs_float(kind: BodyFieldKind) -> Bool {
case kind {
BodyFieldFloat | BodyFieldFloatArray -> True
_ -> False
}
}
/// Generate parse expression for a path parameter (already bound as String).
/// Returns a safe expression that does not crash on invalid input.
/// For types that need parsing (int, float), returns the raw parse call
/// so the router can wrap it in a case expression for error handling.
pub fn param_parse_expr(
var_name: String,
param: spec.Parameter(Resolved),
) -> String {
case spec.parameter_schema(param) {
Some(Inline(schema.IntegerSchema(..))) -> {
"int.parse(" <> var_name <> ")"
}
Some(Inline(schema.NumberSchema(..))) -> {
"float.parse(" <> var_name <> ")"
}
Some(Inline(schema.BooleanSchema(..))) -> {
"{ let v = " <> var_name <> " " <> bool_parse_expr <> " }"
}
_ -> var_name
}
}
/// Return true when the parse expression returns a Result that the router
/// must unwrap (int/float parsing). Bool and string params are always safe.
pub fn param_needs_result_unwrap(param: spec.Parameter(Resolved)) -> Bool {
case spec.parameter_schema(param) {
Some(Inline(schema.IntegerSchema(..)))
| Some(Inline(schema.NumberSchema(..))) -> True
_ -> False
}
}
/// Generate expression for a required query parameter.
///
/// `bound_var` is the name of a variable that the router has already bound
/// in an enclosing case expression. For scalar params (string/bool/int/float)
/// this is the raw String value pulled out of the query dict. For array
/// params with `explode=true` this is the `List(String)` from the dict; with
/// `explode=false` it is the single raw String that still needs splitting.
///
/// The router opens a `case dict.get(query, key) { Ok([raw, ..]) -> ...`
/// (or `Ok(raw_list) -> ...` for explode-true arrays) **before** asking this
/// helper for the value expression, so the lookup never `let assert`s.
///
/// For numeric scalar/array parameters the router additionally opens a
/// `case int.parse(...)` / `float.parse(...)` (or `list.try_map(..., int.parse)`)
/// case, binding `<bound_var>_parsed` (or `<bound_var>_parsed_list`) on
/// success — that's the variable name the helper returns here.
pub fn query_required_expr(
bound_var: String,
param: spec.Parameter(Resolved),
) -> String {
query_required_expr_with_schema(
bound_var,
spec.parameter_schema(param),
Some(operation_ir.effective_explode(param)),
param.style,
)
}
/// Legacy lookup-based required-query expression preserved for deep object
/// constructor fields. Top-level required query params now go through the
/// safer `query_required_expr` + router-side case scaffolding (Issue #263);
/// deep object support is intentionally still on the old `let assert` path
/// pending its own fix.
fn deep_object_required_field_expr(
key: String,
schema_ref: Option(SchemaRef),
) -> String {
let base = "{ let assert Ok([v, ..]) = dict.get(query, \"" <> key <> "\") v }"
case schema_ref {
Some(Inline(schema.ArraySchema(items: Inline(schema.StringSchema(..)), ..))) ->
"{ let assert Ok(vs) = dict.get(query, \""
<> key
<> "\") list.map(vs, fn(item) { string.trim(item) }) }"
Some(Inline(schema.ArraySchema(items: Inline(schema.IntegerSchema(..)), ..))) ->
"{ let assert Ok(vs) = dict.get(query, \""
<> key
<> "\") list.map(vs, fn(item) { let trimmed = string.trim(item) case int.parse(trimmed) { Ok(n) -> n _ -> 0 } }) }"
Some(Inline(schema.ArraySchema(items: Inline(schema.NumberSchema(..)), ..))) ->
"{ let assert Ok(vs) = dict.get(query, \""
<> key
<> "\") list.map(vs, fn(item) { let trimmed = string.trim(item) case float.parse(trimmed) { Ok(n) -> n _ -> 0.0 } }) }"
Some(Inline(schema.ArraySchema(items: Inline(schema.BooleanSchema(..)), ..))) ->
"{ let assert Ok(vs) = dict.get(query, \""
<> key
<> "\") list.map(vs, fn(item) { let v = string.trim(item) "
<> bool_parse_expr
<> " }) }"
Some(Inline(schema.IntegerSchema(..))) ->
"{ let assert Ok([v, ..]) = dict.get(query, \""
<> key
<> "\") let assert Ok(n) = int.parse(v) n }"
Some(Inline(schema.NumberSchema(..))) ->
"{ let assert Ok([v, ..]) = dict.get(query, \""
<> key
<> "\") let assert Ok(n) = float.parse(v) n }"
Some(Inline(schema.BooleanSchema(..))) ->
"{ let assert Ok([v, ..]) = dict.get(query, \""
<> key
<> "\") "
<> bool_parse_expr
<> " }"
_ -> base
}
}
fn query_required_expr_with_schema(
bound_var: String,
schema_ref: Option(SchemaRef),
explode: Option(Bool),
style: Option(spec.ParameterStyle),
) -> String {
let delim = operation_ir.delimiter_for_style(style)
case schema_ref {
Some(Inline(schema.ArraySchema(items: Inline(schema.StringSchema(..)), ..))) ->
case explode {
Some(False) ->
"list.map(string.split("
<> bound_var
<> ", \""
<> delim
<> "\"), fn(item) { string.trim(item) })"
_ -> "list.map(" <> bound_var <> ", fn(item) { string.trim(item) })"
}
Some(Inline(schema.ArraySchema(items: Inline(schema.IntegerSchema(..)), ..))) ->
// The router opens a `case list.try_map(..., int.parse)` and binds
// `<bound_var>_parsed_list` on Ok, so the value expression is just
// that bound name.
bound_var <> "_parsed_list"
Some(Inline(schema.ArraySchema(items: Inline(schema.NumberSchema(..)), ..))) ->
bound_var <> "_parsed_list"
Some(Inline(schema.ArraySchema(items: Inline(schema.BooleanSchema(..)), ..))) ->
case explode {
Some(False) ->
"list.map(string.split("
<> bound_var
<> ", \""
<> delim
<> "\"), fn(item) { let v = string.trim(item) "
<> bool_parse_expr
<> " })"
_ ->
"list.map("
<> bound_var
<> ", fn(item) { let v = string.trim(item) "
<> bool_parse_expr
<> " })"
}
Some(Inline(schema.IntegerSchema(..))) -> bound_var <> "_parsed"
Some(Inline(schema.NumberSchema(..))) -> bound_var <> "_parsed"
Some(Inline(schema.BooleanSchema(..))) ->
"{ let v = " <> bound_var <> " " <> bool_parse_expr <> " }"
_ -> bound_var
}
}
/// Generate expression for an optional query parameter.
pub fn query_optional_expr(
key: String,
param: spec.Parameter(Resolved),
) -> String {
query_optional_expr_with_schema(
key,
spec.parameter_schema(param),
Some(operation_ir.effective_explode(param)),
param.style,
)
}
fn query_optional_expr_with_schema(
key: String,
schema_ref: Option(SchemaRef),
explode: Option(Bool),
style: Option(spec.ParameterStyle),
) -> String {
let delim = operation_ir.delimiter_for_style(style)
case schema_ref {
Some(Inline(schema.ArraySchema(items: Inline(schema.StringSchema(..)), ..))) ->
case explode {
Some(False) ->
"case dict.get(query, \""
<> key
<> "\") { Ok([v, ..]) -> Some(list.map(string.split(v, \""
<> delim
<> "\"), fn(item) { string.trim(item) })) _ -> None }"
_ ->
"case dict.get(query, \""
<> key
<> "\") { Ok(vs) -> Some(list.map(vs, fn(item) { string.trim(item) })) _ -> None }"
}
Some(Inline(schema.ArraySchema(items: Inline(schema.IntegerSchema(..)), ..))) ->
case explode {
Some(False) ->
"case dict.get(query, \""
<> key
<> "\") { Ok([v, ..]) -> Some(list.map(string.split(v, \""
<> delim
<> "\"), fn(item) { let trimmed = string.trim(item) case int.parse(trimmed) { Ok(n) -> n _ -> 0 } })) _ -> None }"
_ ->
"case dict.get(query, \""
<> key
<> "\") { Ok(vs) -> Some(list.map(vs, fn(item) { let trimmed = string.trim(item) case int.parse(trimmed) { Ok(n) -> n _ -> 0 } })) _ -> None }"
}
Some(Inline(schema.ArraySchema(items: Inline(schema.NumberSchema(..)), ..))) ->
case explode {
Some(False) ->
"case dict.get(query, \""
<> key
<> "\") { Ok([v, ..]) -> Some(list.map(string.split(v, \""
<> delim
<> "\"), fn(item) { let trimmed = string.trim(item) case float.parse(trimmed) { Ok(n) -> n _ -> 0.0 } })) _ -> None }"
_ ->
"case dict.get(query, \""
<> key
<> "\") { Ok(vs) -> Some(list.map(vs, fn(item) { let trimmed = string.trim(item) case float.parse(trimmed) { Ok(n) -> n _ -> 0.0 } })) _ -> None }"
}
Some(Inline(schema.ArraySchema(items: Inline(schema.BooleanSchema(..)), ..))) ->
case explode {
Some(False) ->
"case dict.get(query, \""
<> key
<> "\") { Ok([v, ..]) -> Some(list.map(string.split(v, \""
<> delim
<> "\"), fn(item) { let v = string.trim(item) "
<> bool_parse_expr
<> " })) _ -> None }"
_ ->
"case dict.get(query, \""
<> key
<> "\") { Ok(vs) -> Some(list.map(vs, fn(item) { let v = string.trim(item) "
<> bool_parse_expr
<> " })) _ -> None }"
}
Some(Inline(schema.IntegerSchema(..))) ->
"case dict.get(query, \""
<> key
<> "\") { Ok([v, ..]) -> { case int.parse(v) { Ok(n) -> Some(n) _ -> None } } _ -> None }"
Some(Inline(schema.NumberSchema(..))) ->
"case dict.get(query, \""
<> key
<> "\") { Ok([v, ..]) -> { case float.parse(v) { Ok(n) -> Some(n) _ -> None } } _ -> None }"
Some(Inline(schema.BooleanSchema(..))) ->
"case dict.get(query, \""
<> key
<> "\") { Ok([v, ..]) -> Some("
<> bool_parse_expr
<> ") _ -> None }"
_ ->
"case dict.get(query, \""
<> key
<> "\") { Ok([v, ..]) -> Some(v) _ -> None }"
}
}
pub fn is_deep_object_param(
param: spec.Parameter(Resolved),
ctx: Context,
) -> Bool {
operation_ir.is_deep_object_param(param, ctx)
}
fn deep_object_properties(
param: spec.Parameter(Resolved),
ctx: Context,
) -> List(DeepObjectProperty) {
let details = case spec.parameter_schema(param) {
Some(Reference(..) as schema_ref) ->
case resolver.resolve_schema_ref(schema_ref, context.spec(ctx)) {
Ok(ObjectSchema(properties:, required:, ..)) -> #(properties, required)
_ -> #(dict.new(), [])
}
Some(Inline(ObjectSchema(properties:, required:, ..))) -> #(
properties,
required,
)
_ -> #(dict.new(), [])
}
let #(properties, required_fields) = details
ir_build.sorted_entries(properties)
|> list.map(fn(entry) {
let #(prop_name, prop_ref) = entry
DeepObjectProperty(
name: prop_name,
field_name: naming.to_snake_case(prop_name),
schema_ref: prop_ref,
required: list.contains(required_fields, prop_name),
)
})
}
fn deep_object_type_name(
param: spec.Parameter(Resolved),
op_id: String,
) -> String {
case spec.parameter_schema(param) {
Some(Reference(name:, ..)) -> "types." <> naming.schema_to_type_name(name)
_ ->
"types."
<> naming.schema_to_type_name(op_id)
<> "Param"
<> naming.to_pascal_case(param.name)
}
}
pub fn deep_object_required_expr(
key: String,
param: spec.Parameter(Resolved),
op_id: String,
ctx: Context,
) -> String {
deep_object_constructor_expr(key, param, op_id, ctx)
}
pub fn deep_object_optional_expr(
key: String,
param: spec.Parameter(Resolved),
op_id: String,
ctx: Context,
) -> String {
let props = deep_object_properties(param, ctx)
let prop_names =
props
|> list.map(fn(prop) { "\"" <> prop.name <> "\"" })
|> string.join(", ")
let has_untyped_ap = deep_object_has_untyped_additional_properties(param, ctx)
let presence_check = case has_untyped_ap {
True -> "deep_object_present_any(query, \"" <> key <> "\")"
False ->
"deep_object_present(query, \"" <> key <> "\", [" <> prop_names <> "])"
}
"case "
<> presence_check
<> " { True -> Some("
<> deep_object_constructor_expr(key, param, op_id, ctx)
<> ") False -> None }"
}
fn deep_object_constructor_expr(
key: String,
param: spec.Parameter(Resolved),
op_id: String,
ctx: Context,
) -> String {
let fields =
deep_object_properties(param, ctx)
|> list.map(fn(prop) {
let prop_key = key <> "[" <> prop.name <> "]"
let value_expr = case prop.required {
// Deep object property handling intentionally retains the legacy
// `let assert`-based lookup. Issue #263 narrowed the safety fix to
// top-level query/header/cookie params; deep object follow-up is
// tracked separately.
True -> deep_object_required_field_expr(prop_key, Some(prop.schema_ref))
False ->
query_optional_expr_with_schema(
prop_key,
Some(prop.schema_ref),
Some(True),
None,
)
}
prop.field_name <> ": " <> value_expr
})
// Add additional_properties field if the schema has it
let ap_kind = case spec.parameter_schema(param) {
Some(schema_ref) -> schema_ref_additional_properties(schema_ref, ctx)
None -> Forbidden
}
let fields = case ap_kind {
// Forbidden and Unspecified both suppress the generated record's
// additional_properties field — see Issue #249.
Forbidden | Unspecified -> fields
Untyped -> {
let prop_names =
deep_object_properties(param, ctx)
|> list.map(fn(prop) { "\"" <> prop.name <> "\"" })
|> string.join(", ")
let expr =
"coerce_dict(deep_object_additional_properties(query, \""
<> key
<> "\", ["
<> prop_names
<> "]))"
list.append(fields, ["additional_properties: " <> expr])
}
Typed(_) -> {
// Typed additional properties need type-specific conversion;
// use empty dict as fallback until type-aware collection is implemented
list.append(fields, ["additional_properties: dict.new()"])
}
}
let fields_str = string.join(fields, ", ")
deep_object_type_name(param, op_id) <> "(" <> fields_str <> ")"
}
pub fn deep_object_has_additional_properties(
param: spec.Parameter(Resolved),
ctx: Context,
) -> Bool {
case spec.parameter_schema(param) {
Some(Reference(..) as schema_ref) ->
case resolver.resolve_schema_ref(schema_ref, context.spec(ctx)) {
Ok(ObjectSchema(additional_properties: schema.Typed(_), ..)) -> True
Ok(ObjectSchema(additional_properties: schema.Untyped, ..)) -> True
_ -> False
}
Some(Inline(ObjectSchema(additional_properties: schema.Typed(_), ..))) ->
True
Some(Inline(ObjectSchema(additional_properties: schema.Untyped, ..))) ->
True
_ -> False
}
}
pub fn deep_object_has_untyped_additional_properties(
param: spec.Parameter(Resolved),
ctx: Context,
) -> Bool {
case spec.parameter_schema(param) {
Some(schema_ref) ->
case schema_ref_additional_properties(schema_ref, ctx) {
Untyped -> True
_ -> False
}
None -> False
}
}
pub fn deep_object_param_has_optional_fields(
param: spec.Parameter(Resolved),
ctx: Context,
) -> Bool {
use <- bool.guard(!is_deep_object_param(param, ctx), False)
list.any(deep_object_properties(param, ctx), fn(prop) { !prop.required })
}
pub fn deep_object_param_needs_string(
param: spec.Parameter(Resolved),
ctx: Context,
) -> Bool {
deep_object_param_needs(param, ctx, fn(prop) {
query_schema_needs_string(Some(prop.schema_ref))
})
}
pub fn deep_object_param_needs_int(
param: spec.Parameter(Resolved),
ctx: Context,
) -> Bool {
deep_object_param_needs(param, ctx, fn(prop) {
query_schema_needs_int(Some(prop.schema_ref))
})
}
pub fn deep_object_param_needs_float(
param: spec.Parameter(Resolved),
ctx: Context,
) -> Bool {
deep_object_param_needs(param, ctx, fn(prop) {
query_schema_needs_float(Some(prop.schema_ref))
})
}
fn deep_object_param_needs(
param: spec.Parameter(Resolved),
ctx: Context,
predicate: fn(DeepObjectProperty) -> Bool,
) -> Bool {
use <- bool.guard(!is_deep_object_param(param, ctx), False)
list.any(deep_object_properties(param, ctx), predicate)
}
pub fn request_body_uses_form_urlencoded(rb: spec.RequestBody(Resolved)) -> Bool {
dict.has_key(rb.content, "application/x-www-form-urlencoded")
}
pub fn request_body_uses_multipart(rb: spec.RequestBody(Resolved)) -> Bool {
dict.has_key(rb.content, "multipart/form-data")
}
pub fn operation_uses_form_urlencoded_body(
operation: spec.Operation(Resolved),
) -> Bool {
case operation.request_body {
Some(Value(rb)) -> request_body_uses_form_urlencoded(rb)
_ -> False
}
}
pub fn operation_uses_multipart_body(
operation: spec.Operation(Resolved),
) -> Bool {
case operation.request_body {
Some(Value(rb)) -> request_body_uses_multipart(rb)
_ -> False
}
}
fn object_properties_from_schema_ref(
schema_ref: SchemaRef,
ctx: Context,
) -> List(DeepObjectProperty) {
let details = case schema_ref {
Reference(..) as schema_ref ->
case resolver.resolve_schema_ref(schema_ref, context.spec(ctx)) {
Ok(ObjectSchema(properties:, required:, ..)) -> #(properties, required)
_ -> #(dict.new(), [])
}
Inline(ObjectSchema(properties:, required:, ..)) -> #(properties, required)
_ -> #(dict.new(), [])
}
let #(properties, required_fields) = details
ir_build.sorted_entries(properties)
|> list.map(fn(entry) {
let #(prop_name, prop_ref) = entry
DeepObjectProperty(
name: prop_name,
field_name: naming.to_snake_case(prop_name),
schema_ref: prop_ref,
required: list.contains(required_fields, prop_name),
)
})
}
fn form_urlencoded_body_properties(
rb: spec.RequestBody(Resolved),
ctx: Context,
) -> List(DeepObjectProperty) {
case dict.get(rb.content, "application/x-www-form-urlencoded") {
Ok(media_type) -> {
case media_type.schema {
Some(schema_ref) -> object_properties_from_schema_ref(schema_ref, ctx)
None -> []
}
}
// nolint: thrown_away_error -- absence of the content type means no properties to enumerate
Error(_) -> []
}
}
fn multipart_body_properties(
rb: spec.RequestBody(Resolved),
ctx: Context,
) -> List(DeepObjectProperty) {
case dict.get(rb.content, "multipart/form-data") {
Ok(media_type) ->
case media_type.schema {
Some(schema_ref) -> object_properties_from_schema_ref(schema_ref, ctx)
None -> []
}
// nolint: thrown_away_error -- absence of the content type means no properties to enumerate
Error(_) -> []
}
}
fn schema_ref_resolves_to_object(schema_ref: SchemaRef, ctx: Context) -> Bool {
case schema_ref {
Inline(ObjectSchema(..)) -> True
Reference(..) as schema_ref ->
case resolver.resolve_schema_ref(schema_ref, context.spec(ctx)) {
Ok(ObjectSchema(..)) -> True
_ -> False
}
_ -> False
}
}
fn schema_ref_additional_properties(
schema_ref: SchemaRef,
ctx: Context,
) -> AdditionalProperties {
case schema_ref {
Inline(ObjectSchema(additional_properties:, ..)) -> additional_properties
Reference(..) as schema_ref ->
case resolver.resolve_schema_ref(schema_ref, context.spec(ctx)) {
Ok(ObjectSchema(additional_properties:, ..)) -> additional_properties
_ -> Forbidden
}
_ -> Forbidden
}
}
fn body_additional_properties(
rb: spec.RequestBody(Resolved),
content_type: String,
ctx: Context,
) -> AdditionalProperties {
case dict.get(rb.content, content_type) {
Ok(media_type) ->
case media_type.schema {
Some(schema_ref) -> schema_ref_additional_properties(schema_ref, ctx)
None -> Forbidden
}
// nolint: thrown_away_error -- absence of the content type means no additionalProperties
Error(_) -> Forbidden
}
}
fn form_urlencoded_schema_ref_type_name(schema_ref: SchemaRef) -> String {
case schema_ref {
Reference(name:, ..) -> "types." <> naming.schema_to_type_name(name)
_ -> "String"
}
}
fn form_urlencoded_body_type_name(
rb: spec.RequestBody(Resolved),
op_id: String,
) -> String {
case dict.get(rb.content, "application/x-www-form-urlencoded") {
Ok(media_type) ->
case media_type.schema {
Some(Reference(name:, ..)) ->
"types." <> naming.schema_to_type_name(name)
Some(Inline(ObjectSchema(..))) ->
"types." <> naming.schema_to_type_name(op_id) <> "Request"
_ -> "String"
}
// nolint: thrown_away_error -- absence of the content type falls back to the raw body type
Error(_) -> "String"
}
}
fn multipart_body_type_name(
rb: spec.RequestBody(Resolved),
op_id: String,
) -> String {
case dict.get(rb.content, "multipart/form-data") {
Ok(media_type) ->
case media_type.schema {
Some(Reference(name:, ..)) ->
"types." <> naming.schema_to_type_name(name)
Some(Inline(ObjectSchema(..))) ->
"types." <> naming.schema_to_type_name(op_id) <> "Request"
_ -> "String"
}
// nolint: thrown_away_error -- absence of the content type falls back to the raw body type
Error(_) -> "String"
}
}
fn form_urlencoded_key(prefix: String, name: String) -> String {
case prefix {
"" -> name
_ -> prefix <> "[" <> name <> "]"
}
}
fn form_urlencoded_object_constructor_expr(
type_name: String,
prefix: String,
properties: List(DeepObjectProperty),
additional_properties: AdditionalProperties,
ctx: Context,
nesting_depth: Int,
) -> String {
let fields =
properties
|> list.map(fn(prop) {
let key = form_urlencoded_key(prefix, prop.name)
let value_expr = case
nesting_depth < 5 && schema_ref_resolves_to_object(prop.schema_ref, ctx),
prop.required
{
True, True ->
form_urlencoded_object_required_expr(
key,
prop.schema_ref,
ctx,
nesting_depth + 1,
)
True, False ->
form_urlencoded_object_optional_expr(
key,
prop.schema_ref,
ctx,
nesting_depth + 1,
)
False, True ->
form_body_required_expr_with_schema(key, Some(prop.schema_ref), ctx)
False, False ->
form_body_optional_expr_with_schema(key, Some(prop.schema_ref), ctx)
}
prop.field_name <> ": " <> value_expr
})
|> string.join(", ")
let additional_props_suffix = case additional_properties {
// Forbidden and Unspecified both mean the generated record has no
// additional_properties field — Issue #249.
Forbidden | Unspecified -> ""
Typed(_) | Untyped -> ", additional_properties: dict.new()"
}
type_name <> "(" <> fields <> additional_props_suffix <> ")"
}
fn form_urlencoded_object_required_expr(
prefix: String,
schema_ref: SchemaRef,
ctx: Context,
nesting_depth: Int,
) -> String {
form_urlencoded_object_constructor_expr(
form_urlencoded_schema_ref_type_name(schema_ref),
prefix,
object_properties_from_schema_ref(schema_ref, ctx),
schema_ref_additional_properties(schema_ref, ctx),
ctx,
nesting_depth,
)
}
fn form_urlencoded_object_optional_expr(
prefix: String,
schema_ref: SchemaRef,
ctx: Context,
nesting_depth: Int,
) -> String {
let props = object_properties_from_schema_ref(schema_ref, ctx)
let prop_names =
props
|> list.map(fn(prop) { "\"" <> prop.name <> "\"" })
|> string.join(", ")
"case form_object_present(form_body, \""
<> prefix
<> "\", ["
<> prop_names
<> "]) { True -> Some("
<> form_urlencoded_object_constructor_expr(
form_urlencoded_schema_ref_type_name(schema_ref),
prefix,
props,
schema_ref_additional_properties(schema_ref, ctx),
ctx,
nesting_depth,
)
<> ") False -> None }"
}
fn form_urlencoded_body_constructor_expr(
rb: spec.RequestBody(Resolved),
op_id: String,
ctx: Context,
) -> String {
form_urlencoded_object_constructor_expr(
form_urlencoded_body_type_name(rb, op_id),
"",
form_urlencoded_body_properties(rb, ctx),
body_additional_properties(rb, "application/x-www-form-urlencoded", ctx),
ctx,
0,
)
}
pub fn form_urlencoded_body_has_optional_fields(
rb: spec.RequestBody(Resolved),
ctx: Context,
) -> Bool {
form_urlencoded_properties_have_optional_fields(
form_urlencoded_body_properties(rb, ctx),
ctx,
True,
)
}
pub fn form_urlencoded_body_needs_string(
rb: spec.RequestBody(Resolved),
ctx: Context,
) -> Bool {
form_urlencoded_properties_need_string(
form_urlencoded_body_properties(rb, ctx),
ctx,
True,
)
}
pub fn form_urlencoded_body_needs_int(
rb: spec.RequestBody(Resolved),
ctx: Context,
) -> Bool {
form_urlencoded_properties_need_int(
form_urlencoded_body_properties(rb, ctx),
ctx,
True,
)
}
pub fn form_urlencoded_body_needs_float(
rb: spec.RequestBody(Resolved),
ctx: Context,
) -> Bool {
form_urlencoded_properties_need_float(
form_urlencoded_body_properties(rb, ctx),
ctx,
True,
)
}
pub fn form_urlencoded_body_has_nested_object(
rb: spec.RequestBody(Resolved),
ctx: Context,
) -> Bool {
list.any(form_urlencoded_body_properties(rb, ctx), fn(prop) {
schema_ref_resolves_to_object(prop.schema_ref, ctx)
})
}
fn multipart_body_constructor_expr(
rb: spec.RequestBody(Resolved),
op_id: String,
ctx: Context,
) -> String {
let fields =
multipart_body_properties(rb, ctx)
|> list.map(fn(prop) {
let value_expr = case prop.required {
True ->
multipart_body_required_expr_with_schema(
prop.name,
Some(prop.schema_ref),
ctx,
)
False ->
multipart_body_optional_expr_with_schema(
prop.name,
Some(prop.schema_ref),
ctx,
)
}
prop.field_name <> ": " <> value_expr
})
|> string.join(", ")
let additional_props_suffix = case
body_additional_properties(rb, "multipart/form-data", ctx)
{
// Forbidden and Unspecified both mean the generated record has no
// additional_properties field — Issue #249.
Forbidden | Unspecified -> ""
Typed(_) | Untyped -> ", additional_properties: dict.new()"
}
multipart_body_type_name(rb, op_id)
<> "("
<> fields
<> additional_props_suffix
<> ")"
}
pub fn multipart_body_has_optional_fields(
rb: spec.RequestBody(Resolved),
ctx: Context,
) -> Bool {
list.any(multipart_body_properties(rb, ctx), fn(prop) { !prop.required })
}
pub fn multipart_body_needs_int(
rb: spec.RequestBody(Resolved),
ctx: Context,
) -> Bool {
list.any(multipart_body_properties(rb, ctx), fn(prop) {
body_field_kind_needs_int(schema_ref_body_field_kind(
Some(prop.schema_ref),
ctx,
))
})
}
pub fn multipart_body_needs_float(
rb: spec.RequestBody(Resolved),
ctx: Context,
) -> Bool {
list.any(multipart_body_properties(rb, ctx), fn(prop) {
body_field_kind_needs_float(schema_ref_body_field_kind(
Some(prop.schema_ref),
ctx,
))
})
}
fn form_urlencoded_properties_have_optional_fields(
props: List(DeepObjectProperty),
ctx: Context,
allow_nested_objects: Bool,
) -> Bool {
list.any(props, fn(prop) {
!prop.required
|| case
allow_nested_objects
&& schema_ref_resolves_to_object(prop.schema_ref, ctx)
{
True ->
form_urlencoded_properties_have_optional_fields(
object_properties_from_schema_ref(prop.schema_ref, ctx),
ctx,
False,
)
False -> False
}
})
}
fn form_urlencoded_properties_need_string(
props: List(DeepObjectProperty),
ctx: Context,
allow_nested_objects: Bool,
) -> Bool {
form_urlencoded_properties_need(props, ctx, allow_nested_objects, fn(prop) {
query_schema_needs_string(Some(prop.schema_ref))
})
}
fn form_urlencoded_properties_need_int(
props: List(DeepObjectProperty),
ctx: Context,
allow_nested_objects: Bool,
) -> Bool {
form_urlencoded_properties_need(props, ctx, allow_nested_objects, fn(prop) {
body_field_kind_needs_int(schema_ref_body_field_kind(
Some(prop.schema_ref),
ctx,
))
})
}
fn form_urlencoded_properties_need_float(
props: List(DeepObjectProperty),
ctx: Context,
allow_nested_objects: Bool,
) -> Bool {
form_urlencoded_properties_need(props, ctx, allow_nested_objects, fn(prop) {
body_field_kind_needs_float(schema_ref_body_field_kind(
Some(prop.schema_ref),
ctx,
))
})
}
fn form_urlencoded_properties_need(
props: List(DeepObjectProperty),
ctx: Context,
allow_nested_objects: Bool,
predicate: fn(DeepObjectProperty) -> Bool,
) -> Bool {
list.any(props, fn(prop) {
predicate(prop)
|| case
allow_nested_objects
&& schema_ref_resolves_to_object(prop.schema_ref, ctx)
{
True ->
form_urlencoded_properties_need(
object_properties_from_schema_ref(prop.schema_ref, ctx),
ctx,
False,
predicate,
)
False -> False
}
})
}
pub fn query_schema_needs_string(schema_ref: Option(SchemaRef)) -> Bool {
case schema_ref {
Some(Inline(schema.ArraySchema(..))) -> True
Some(Inline(schema.BooleanSchema(..))) -> True
_ -> False
}
}
pub fn query_schema_needs_int(schema_ref: Option(SchemaRef)) -> Bool {
case schema_ref {
Some(Inline(schema.IntegerSchema(..))) -> True
Some(Inline(schema.ArraySchema(items: Inline(schema.IntegerSchema(..)), ..))) ->
True
_ -> False
}
}
pub fn query_schema_needs_float(schema_ref: Option(SchemaRef)) -> Bool {
case schema_ref {
Some(Inline(schema.NumberSchema(..))) -> True
Some(Inline(schema.ArraySchema(items: Inline(schema.NumberSchema(..)), ..))) ->
True
_ -> False
}
}
fn form_body_required_expr_with_schema(
key: String,
schema_ref: Option(SchemaRef),
ctx: Context,
) -> String {
body_required_expr(key, "form_body", True, schema_ref, ctx)
}
fn form_body_optional_expr_with_schema(
key: String,
schema_ref: Option(SchemaRef),
ctx: Context,
) -> String {
body_optional_expr(key, "form_body", True, "_", schema_ref, ctx)
}
fn multipart_body_required_expr_with_schema(
key: String,
schema_ref: Option(SchemaRef),
ctx: Context,
) -> String {
body_required_expr(key, "multipart_body", False, schema_ref, ctx)
}
fn multipart_body_optional_expr_with_schema(
key: String,
schema_ref: Option(SchemaRef),
ctx: Context,
) -> String {
body_optional_expr(key, "multipart_body", False, "_", schema_ref, ctx)
}
/// Generate a required body field expression.
/// `source` is the dict name (e.g., "form_body", "multipart_body").
/// `trim_items` controls whether array items are trimmed (form_body) or used raw (multipart).
fn body_required_expr(
key: String,
source: String,
trim_items: Bool,
schema_ref: Option(SchemaRef),
ctx: Context,
) -> String {
let lookup = "dict.get(" <> source <> ", \"" <> key <> "\")"
let base = "{ let assert Ok([v, ..]) = " <> lookup <> " v }"
case schema_ref_body_field_kind(schema_ref, ctx) {
BodyFieldStringArray ->
case trim_items {
True ->
"{ let assert Ok(vs) = "
<> lookup
<> " list.map(vs, fn(item) { string.trim(item) }) }"
False -> "{ let assert Ok(vs) = " <> lookup <> " vs }"
}
BodyFieldIntArray ->
"{ let assert Ok(vs) = "
<> lookup
<> " list.map(vs, fn(item) { "
<> array_item_int_parse(trim_items)
<> " }) }"
BodyFieldFloatArray ->
"{ let assert Ok(vs) = "
<> lookup
<> " list.map(vs, fn(item) { "
<> array_item_float_parse(trim_items)
<> " }) }"
BodyFieldBoolArray ->
"{ let assert Ok(vs) = "
<> lookup
<> " list.map(vs, fn(item) { "
<> array_item_bool_parse(trim_items)
<> " }) }"
BodyFieldInt ->
"{ let assert Ok([v, ..]) = "
<> lookup
<> " let assert Ok(n) = int.parse(v) n }"
BodyFieldFloat ->
"{ let assert Ok([v, ..]) = "
<> lookup
<> " let assert Ok(n) = float.parse(v) n }"
BodyFieldBool ->
"{ let assert Ok([v, ..]) = " <> lookup <> " " <> bool_parse_expr <> " }"
_ -> base
}
}
/// Generate an optional body field expression.
/// `source` is the dict name, `miss` is the miss pattern ("_" or "Error(_)").
fn body_optional_expr(
key: String,
source: String,
trim_items: Bool,
miss: String,
schema_ref: Option(SchemaRef),
ctx: Context,
) -> String {
let lookup = "dict.get(" <> source <> ", \"" <> key <> "\")"
case schema_ref_body_field_kind(schema_ref, ctx) {
BodyFieldStringArray ->
case trim_items {
True ->
"case "
<> lookup
<> " { Ok(vs) -> Some(list.map(vs, fn(item) { string.trim(item) })) "
<> miss
<> " -> None }"
False ->
"case " <> lookup <> " { Ok(vs) -> Some(vs) " <> miss <> " -> None }"
}
BodyFieldIntArray ->
"case "
<> lookup
<> " { Ok(vs) -> Some(list.map(vs, fn(item) { "
<> array_item_int_parse(trim_items)
<> " })) "
<> miss
<> " -> None }"
BodyFieldFloatArray ->
"case "
<> lookup
<> " { Ok(vs) -> Some(list.map(vs, fn(item) { "
<> array_item_float_parse(trim_items)
<> " })) "
<> miss
<> " -> None }"
BodyFieldBoolArray ->
"case "
<> lookup
<> " { Ok(vs) -> Some(list.map(vs, fn(item) { "
<> array_item_bool_parse(trim_items)
<> " })) "
<> miss
<> " -> None }"
BodyFieldInt ->
"case "
<> lookup
<> " { Ok([v, ..]) -> { case int.parse(v) { Ok(n) -> Some(n) _ -> None } } "
<> miss
<> " -> None }"
BodyFieldFloat ->
"case "
<> lookup
<> " { Ok([v, ..]) -> { case float.parse(v) { Ok(n) -> Some(n) _ -> None } } "
<> miss
<> " -> None }"
BodyFieldBool ->
"case "
<> lookup
<> " { Ok([v, ..]) -> Some("
<> bool_parse_expr
<> ") "
<> miss
<> " -> None }"
_ ->
"case " <> lookup <> " { Ok([v, ..]) -> Some(v) " <> miss <> " -> None }"
}
}
/// Parse expression for array items: int, with optional trimming.
/// Uses a case expression instead of `let assert` to avoid panicking on
/// malformed input — bad values fall back to 0 (#291).
fn array_item_int_parse(trim: Bool) -> String {
use <- bool.guard(!trim, "case int.parse(item) { Ok(n) -> n _ -> 0 }")
"let trimmed = string.trim(item) case int.parse(trimmed) { Ok(n) -> n _ -> 0 }"
}
/// Parse expression for array items: float, with optional trimming.
/// Uses a case expression instead of `let assert` to avoid panicking on
/// malformed input — bad values fall back to 0.0 (#291).
fn array_item_float_parse(trim: Bool) -> String {
use <- bool.guard(!trim, "case float.parse(item) { Ok(n) -> n _ -> 0.0 }")
"let trimmed = string.trim(item) case float.parse(trimmed) { Ok(n) -> n _ -> 0.0 }"
}
/// Parse expression for array items: bool, with optional trimming.
fn array_item_bool_parse(trim: Bool) -> String {
case trim {
True -> "let v = string.trim(item) " <> bool_parse_expr
False -> "let v = item " <> bool_parse_expr
}
}
/// Generate expression for a required header parameter.
///
/// `bound_var` is the raw header String the router has already pulled
/// out of the headers dict in an enclosing `case dict.get(...) { Ok(<var>) ->`.
/// The router additionally opens a numeric parse case for int/float scalar
/// params, binding `<bound_var>_parsed` on success.
pub fn header_required_expr(
bound_var: String,
param: spec.Parameter(Resolved),
) -> String {
single_value_required_expr(bound_var, spec.parameter_schema(param))
}
/// Generate expression for an optional header parameter.
pub fn header_optional_expr(
key: String,
param: spec.Parameter(Resolved),
) -> String {
let lookup = "dict.get(headers, \"" <> key <> "\")"
single_value_optional_expr(lookup, "v", "_", spec.parameter_schema(param))
}
/// Generate expression for a required cookie parameter.
///
/// `bound_var` is the raw cookie String the router has already pulled out
/// via `case cookie_lookup(...) { Ok(<var>) ->`. As with the header helper
/// the router pre-binds `<bound_var>_parsed` for numeric scalar types.
pub fn cookie_required_expr(
bound_var: String,
param: spec.Parameter(Resolved),
) -> String {
single_value_required_expr(bound_var, spec.parameter_schema(param))
}
/// Generate expression for an optional cookie parameter.
pub fn cookie_optional_expr(
key: String,
param: spec.Parameter(Resolved),
) -> String {
let lookup = "cookie_lookup(headers, \"" <> key <> "\")"
single_value_optional_expr(
lookup,
"v",
"Error(_)",
spec.parameter_schema(param),
)
}
/// Generate a required value expression for a source that returns a single
/// String value. Used for headers (dict.get returns single string) and
/// cookies. Header / cookie arrays are comma-separated in a single string.
///
/// The lookup itself (and any int/float parse) is wrapped in an enclosing
/// case in the router, which binds `bound_var` (and `<bound_var>_parsed`
/// for numeric scalars). This helper just produces the value expression
/// that consumes those bound names — it never emits `let assert`.
fn single_value_required_expr(
bound_var: String,
schema_ref: Option(SchemaRef),
) -> String {
case schema_ref {
Some(Inline(schema.ArraySchema(items: Inline(schema.StringSchema(..)), ..))) ->
"list.map(string.split("
<> bound_var
<> ", \",\"), fn(item) { string.trim(item) })"
Some(Inline(schema.ArraySchema(items: Inline(schema.IntegerSchema(..)), ..))) ->
// Router pre-parses to a List(Int) and binds <bound_var>_parsed_list.
bound_var <> "_parsed_list"
Some(Inline(schema.ArraySchema(items: Inline(schema.NumberSchema(..)), ..))) ->
bound_var <> "_parsed_list"
Some(Inline(schema.ArraySchema(items: Inline(schema.BooleanSchema(..)), ..))) ->
"list.map(string.split("
<> bound_var
<> ", \",\"), fn(item) { "
<> array_item_bool_parse(True)
<> " })"
Some(Inline(schema.IntegerSchema(..))) -> bound_var <> "_parsed"
Some(Inline(schema.NumberSchema(..))) -> bound_var <> "_parsed"
Some(Inline(schema.BooleanSchema(..))) ->
"{ let v = " <> bound_var <> " " <> bool_parse_expr <> " }"
_ -> bound_var
}
}
/// Generate an optional expression for a source that returns a single value.
fn single_value_optional_expr(
lookup: String,
var: String,
miss: String,
schema_ref: Option(SchemaRef),
) -> String {
case schema_ref {
Some(Inline(schema.ArraySchema(items: Inline(schema.StringSchema(..)), ..))) ->
"case "
<> lookup
<> " { Ok("
<> var
<> ") -> Some(list.map(string.split("
<> var
<> ", \",\"), fn(item) { string.trim(item) })) "
<> miss
<> " -> None }"
Some(Inline(schema.ArraySchema(items: Inline(schema.IntegerSchema(..)), ..))) ->
"case "
<> lookup
<> " { Ok("
<> var
<> ") -> Some(list.map(string.split("
<> var
<> ", \",\"), fn(item) { "
<> array_item_int_parse(True)
<> " })) "
<> miss
<> " -> None }"
Some(Inline(schema.ArraySchema(items: Inline(schema.NumberSchema(..)), ..))) ->
"case "
<> lookup
<> " { Ok("
<> var
<> ") -> Some(list.map(string.split("
<> var
<> ", \",\"), fn(item) { "
<> array_item_float_parse(True)
<> " })) "
<> miss
<> " -> None }"
Some(Inline(schema.ArraySchema(items: Inline(schema.BooleanSchema(..)), ..))) ->
"case "
<> lookup
<> " { Ok("
<> var
<> ") -> Some(list.map(string.split("
<> var
<> ", \",\"), fn(item) { "
<> array_item_bool_parse(True)
<> " })) "
<> miss
<> " -> None }"
Some(Inline(schema.IntegerSchema(..))) ->
"case "
<> lookup
<> " { Ok("
<> var
<> ") -> { case int.parse("
<> var
<> ") { Ok(n) -> Some(n) _ -> None } } "
<> miss
<> " -> None }"
Some(Inline(schema.NumberSchema(..))) ->
"case "
<> lookup
<> " { Ok("
<> var
<> ") -> { case float.parse("
<> var
<> ") { Ok(n) -> Some(n) _ -> None } } "
<> miss
<> " -> None }"
Some(Inline(schema.BooleanSchema(..))) ->
"case "
<> lookup
<> " { Ok("
<> var
<> ") -> Some("
<> bool_parse_expr
<> ") "
<> miss
<> " -> None }"
_ ->
"case "
<> lookup
<> " { Ok("
<> var
<> ") -> Some("
<> var
<> ") "
<> miss
<> " -> None }"
}
}
/// Generate the body decode expression for a request body.
pub fn generate_body_decode_expr(
rb: spec.RequestBody(Resolved),
op_id: String,
ctx: Context,
) -> String {
let content_entries = dict.to_list(rb.content)
case content_entries {
[#(ct_name, media_type)] ->
case content_type.from_string(ct_name) {
content_type.ApplicationJson -> {
let decode_fn = case media_type.schema {
Some(Reference(name:, ..)) ->
"decode.decode_" <> naming.to_snake_case(name) <> "(body)"
_ ->
"decode.decode_"
<> naming.to_snake_case(op_id)
<> "_request_body(body)"
}
case rb.required {
True -> "{ let assert Ok(decoded) = " <> decode_fn <> " decoded }"
False ->
"case body { \"\" -> None _ -> { case "
<> decode_fn
<> " { Ok(decoded) -> Some(decoded) _ -> None } } }"
}
}
content_type.FormUrlEncoded -> {
let body_expr = form_urlencoded_body_constructor_expr(rb, op_id, ctx)
case rb.required {
True -> body_expr
False -> "case body { \"\" -> None _ -> Some(" <> body_expr <> ") }"
}
}
content_type.MultipartFormData -> {
let body_expr = multipart_body_constructor_expr(rb, op_id, ctx)
case rb.required {
True -> body_expr
False -> "case body { \"\" -> None _ -> Some(" <> body_expr <> ") }"
}
}
_ -> {
case rb.required {
True -> "body"
False -> "case body { \"\" -> None _ -> Some(body) }"
}
}
}
_ -> {
case rb.required {
True -> "body"
False -> "case body { \"\" -> None _ -> Some(body) }"
}
}
}
}