Packages
oaspec
0.57.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/schema_utils.gleam
/// Shared schema analysis utilities used by types.gleam and ir_build.gleam.
/// Extracted to break the import-cycle that previously forced ir_build.gleam
/// to mirror these helpers from types.gleam.
import gleam/dict
import gleam/list
import gleam/option.{type Option, None, Some}
import oaspec/internal/codegen/context.{type Context}
import oaspec/internal/openapi/schema.{
type SchemaObject, type SchemaRef, AllOfSchema, Forbidden, Inline,
ObjectSchema, OneOfSchema, Reference, StringSchema, Typed, Untyped,
}
/// Check if a schema has typed or untyped additionalProperties that would need Dict.
pub fn schema_has_additional_properties(
schema_ref: SchemaRef,
ctx: Context,
) -> Bool {
case schema_ref {
Inline(ObjectSchema(additional_properties: Typed(_), ..)) -> True
Inline(ObjectSchema(additional_properties: Untyped, ..)) -> True
Inline(AllOfSchema(schemas:, ..)) ->
list.any(schemas, fn(s) { schema_has_additional_properties(s, ctx) })
Reference(..) ->
case context.resolve_schema_ref(schema_ref, ctx) {
Ok(schema_obj) ->
schema_has_additional_properties(Inline(schema_obj), ctx)
// nolint: thrown_away_error -- unresolved refs are treated as not having additionalProperties; the resolver reports the ref error separately
Error(_) -> False
}
_ -> False
}
}
/// Check if a schema has `additionalProperties: false` (needs Dict for the
/// closed-schema unknown-key check at decode time). Recurses through allOf
/// children and resolves `$ref` so nested closed schemas are surfaced too.
pub fn schema_has_forbidden_additional_properties(
schema_ref: SchemaRef,
ctx: Context,
) -> Bool {
case schema_ref {
Inline(ObjectSchema(additional_properties: Forbidden, ..)) -> True
Inline(AllOfSchema(schemas:, ..)) ->
list.any(schemas, fn(s) {
schema_has_forbidden_additional_properties(s, ctx)
})
Reference(..) ->
case context.resolve_schema_ref(schema_ref, ctx) {
Ok(schema_obj) ->
schema_has_forbidden_additional_properties(Inline(schema_obj), ctx)
// nolint: thrown_away_error -- unresolved refs are treated as not having forbidden additionalProperties; the resolver reports the ref error separately
Error(_) -> False
}
_ -> False
}
}
/// Check if a schema is — or transitively contains — a `oneOf` with at least
/// one $ref variant and no discriminator. The strict-oneOf decoder body
/// generated for that shape uses `list.filter_map` and `result.map`, so the
/// generator must add `gleam/list` and `gleam/result` to the imports of
/// `decode.gleam` when this returns True.
pub fn schema_has_non_discriminator_oneof(
schema_ref: SchemaRef,
ctx: Context,
) -> Bool {
case schema_ref {
Inline(OneOfSchema(schemas:, discriminator: option.None, ..)) ->
list.any(schemas, fn(s) {
case s {
Reference(..) -> True
_ -> False
}
})
Inline(OneOfSchema(schemas:, ..)) ->
list.any(schemas, fn(s) { schema_has_non_discriminator_oneof(s, ctx) })
Inline(AllOfSchema(schemas:, ..)) ->
list.any(schemas, fn(s) { schema_has_non_discriminator_oneof(s, ctx) })
Reference(..) ->
case context.resolve_schema_ref(schema_ref, ctx) {
Ok(schema_obj) ->
schema_has_non_discriminator_oneof(Inline(schema_obj), ctx)
// nolint: thrown_away_error -- unresolved refs are treated as not having strict-oneOf needs; the resolver reports the ref error separately
Error(_) -> False
}
_ -> False
}
}
/// Check if a schema has untyped additionalProperties (needs Dynamic import).
pub fn schema_has_untyped_additional_properties(
schema_ref: SchemaRef,
ctx: Context,
) -> Bool {
case schema_ref {
Inline(ObjectSchema(additional_properties: Untyped, ..)) -> True
Inline(AllOfSchema(schemas:, ..)) ->
list.any(schemas, fn(s) {
schema_has_untyped_additional_properties(s, ctx)
})
Reference(..) ->
case context.resolve_schema_ref(schema_ref, ctx) {
Ok(schema_obj) ->
schema_has_untyped_additional_properties(Inline(schema_obj), ctx)
// nolint: thrown_away_error -- unresolved refs are treated as not having untyped additionalProperties; the resolver reports the ref error separately
Error(_) -> False
}
_ -> False
}
}
/// Check if a schema has any optional or nullable fields that would need Option.
pub fn schema_has_optional_fields(schema_ref: SchemaRef, ctx: Context) -> Bool {
case schema_ref {
Inline(ObjectSchema(properties:, required:, ..)) -> {
dict.to_list(properties)
|> list.any(fn(entry) {
let #(prop_name, prop_ref) = entry
!list.contains(required, prop_name)
|| schema_ref_is_nullable(prop_ref, ctx)
})
}
Inline(AllOfSchema(schemas:, ..)) ->
list.any(schemas, fn(s) { schema_has_optional_fields(s, ctx) })
Reference(..) ->
case context.resolve_schema_ref(schema_ref, ctx) {
Ok(schema_obj) -> schema_has_optional_fields(Inline(schema_obj), ctx)
// nolint: thrown_away_error -- unresolved refs are treated as not having optional fields; the resolver reports the ref error separately
Error(_) -> False
}
_ -> False
}
}
/// Issue #309: detect a property that is fully determined by the
/// schema — an inline string-enum with exactly one allowed value that
/// the parent schema lists as required. Such a property is a constant
/// marker on the wire (e.g. a `kind: type: string, enum: [text]`
/// discriminator on a oneOf variant): the only legal value is known
/// at codegen time, so the generated record drops the field, the
/// encoder emits the constant, and the decoder validates it.
///
/// Returns `Some(value)` for an elidable constant property; `None`
/// otherwise (including `$ref`s, multi-value enums, optional
/// properties, and non-string schemas — those keep their current
/// record-field shape).
pub fn constant_property_value(
prop_ref: SchemaRef,
prop_name: String,
required: List(String),
) -> Option(String) {
case list.contains(required, prop_name), prop_ref {
True, Inline(StringSchema(enum_values: [single_value], ..)) ->
Some(single_value)
_, _ -> None
}
}
/// Check if a SchemaRef is nullable, resolving $ref if needed.
pub fn schema_ref_is_nullable(ref: SchemaRef, ctx: Context) -> Bool {
case context.schema_metadata(ref, ctx) {
Some(metadata) -> metadata.nullable
None -> False
}
}
/// Check if a SchemaRef has readOnly metadata, resolving $ref if needed.
pub fn schema_ref_is_read_only(ref: SchemaRef, ctx: Context) -> Bool {
case context.schema_metadata(ref, ctx) {
Some(metadata) -> metadata.read_only
None -> False
}
}
/// Check if a SchemaRef has writeOnly metadata, resolving $ref if needed.
pub fn schema_ref_is_write_only(ref: SchemaRef, ctx: Context) -> Bool {
case context.schema_metadata(ref, ctx) {
Some(metadata) -> metadata.write_only
None -> False
}
}
/// Filter readOnly properties from an ObjectSchema for request body context.
/// Returns a new schema with readOnly properties removed.
pub fn filter_read_only_properties(
schema_obj: SchemaObject,
ctx: Context,
) -> SchemaObject {
case schema_obj {
ObjectSchema(
metadata:,
properties:,
required:,
additional_properties:,
min_properties:,
max_properties:,
) -> {
let filtered_props =
dict.filter(properties, fn(_name, prop_ref) {
!schema_ref_is_read_only(prop_ref, ctx)
})
let filtered_required =
list.filter(required, fn(name) {
case dict.get(filtered_props, name) {
Ok(_) -> True
// nolint: thrown_away_error -- dict miss means the required field was filtered out; no error value to preserve
Error(_) -> False
}
})
ObjectSchema(
metadata:,
properties: filtered_props,
required: filtered_required,
additional_properties:,
min_properties:,
max_properties:,
)
}
_ -> schema_obj
}
}
/// Filter writeOnly properties from an ObjectSchema for response body context.
/// Returns a new schema with writeOnly properties removed.
pub fn filter_write_only_properties(
schema_obj: SchemaObject,
ctx: Context,
) -> SchemaObject {
case schema_obj {
ObjectSchema(
metadata:,
properties:,
required:,
additional_properties:,
min_properties:,
max_properties:,
) -> {
let filtered_props =
dict.filter(properties, fn(_name, prop_ref) {
!schema_ref_is_write_only(prop_ref, ctx)
})
let filtered_required =
list.filter(required, fn(name) {
case dict.get(filtered_props, name) {
Ok(_) -> True
// nolint: thrown_away_error -- dict miss means the required field was filtered out; no error value to preserve
Error(_) -> False
}
})
ObjectSchema(
metadata:,
properties: filtered_props,
required: filtered_required,
additional_properties:,
min_properties:,
max_properties:,
)
}
_ -> schema_obj
}
}