Packages
oaspec
0.53.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/router_ir.gleam
import gleam/dict
import gleam/list
import gleam/option.{None, Some}
import gleam/string
import oaspec/config
import oaspec/internal/codegen/context.{type Context}
import oaspec/internal/codegen/guards
import oaspec/internal/codegen/import_analysis
import oaspec/internal/codegen/server_request_decode as decode_helpers
import oaspec/internal/openapi/schema.{Inline, Reference}
import oaspec/internal/openapi/spec.{type Resolved, Value}
import oaspec/internal/util/content_type
import oaspec/internal/util/naming
/// Structured requirements for router generation.
pub type RouterRequirements {
RouterRequirements(
has_deep_object: Bool,
has_deep_object_with_ap: Bool,
needs_deep_object_present: Bool,
has_deep_object_untyped_ap: Bool,
has_form_urlencoded_body: Bool,
has_multipart_body: Bool,
has_binary_request_body: Bool,
needs_bit_array_import: Bool,
has_nested_form_urlencoded_body: Bool,
needs_int: Bool,
needs_float: Bool,
needs_bool: Bool,
needs_string: Bool,
needs_cookie_lookup: Bool,
needs_list_import: Bool,
needs_uri_import: Bool,
needs_option: Bool,
needs_json: Bool,
needs_json_for_guards: Bool,
needs_decode: Bool,
needs_encode: Bool,
uses_query: Bool,
uses_headers: Bool,
uses_body: Bool,
has_params_ops: Bool,
has_enum_ref_params: Bool,
needs_guards: Bool,
)
}
/// Compute semantic requirements for the generated router module.
pub fn analyze(ctx: Context) -> RouterRequirements {
let operations = context.operations(ctx)
let has_deep_object =
list.any(operations, fn(op) {
let #(_, operation, _, _) = op
list.any(operation.parameters, fn(ref_p) {
case ref_p {
Value(p) -> decode_helpers.is_deep_object_param(p, ctx)
_ -> False
}
})
})
let has_deep_object_with_ap =
list.any(operations, fn(op) {
let #(_, operation, _, _) = op
list.any(operation.parameters, fn(ref_p) {
case ref_p {
Value(p) ->
decode_helpers.is_deep_object_param(p, ctx)
&& decode_helpers.deep_object_has_additional_properties(p, ctx)
_ -> False
}
})
})
let needs_deep_object_present =
list.any(operations, fn(op) {
let #(_, operation, _, _) = op
list.any(operation.parameters, fn(ref_p) {
case ref_p {
Value(p) ->
decode_helpers.is_deep_object_param(p, ctx)
&& !p.required
&& !decode_helpers.deep_object_has_untyped_additional_properties(
p,
ctx,
)
_ -> False
}
})
})
let has_deep_object_untyped_ap =
list.any(operations, fn(op) {
let #(_, operation, _, _) = op
list.any(operation.parameters, fn(ref_p) {
case ref_p {
Value(p) ->
decode_helpers.is_deep_object_param(p, ctx)
&& decode_helpers.deep_object_has_untyped_additional_properties(
p,
ctx,
)
_ -> False
}
})
})
let has_form_urlencoded_body =
list.any(operations, fn(op) {
let #(_, operation, _, _) = op
decode_helpers.operation_uses_form_urlencoded_body(operation)
})
let has_multipart_body =
list.any(operations, fn(op) {
let #(_, operation, _, _) = op
decode_helpers.operation_uses_multipart_body(operation)
})
// Issue #485: when any operation declares
// `application/octet-stream` on its request body, the route
// function takes `body: BitArray` instead of `body: String` so
// arbitrary binary payloads round-trip without being forced
// through `bit_array.to_string`. Specs without any binary
// request body keep the existing `body: String` signature.
let has_binary_request_body =
list.any(operations, fn(op) {
let #(_, operation, _, _) = op
decode_helpers.operation_uses_octet_stream_body(operation)
})
// The route arms shadow `body` with the String conversion
// only for non-binary arms that actually have a request body.
// The `bit_array` / `result` imports are needed iff at least
// one such arm exists alongside a binary one.
let needs_bit_array_import =
has_binary_request_body
&& list.any(operations, fn(op) {
let #(_, operation, _, _) = op
option.is_some(operation.request_body)
&& !decode_helpers.operation_uses_octet_stream_body(operation)
})
let has_nested_form_urlencoded_body =
list.any(operations, fn(op) {
let #(_, operation, _, _) = op
case operation.request_body {
Some(Value(rb)) ->
decode_helpers.form_urlencoded_body_has_nested_object(rb, ctx)
_ -> False
}
})
let needs_int =
list.any(operations, fn(op) {
let #(_, operation, _, _) = op
list.any(operation.parameters, fn(ref_p) {
case ref_p {
Value(p) ->
decode_helpers.query_schema_needs_int(spec.parameter_schema(p))
|| decode_helpers.deep_object_param_needs_int(p, ctx)
_ -> False
}
})
|| case operation.request_body {
Some(Value(rb)) ->
decode_helpers.form_urlencoded_body_needs_int(rb, ctx)
_ -> False
}
|| case operation.request_body {
Some(Value(rb)) -> decode_helpers.multipart_body_needs_int(rb, ctx)
_ -> False
}
})
|| operations_have_response_header_of_type(operations, "Int")
let needs_float =
list.any(operations, fn(op) {
let #(_, operation, _, _) = op
list.any(operation.parameters, fn(ref_p) {
case ref_p {
Value(p) ->
decode_helpers.query_schema_needs_float(spec.parameter_schema(p))
|| decode_helpers.deep_object_param_needs_float(p, ctx)
_ -> False
}
})
|| case operation.request_body {
Some(Value(rb)) ->
decode_helpers.form_urlencoded_body_needs_float(rb, ctx)
_ -> False
}
|| case operation.request_body {
Some(Value(rb)) -> decode_helpers.multipart_body_needs_float(rb, ctx)
_ -> False
}
})
|| operations_have_response_header_of_type(operations, "Float")
let needs_bool = operations_have_response_header_of_type(operations, "Bool")
let needs_string =
has_form_urlencoded_body
|| has_multipart_body
|| has_deep_object_with_ap
|| list.any(operations, fn(op) {
let #(_, operation, _, _) = op
list.any(operation.parameters, fn(ref_p) {
case ref_p {
Value(p) ->
case p.in_ {
spec.InCookie -> True
spec.InQuery | spec.InHeader ->
decode_helpers.query_schema_needs_string(spec.parameter_schema(
p,
))
|| decode_helpers.deep_object_param_needs_string(p, ctx)
spec.InPath ->
decode_helpers.query_schema_needs_string(spec.parameter_schema(
p,
))
}
_ -> False
}
})
|| case operation.request_body {
Some(Value(rb)) ->
decode_helpers.form_urlencoded_body_needs_string(rb, ctx)
_ -> False
}
})
let needs_cookie_lookup =
list.any(operations, fn(op) {
let #(_, operation, _, _) = op
list.any(operation.parameters, fn(ref_p) {
case ref_p {
Value(p) -> p.in_ == spec.InCookie
_ -> False
}
})
})
let needs_list_import =
needs_cookie_lookup
|| has_deep_object
|| has_form_urlencoded_body
|| has_multipart_body
|| operations_have_response_headers(operations)
|| list.any(operations, fn(op) {
let #(_, operation, _, _) = op
list.any(operation.parameters, fn(ref_p) {
case ref_p {
Value(p) ->
case p.in_, spec.parameter_schema(p) {
spec.InQuery, Some(Inline(schema.ArraySchema(..))) -> True
spec.InHeader, Some(Inline(schema.ArraySchema(..))) -> True
_, _ -> False
}
_ -> False
}
})
})
let needs_uri_import = needs_cookie_lookup || has_form_urlencoded_body
let needs_option =
import_analysis.operations_have_optional_params(operations)
|| import_analysis.operations_have_optional_body(operations)
|| list.any(operations, fn(op) {
let #(_, operation, _, _) = op
let has_optional_deep_object_fields =
list.any(operation.parameters, fn(ref_p) {
case ref_p {
Value(p) ->
decode_helpers.deep_object_param_has_optional_fields(p, ctx)
_ -> False
}
})
let has_optional_form_urlencoded_fields = case operation.request_body {
Some(Value(rb)) ->
decode_helpers.form_urlencoded_body_has_optional_fields(rb, ctx)
_ -> False
}
let has_optional_multipart_fields = case operation.request_body {
Some(Value(rb)) ->
decode_helpers.multipart_body_has_optional_fields(rb, ctx)
_ -> False
}
has_optional_deep_object_fields
|| has_optional_form_urlencoded_fields
|| has_optional_multipart_fields
})
|| operations_have_optional_response_header(operations)
let needs_json =
list.any(operations, fn(op) {
let #(_, operation, _, _) = op
list.any(dict.to_list(operation.responses), fn(entry) {
let #(_, ref_or) = entry
case ref_or {
Value(response) -> {
let content_entries = dict.to_list(response.content)
case content_entries {
[#(media_type_name, media_type)] ->
case content_type.is_json_compatible(media_type_name) {
True ->
case media_type.schema {
Some(_) -> True
None -> False
}
False -> False
}
_ -> False
}
}
_ -> False
}
})
})
let needs_decode =
list.any(operations, fn(op) {
let #(_, operation, _, _) = op
case operation.request_body {
Some(Value(rb)) ->
list.any(dict.to_list(rb.content), fn(entry) {
let #(content_type_name, _) = entry
content_type.is_json_compatible(content_type_name)
})
_ -> False
}
})
let uses_query =
list.any(operations, fn(op) {
let #(_, operation, _, _) = op
list.any(operation.parameters, fn(ref_p) {
case ref_p {
Value(p) -> p.in_ == spec.InQuery
_ -> False
}
})
})
let uses_headers =
has_multipart_body
|| list.any(operations, fn(op) {
let #(_, operation, _, _) = op
list.any(operation.parameters, fn(ref_p) {
case ref_p {
Value(p) -> p.in_ == spec.InHeader || p.in_ == spec.InCookie
_ -> False
}
})
})
let uses_body =
list.any(operations, fn(op) {
let #(_, operation, _, _) = op
option.is_some(operation.request_body)
})
let has_params_ops =
list.any(operations, fn(op) {
let #(_, operation, _, _) = op
!list.is_empty(operation.parameters)
|| option.is_some(operation.request_body)
})
let has_enum_ref_params =
decode_helpers.operations_have_enum_ref_params(operations, ctx)
let needs_guards =
config.validate(context.config(ctx))
&& list.any(operations, fn(op) {
let #(_, operation, _, _) = op
operation_needs_guard_validation(operation, ctx)
})
let needs_json_for_guards = needs_guards
RouterRequirements(
has_deep_object: has_deep_object,
has_deep_object_with_ap: has_deep_object_with_ap,
needs_deep_object_present: needs_deep_object_present,
has_deep_object_untyped_ap: has_deep_object_untyped_ap,
has_form_urlencoded_body: has_form_urlencoded_body,
has_multipart_body: has_multipart_body,
has_binary_request_body: has_binary_request_body,
needs_bit_array_import: needs_bit_array_import,
has_nested_form_urlencoded_body: has_nested_form_urlencoded_body,
needs_int: needs_int,
needs_float: needs_float,
needs_bool: needs_bool,
needs_string: needs_string,
needs_cookie_lookup: needs_cookie_lookup,
needs_list_import: needs_list_import,
needs_uri_import: needs_uri_import,
needs_option: needs_option,
needs_json: needs_json,
needs_json_for_guards: needs_json_for_guards,
needs_decode: needs_decode,
needs_encode: needs_json,
uses_query: uses_query,
uses_headers: uses_headers,
uses_body: uses_body,
has_params_ops: has_params_ops,
has_enum_ref_params: has_enum_ref_params,
needs_guards: needs_guards,
)
}
/// Render the final import list from structured requirements.
pub fn imports(requirements: RouterRequirements, ctx: Context) -> List(String) {
let std_imports = ["gleam/dict.{type Dict}"]
let std_imports = case requirements.needs_list_import {
True -> list.append(std_imports, ["gleam/list"])
False -> std_imports
}
let std_imports = case requirements.needs_uri_import {
True -> list.append(std_imports, ["gleam/uri"])
False -> std_imports
}
let std_imports = case requirements.needs_int {
True -> list.append(std_imports, ["gleam/int"])
False -> std_imports
}
let std_imports = case requirements.needs_float {
True -> list.append(std_imports, ["gleam/float"])
False -> std_imports
}
let std_imports = case requirements.needs_bool {
True -> list.append(std_imports, ["gleam/bool"])
False -> std_imports
}
let std_imports = case requirements.needs_option {
True -> list.append(std_imports, ["gleam/option.{None, Some}"])
False -> std_imports
}
let std_imports = case
requirements.needs_json || requirements.needs_json_for_guards
{
True -> list.append(std_imports, ["gleam/json"])
False -> std_imports
}
let std_imports = case requirements.needs_string {
True -> list.append(std_imports, ["gleam/string"])
False -> std_imports
}
let std_imports = case requirements.has_deep_object_untyped_ap {
True -> list.append(std_imports, ["gleam/dynamic"])
False -> std_imports
}
// Issue #485: when the route signature is `body: BitArray`
// and at least one non-binary arm exists, every non-binary
// arm shadows `body` with
// `bit_array.to_string(body) |> result.unwrap("")`, so both
// `gleam/bit_array` and `gleam/result` need to be imported.
// Pure-binary specs skip the imports to avoid unused warnings.
let std_imports = case requirements.needs_bit_array_import {
True -> list.append(std_imports, ["gleam/bit_array", "gleam/result"])
False -> std_imports
}
let pkg_imports = [
config.package(context.config(ctx)) <> "/handlers",
config.package(context.config(ctx)) <> "/handlers_generated",
]
let pkg_imports = case
requirements.has_deep_object
|| requirements.has_form_urlencoded_body
|| requirements.has_multipart_body
|| requirements.has_enum_ref_params
{
True ->
list.append(pkg_imports, [config.package(context.config(ctx)) <> "/types"])
False -> pkg_imports
}
let pkg_imports = case requirements.needs_decode {
True ->
list.append(pkg_imports, [
config.package(context.config(ctx)) <> "/decode",
])
False -> pkg_imports
}
let pkg_imports = case requirements.needs_encode {
True ->
list.append(pkg_imports, [
config.package(context.config(ctx)) <> "/encode",
])
False -> pkg_imports
}
let pkg_imports = case requirements.has_params_ops {
True ->
list.append(pkg_imports, [
config.package(context.config(ctx)) <> "/request_types",
config.package(context.config(ctx)) <> "/response_types",
])
False ->
list.append(pkg_imports, [
config.package(context.config(ctx)) <> "/response_types",
])
}
let pkg_imports = case requirements.needs_guards {
True ->
list.append(pkg_imports, [
config.package(context.config(ctx)) <> "/guards",
])
False -> pkg_imports
}
list.append(std_imports, pkg_imports)
}
fn operation_needs_guard_validation(
operation: spec.Operation(Resolved),
ctx: Context,
) -> Bool {
let needs_body_guard = case operation.request_body {
Some(Value(rb)) -> {
let content_entries = dict.to_list(rb.content)
case content_entries {
[#(_, mt)] ->
case mt.schema {
Some(Inline(schema.ObjectSchema(..)))
| Some(Inline(schema.AllOfSchema(..))) -> True
Some(Reference(name:, ..)) -> guards.schema_has_validator(name, ctx)
_ -> False
}
_ -> False
}
}
_ -> False
}
config.validate(context.config(ctx)) && needs_body_guard
}
fn operations_have_response_headers(
operations: List(context.AnalyzedOperation),
) -> Bool {
list.any(operations, fn(op) {
let #(_, operation, _, _) = op
list.any(dict.to_list(operation.responses), fn(entry) {
let #(_, ref_or) = entry
case ref_or {
Value(response) -> !dict.is_empty(response.headers)
_ -> False
}
})
})
}
fn operations_have_response_header_of_type(
operations: List(context.AnalyzedOperation),
type_name: String,
) -> Bool {
list.any(operations, fn(op) {
let #(_, operation, _, _) = op
list.any(dict.to_list(operation.responses), fn(entry) {
let #(_, ref_or) = entry
case ref_or {
Value(response) ->
list.any(sorted_header_specs(response.headers), fn(spec) {
spec.field_type == type_name
})
_ -> False
}
})
})
}
fn operations_have_optional_response_header(
operations: List(context.AnalyzedOperation),
) -> Bool {
list.any(operations, fn(op) {
let #(_, operation, _, _) = op
list.any(dict.to_list(operation.responses), fn(entry) {
let #(_, ref_or) = entry
case ref_or {
Value(response) ->
list.any(sorted_header_specs(response.headers), fn(spec) {
!spec.required
})
_ -> False
}
})
})
}
type HeaderSpec {
HeaderSpec(
header_name: String,
field_name: String,
field_type: String,
required: Bool,
)
}
fn sorted_header_specs(
headers: dict.Dict(String, spec.Header),
) -> List(HeaderSpec) {
headers
|> dict.to_list
|> list.sort(fn(a, b) { string.compare(a.0, b.0) })
|> list.map(fn(entry) {
let #(header_name, header) = entry
let field_name = naming.to_snake_case(header_name)
let field_type = response_header_field_type(header.schema)
HeaderSpec(
header_name: header_name,
field_name: field_name,
field_type: field_type,
required: header.required,
)
})
}
fn response_header_field_type(
schema_opt: option.Option(schema.SchemaRef),
) -> String {
case schema_opt {
Some(Inline(schema.IntegerSchema(..))) -> "Int"
Some(Inline(schema.NumberSchema(..))) -> "Float"
Some(Inline(schema.BooleanSchema(..))) -> "Bool"
Some(Inline(schema.StringSchema(..))) -> "String"
Some(Reference(name:, ..)) -> "types." <> naming.schema_to_type_name(name)
_ -> "String"
}
}