Packages
oaspec
0.58.1
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/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/internal/codegen/context.{type Context}
import oaspec/internal/codegen/ir_build
import oaspec/internal/codegen/operation_ir
import oaspec/internal/openapi/schema.{
type AdditionalProperties, type SchemaRef, Forbidden, Inline, ObjectSchema,
Reference, Typed, Unspecified, Untyped,
}
import oaspec/internal/openapi/spec.{type Resolved, Value}
import oaspec/internal/util/content_type
import oaspec/internal/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 context.resolve_schema_ref(schema_ref, 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),
ctx: Context,
) -> String {
query_required_expr_with_schema(
bound_var,
spec.parameter_schema(param),
Some(operation_ir.effective_explode(param)),
param.style,
ctx,
)
}
/// Lookup-based required-query expression for deep object constructor
/// fields. Top-level required query params already go through the
/// `query_required_expr` + router-side case scaffolding (Issue #263);
/// the deep-object path additionally falls back to a per-type default
/// when the constructor key is missing or the inner parse fails, so a
/// malformed deep-object request no longer panics inside the generated
/// handler. (#327)
fn deep_object_required_field_expr(
key: String,
schema_ref: Option(SchemaRef),
) -> String {
let base =
"case dict.get(query, \"" <> key <> "\") { Ok([v, ..]) -> v _ -> \"\" }"
case schema_ref {
Some(Inline(schema.ArraySchema(items: Inline(schema.StringSchema(..)), ..))) ->
"case dict.get(query, \""
<> key
<> "\") { Ok(vs) -> list.map(vs, fn(item) { string.trim(item) }) _ -> [] }"
Some(Inline(schema.ArraySchema(items: Inline(schema.IntegerSchema(..)), ..))) ->
"case dict.get(query, \""
<> key
<> "\") { Ok(vs) -> 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(..)), ..))) ->
"case dict.get(query, \""
<> key
<> "\") { Ok(vs) -> 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(..)), ..))) ->
"case dict.get(query, \""
<> key
<> "\") { Ok(vs) -> list.map(vs, fn(item) { let v = string.trim(item) "
<> bool_parse_expr
<> " }) _ -> [] }"
Some(Inline(schema.IntegerSchema(..))) ->
"case dict.get(query, \""
<> key
<> "\") { Ok([v, ..]) -> case int.parse(v) { Ok(n) -> n _ -> 0 } _ -> 0 }"
Some(Inline(schema.NumberSchema(..))) ->
"case dict.get(query, \""
<> key
<> "\") { Ok([v, ..]) -> case float.parse(v) { Ok(n) -> n _ -> 0.0 } _ -> 0.0 }"
Some(Inline(schema.BooleanSchema(..))) ->
"case dict.get(query, \""
<> key
<> "\") { Ok([v, ..]) -> "
<> bool_parse_expr
<> " _ -> False }"
_ -> base
}
}
fn query_required_expr_with_schema(
bound_var: String,
schema_ref: Option(SchemaRef),
explode: Option(Bool),
style: Option(spec.ParameterStyle),
ctx: Context,
) -> 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 <> " }"
Some(ref) ->
case schema_ref_string_enum(ref, ctx) {
// Issue #305: required enum query params rely on the router-side
// open/close scaffolding (see `query_required_open_parse_case` /
// `close_single_value_required_parse_case` in server.gleam) to
// bind `<bound_var>_parsed` on a successful match and emit a 400
// on unknown values. The expression here is just the bound name.
Some(_) -> bound_var <> "_parsed"
None -> bound_var
}
None -> bound_var
}
}
/// Generate expression for an optional query parameter.
pub fn query_optional_expr(
key: String,
param: spec.Parameter(Resolved),
ctx: Context,
) -> String {
query_optional_expr_with_schema(
key,
spec.parameter_schema(param),
Some(operation_ir.effective_explode(param)),
param.style,
ctx,
)
}
/// Issue #522: classify the underlying scalar kind of a SchemaRef,
/// resolving `$ref` to its target schema. Used by query / header /
/// cookie parameter codegen so that ref-typed scalars get the same
/// `int.parse` / `float.parse` / boolean parsing as their inline
/// equivalents — not the raw-String fallback that pre-fix produced
/// (resulting in `Expected Option(Int), Found Option(String)` build
/// errors).
type ScalarKind {
IntegerScalar
NumberScalar
BooleanScalar
}
fn schema_ref_scalar_kind(
schema_ref: SchemaRef,
ctx: Context,
) -> Option(ScalarKind) {
case context.resolve_schema_ref(schema_ref, ctx) {
Ok(schema.IntegerSchema(..)) -> Some(IntegerScalar)
Ok(schema.NumberSchema(..)) -> Some(NumberScalar)
Ok(schema.BooleanSchema(..)) -> Some(BooleanScalar)
_ -> None
}
}
/// If `schema_ref` is a `$ref` to a string enum schema, return the
/// generated Gleam type name and the list of enum values (in spec
/// order). Used by query parameter codegen to emit a string→variant
/// match for `$ref`-based enum query parameters (issue #305).
pub fn schema_ref_string_enum(
schema_ref: SchemaRef,
ctx: Context,
) -> Option(#(String, List(String))) {
case schema_ref {
Reference(name: ref_name, ..) ->
case context.resolve_schema_ref(schema_ref, ctx) {
Ok(schema.StringSchema(enum_values: values, ..)) ->
case values {
[_, ..] -> Some(#(naming.schema_to_type_name(ref_name), values))
[] -> None
}
_ -> None
}
_ -> None
}
}
/// True iff any query / header / cookie parameter on any of the given
/// operations has a `$ref` schema that resolves to a string-enum.
///
/// Issue #318: the router emits `types.<EnumType><Variant>` references
/// for these parameters (see `enum_match_result_expr` and
/// `enum_match_option_expr` below), but the import of the `types`
/// module is gated on a separate set of features (deep object, form,
/// multipart). This helper lets the router-import logic ask "does any
/// of my emitted code reference `types`?" without re-implementing the
/// per-param dispatch.
pub fn operations_have_enum_ref_params(
operations: List(context.AnalyzedOperation),
ctx: Context,
) -> Bool {
list.any(operations, fn(op) {
let #(_, operation, _, _) = op
list.any(operation.parameters, fn(ref_p) {
case ref_p {
Value(p) -> parameter_is_enum_ref(p, ctx)
_ -> False
}
})
})
}
fn parameter_is_enum_ref(p: spec.Parameter(Resolved), ctx: Context) -> Bool {
case p.in_, spec.parameter_schema(p) {
spec.InQuery, Some(s) | spec.InHeader, Some(s) | spec.InCookie, Some(s) ->
case schema_ref_string_enum(s, ctx) {
Some(_) -> True
None -> False
}
_, _ -> False
}
}
/// Render the inline Gleam expression that converts a raw query-string
/// `<bound_var>` into `Result(types.<TypeName><Variant>, Nil)`. Used by
/// the required-enum query path (server.gleam opens a case on this
/// expression; the Ok arm binds `<bound_var>_parsed`).
pub fn enum_match_result_expr(
bound_var: String,
type_name: String,
values: List(String),
) -> String {
let arms =
values
|> list.map(fn(v) {
"\""
<> v
<> "\" -> Ok(types."
<> type_name
<> naming.to_pascal_case(v)
<> ")"
})
|> string.join(" ")
"case " <> bound_var <> " { " <> arms <> " _ -> Error(Nil) }"
}
/// Render the inline Gleam expression that converts a raw query-string
/// `<v_var>` into `Option(types.<TypeName><Variant>)` for an optional
/// enum query parameter. Unknown values map to `None`, matching the
/// permissive behaviour of Int / Float optional query handling.
fn enum_match_option_expr(
v_var: String,
type_name: String,
values: List(String),
) -> String {
let arms =
values
|> list.map(fn(v) {
"\""
<> v
<> "\" -> Some(types."
<> type_name
<> naming.to_pascal_case(v)
<> ")"
})
|> string.join(" ")
"case " <> v_var <> " { " <> arms <> " _ -> None }"
}
fn query_optional_expr_with_schema(
key: String,
schema_ref: Option(SchemaRef),
explode: Option(Bool),
style: Option(spec.ParameterStyle),
ctx: Context,
) -> String {
let delim = operation_ir.delimiter_for_style(style)
case schema_ref {
// Issue #526: for `style: pipeDelimited` / `style:
// spaceDelimited` / `style: form, explode: false` (i.e. delim
// != ","), the previous codegen used `Ok([v, ..])` — taking
// only the FIRST occurrence — and `list.map(string.split(...))`
// — leaving empty splits as empty-string elements. Two
// edge-case bugs followed: `?tags=` (empty value) became
// `Some([""])` instead of `Some([])`, and `?tags=a&tags=b`
// (repeated keys) silently dropped `b`. The new shape uses
// `Ok(vs)` to accept ALL occurrences, flat_maps the splits
// across them, and filters out the empty strings produced by
// empty input or trailing delimiters (`?tags=foo|`).
Some(Inline(schema.ArraySchema(items: Inline(schema.StringSchema(..)), ..))) ->
case explode {
Some(False) ->
"case dict.get(query, \""
<> key
<> "\") { Ok(vs) -> Some(vs |> list.flat_map(fn(v) { string.split(v, \""
<> delim
<> "\") }) |> list.map(string.trim) |> list.filter(fn(s) { s != \"\" })) _ -> 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(vs) -> Some(vs |> list.flat_map(fn(v) { string.split(v, \""
<> delim
<> "\") }) |> list.filter_map(fn(item) { let trimmed = string.trim(item) case trimmed { \"\" -> Error(Nil) _ -> int.parse(trimmed) } })) _ -> 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(vs) -> Some(vs |> list.flat_map(fn(v) { string.split(v, \""
<> delim
<> "\") }) |> list.filter_map(fn(item) { let trimmed = string.trim(item) case trimmed { \"\" -> Error(Nil) _ -> float.parse(trimmed) } })) _ -> 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(vs) -> Some(vs |> list.flat_map(fn(v) { string.split(v, \""
<> delim
<> "\") }) |> list.filter(fn(s) { string.trim(s) != \"\" }) |> list.map(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 }"
Some(ref) ->
case schema_ref_string_enum(ref, ctx) {
Some(#(type_name, values)) -> {
// Issue #305: enum-typed query params need string→variant
// conversion. Unknown values fall through to None, matching
// the permissive handling of malformed Int / Float optional
// query params.
"case dict.get(query, \""
<> key
<> "\") { Ok([v, ..]) -> "
<> enum_match_option_expr("v", type_name, values)
<> " _ -> None }"
}
None ->
// Issue #522: when the `$ref` resolves to an integer /
// number / boolean schema, mirror the parse handling of
// the corresponding `Inline(...)` arms above. Pre-fix this
// path emitted `Some(v)` (a String) into a typed
// `Option(Int|Float|Bool)` slot, breaking `gleam build`.
case schema_ref_scalar_kind(ref, ctx) {
Some(IntegerScalar) ->
"case dict.get(query, \""
<> key
<> "\") { Ok([v, ..]) -> { case int.parse(v) { Ok(n) -> Some(n) _ -> None } } _ -> None }"
Some(NumberScalar) ->
"case dict.get(query, \""
<> key
<> "\") { Ok([v, ..]) -> { case float.parse(v) { Ok(n) -> Some(n) _ -> None } } _ -> None }"
Some(BooleanScalar) ->
"case dict.get(query, \""
<> key
<> "\") { Ok([v, ..]) -> Some("
<> bool_parse_expr
<> ") _ -> None }"
None ->
"case dict.get(query, \""
<> key
<> "\") { Ok([v, ..]) -> Some(v) _ -> None }"
}
}
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 context.resolve_schema_ref(schema_ref, 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,
ctx,
)
}
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 context.resolve_schema_ref(schema_ref, 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), ctx)
})
}
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), ctx)
})
}
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")
}
/// Issue #485: an `application/octet-stream` request body is raw
/// bytes, so the router takes `body: BitArray` instead of
/// `body: String`. The single-arm dispatch (no other content types
/// allowed alongside binary) is enforced by the validator.
fn request_body_uses_octet_stream(rb: spec.RequestBody(Resolved)) -> Bool {
dict.has_key(rb.content, "application/octet-stream")
}
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
}
}
pub fn operation_uses_octet_stream_body(
operation: spec.Operation(Resolved),
) -> Bool {
case operation.request_body {
Some(Value(rb)) -> request_body_uses_octet_stream(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 context.resolve_schema_ref(schema_ref, 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 context.resolve_schema_ref(schema_ref, 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 context.resolve_schema_ref(schema_ref, 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),
ctx: Context,
) -> Bool {
case schema_ref {
Some(Inline(schema.IntegerSchema(..))) -> True
Some(Inline(schema.ArraySchema(items: Inline(schema.IntegerSchema(..)), ..))) ->
True
// Issue #522: a `$ref` whose target is `type: integer` triggers
// `int.parse` emission downstream, so the router needs the
// `gleam/int` import. Pre-fix, only inline scalars were detected.
Some(Reference(..) as ref) ->
case schema_ref_scalar_kind(ref, ctx) {
Some(IntegerScalar) -> True
_ -> False
}
_ -> False
}
}
pub fn query_schema_needs_float(
schema_ref: Option(SchemaRef),
ctx: Context,
) -> Bool {
case schema_ref {
Some(Inline(schema.NumberSchema(..))) -> True
Some(Inline(schema.ArraySchema(items: Inline(schema.NumberSchema(..)), ..))) ->
True
Some(Reference(..) as ref) ->
case schema_ref_scalar_kind(ref, ctx) {
Some(NumberScalar) -> True
_ -> False
}
_ -> 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)
}
/// Inline string-enum dispatch for a required form/multipart body
/// field (issue #482). When the dict is missing the key, or the value
/// is outside the spec's enum set, falls back to the first variant —
/// mirroring the type-zero fallback used for primitive required body
/// fields (#327). The handler is responsible for any stricter
/// validation.
fn body_required_enum_expr(
lookup: String,
type_name: String,
values: List(String),
) -> String {
let default = case values {
[first, ..] -> "types." <> type_name <> naming.to_pascal_case(first)
[] -> ""
}
let arms =
values
|> list.map(fn(v) {
"\"" <> v <> "\" -> types." <> type_name <> naming.to_pascal_case(v)
})
|> string.join(" ")
"case "
<> lookup
<> " { Ok([v, ..]) -> case v { "
<> arms
<> " _ -> "
<> default
<> " } _ -> "
<> default
<> " }"
}
/// Inline string-enum dispatch for an optional form/multipart body
/// field (issue #482). Unknown values map to `None`, matching the
/// permissive behaviour already used for `$ref`-typed enum query
/// parameters (#305).
fn body_optional_enum_expr(
lookup: String,
miss: String,
type_name: String,
values: List(String),
) -> String {
"case "
<> lookup
<> " { Ok([v, ..]) -> "
<> enum_match_option_expr("v", type_name, values)
<> " "
<> miss
<> " -> None }"
}
/// 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).
///
/// The emitted snippet falls back to a per-type default when the body
/// dict is missing the key or the inner parse fails. The previous
/// `let assert Ok(...)` shape would crash the BEAM process on a malformed
/// request body; the safe-default shape returns the type-zero
/// (empty string / 0 / 0.0 / False / empty list) so the handler can
/// still answer rather than aborting. Validation of those defaults is
/// the application's job. (#327)
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 = "case " <> lookup <> " { Ok([v, ..]) -> v _ -> \"\" }"
// Issue #482: a `$ref`-typed string-enum field must dispatch to the
// generated sum-type variant. Without this branch the codegen falls
// through to `base` (raw String), which fails `gleam check` because
// the field's compile-time type is `types.<EnumName>`.
case
schema_ref
|> option.then(fn(ref) { schema_ref_string_enum(ref, ctx) })
{
Some(#(type_name, values)) ->
body_required_enum_expr(lookup, type_name, values)
None -> body_required_expr_kind(lookup, base, schema_ref, trim_items, ctx)
}
}
/// Existing per-`BodyFieldKind` dispatch for `body_required_expr`,
/// extracted so the enum check above can short-circuit cleanly.
fn body_required_expr_kind(
lookup: String,
base: String,
schema_ref: Option(SchemaRef),
trim_items: Bool,
ctx: Context,
) -> String {
case schema_ref_body_field_kind(schema_ref, ctx) {
BodyFieldStringArray ->
case trim_items {
True ->
"case "
<> lookup
<> " { Ok(vs) -> list.map(vs, fn(item) { string.trim(item) }) _ -> [] }"
False -> "case " <> lookup <> " { Ok(vs) -> vs _ -> [] }"
}
BodyFieldIntArray ->
"case "
<> lookup
<> " { Ok(vs) -> list.map(vs, fn(item) { "
<> array_item_int_parse(trim_items)
<> " }) _ -> [] }"
BodyFieldFloatArray ->
"case "
<> lookup
<> " { Ok(vs) -> list.map(vs, fn(item) { "
<> array_item_float_parse(trim_items)
<> " }) _ -> [] }"
BodyFieldBoolArray ->
"case "
<> lookup
<> " { Ok(vs) -> list.map(vs, fn(item) { "
<> array_item_bool_parse(trim_items)
<> " }) _ -> [] }"
BodyFieldInt ->
"case "
<> lookup
<> " { Ok([v, ..]) -> case int.parse(v) { Ok(n) -> n _ -> 0 } _ -> 0 }"
BodyFieldFloat ->
"case "
<> lookup
<> " { Ok([v, ..]) -> case float.parse(v) { Ok(n) -> n _ -> 0.0 } _ -> 0.0 }"
BodyFieldBool ->
"case "
<> lookup
<> " { Ok([v, ..]) -> "
<> bool_parse_expr
<> " _ -> False }"
_ -> 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 <> "\")"
// Issue #482: as in `body_required_expr`, a `$ref`-typed string-enum
// field needs string→variant dispatch. Without this branch the field
// ends up as `Some(v)` where `v: String` clashes with the
// `Option(types.<EnumName>)` slot.
case
schema_ref
|> option.then(fn(ref) { schema_ref_string_enum(ref, ctx) })
{
Some(#(type_name, values)) ->
body_optional_enum_expr(lookup, miss, type_name, values)
None -> body_optional_expr_kind(lookup, miss, schema_ref, trim_items, ctx)
}
}
/// Existing per-`BodyFieldKind` dispatch for `body_optional_expr`,
/// extracted so the enum check above can short-circuit cleanly.
fn body_optional_expr_kind(
lookup: String,
miss: String,
schema_ref: Option(SchemaRef),
trim_items: Bool,
ctx: Context,
) -> String {
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) }"
}
}
}
}