Current section

Files

Jump to
oaspec src oaspec internal openapi schema.gleam
Raw

src/oaspec/internal/openapi/schema.gleam

import gleam/dict.{type Dict}
import gleam/list
import gleam/option.{type Option}
import gleam/result
import gleam/string
import oaspec/internal/openapi/value.{type JsonValue}
/// Origin of a schema — user-authored or hoisted during codegen.
/// Hoisted variants carry the call-site context so tooling can explain
/// where synthetic component schemas came from.
pub type OriginKind {
UserAuthored
HoistedProperty(parent: String, property: String)
HoistedArrayItem(parent: String)
HoistedOneOfVariant(parent: String, index: Int)
HoistedAnyOfVariant(parent: String, index: Int)
HoistedAllOfPart(parent: String, index: Int)
HoistedRequestBody(operation_id: String)
HoistedResponse(operation_id: String, status: String)
HoistedParameter(operation_id: String, name: String)
HoistedAdditionalProperties(parent: String)
}
/// Shared metadata for all schema types.
/// Extracted from variants to avoid duplication and ensure composition
/// schemas (allOf/oneOf/anyOf) don't lose these fields.
pub type SchemaMetadata {
SchemaMetadata(
description: Option(String),
nullable: Bool,
deprecated: Bool,
title: Option(String),
read_only: Bool,
write_only: Bool,
default: Option(JsonValue),
example: Option(JsonValue),
const_value: Option(JsonValue),
raw_type: Option(List(String)),
unsupported_keywords: List(String),
internal: Bool,
provenance: OriginKind,
)
}
/// Create default metadata with no description, not nullable, not deprecated.
pub fn default_metadata() -> SchemaMetadata {
SchemaMetadata(
description: option.None,
nullable: False,
deprecated: False,
title: option.None,
read_only: False,
write_only: False,
default: option.None,
example: option.None,
const_value: option.None,
raw_type: option.None,
unsupported_keywords: [],
internal: False,
provenance: UserAuthored,
)
}
/// How additionalProperties is modeled in the AST.
pub type AdditionalProperties {
/// additionalProperties: false (extra keys rejected)
Forbidden
/// additionalProperties: true (accept any JSON value, surfaced in
/// generated types as `Dict(String, Dynamic)`)
Untyped
/// additionalProperties: { schema } (typed dict in generated types)
Typed(SchemaRef)
/// additionalProperties key absent from the spec. Per JSON Schema
/// any extra keys are still permitted at runtime, but the generated
/// record does not surface them — users opt in by writing `true` or
/// a schema explicitly.
Unspecified
}
/// Represents a JSON Schema object within OpenAPI 3.x.
/// This is the core building block for all type generation.
/// All variants carry shared `metadata` for description, nullable, deprecated.
pub type SchemaObject {
StringSchema(
metadata: SchemaMetadata,
format: Option(String),
enum_values: List(String),
min_length: Option(Int),
max_length: Option(Int),
pattern: Option(String),
)
IntegerSchema(
metadata: SchemaMetadata,
format: Option(String),
minimum: Option(Int),
maximum: Option(Int),
exclusive_minimum: Option(Int),
exclusive_maximum: Option(Int),
multiple_of: Option(Int),
)
NumberSchema(
metadata: SchemaMetadata,
format: Option(String),
minimum: Option(Float),
maximum: Option(Float),
exclusive_minimum: Option(Float),
exclusive_maximum: Option(Float),
multiple_of: Option(Float),
)
BooleanSchema(metadata: SchemaMetadata)
ArraySchema(
metadata: SchemaMetadata,
items: SchemaRef,
min_items: Option(Int),
max_items: Option(Int),
unique_items: Bool,
)
ObjectSchema(
metadata: SchemaMetadata,
properties: Dict(String, SchemaRef),
required: List(String),
additional_properties: AdditionalProperties,
min_properties: Option(Int),
max_properties: Option(Int),
)
AllOfSchema(metadata: SchemaMetadata, schemas: List(SchemaRef))
OneOfSchema(
metadata: SchemaMetadata,
schemas: List(SchemaRef),
discriminator: Option(Discriminator),
)
AnyOfSchema(
metadata: SchemaMetadata,
schemas: List(SchemaRef),
discriminator: Option(Discriminator),
)
}
/// A reference to a schema, either inline or via $ref.
/// Reference carries both the full $ref string and the pre-extracted name
/// (last segment) to eliminate repeated string splitting in codegen.
pub type SchemaRef {
Inline(SchemaObject)
Reference(ref: String, name: String)
}
/// Create a Reference from a $ref string, auto-extracting the name.
pub fn make_reference(ref: String) -> SchemaRef {
let name = ref_to_schema_name(ref)
Reference(ref:, name:)
}
/// Extract the schema name from a $ref string (last path segment).
/// Example: "#/components/schemas/User" -> "User"
fn ref_to_schema_name(ref: String) -> String {
case string.split(ref, "/") {
[] -> "Unknown"
segments ->
list.last(segments)
|> result.unwrap("Unknown")
}
}
/// OpenAPI discriminator for oneOf/anyOf.
pub type Discriminator {
Discriminator(property_name: String, mapping: Dict(String, String))
}
/// Check if a schema is nullable.
pub fn is_nullable(schema: SchemaObject) -> Bool {
get_metadata(schema).nullable
}
/// Extract the shared metadata from any schema variant.
pub fn get_metadata(schema: SchemaObject) -> SchemaMetadata {
case schema {
StringSchema(metadata:, ..) -> metadata
IntegerSchema(metadata:, ..) -> metadata
NumberSchema(metadata:, ..) -> metadata
BooleanSchema(metadata:) -> metadata
ArraySchema(metadata:, ..) -> metadata
ObjectSchema(metadata:, ..) -> metadata
AllOfSchema(metadata:, ..) -> metadata
OneOfSchema(metadata:, ..) -> metadata
AnyOfSchema(metadata:, ..) -> metadata
}
}
/// Mark a schema as internal (not part of the public generated API).
pub fn set_internal(schema: SchemaObject) -> SchemaObject {
let meta = get_metadata(schema)
let meta = SchemaMetadata(..meta, internal: True)
set_metadata(schema, meta)
}
/// Stamp the origin of a hoisted schema onto its metadata so downstream
/// consumers (diagnostics, tooling) can distinguish user-authored schemas
/// from synthetic ones created during the hoist pass.
pub fn set_provenance(schema: SchemaObject, origin: OriginKind) -> SchemaObject {
let meta = get_metadata(schema)
let meta = SchemaMetadata(..meta, provenance: origin)
set_metadata(schema, meta)
}
/// Read the provenance of a schema. Unhoisted schemas return `UserAuthored`.
pub fn get_provenance(schema: SchemaObject) -> OriginKind {
get_metadata(schema).provenance
}
/// Replace the metadata on a schema object.
pub fn set_metadata(schema: SchemaObject, meta: SchemaMetadata) -> SchemaObject {
case schema {
StringSchema(format:, enum_values:, min_length:, max_length:, pattern:, ..) ->
StringSchema(
metadata: meta,
format:,
enum_values:,
min_length:,
max_length:,
pattern:,
)
IntegerSchema(
format:,
minimum:,
maximum:,
exclusive_minimum:,
exclusive_maximum:,
multiple_of:,
..,
) ->
IntegerSchema(
metadata: meta,
format:,
minimum:,
maximum:,
exclusive_minimum:,
exclusive_maximum:,
multiple_of:,
)
NumberSchema(
format:,
minimum:,
maximum:,
exclusive_minimum:,
exclusive_maximum:,
multiple_of:,
..,
) ->
NumberSchema(
metadata: meta,
format:,
minimum:,
maximum:,
exclusive_minimum:,
exclusive_maximum:,
multiple_of:,
)
BooleanSchema(..) -> BooleanSchema(metadata: meta)
ArraySchema(items:, min_items:, max_items:, unique_items:, ..) ->
ArraySchema(metadata: meta, items:, min_items:, max_items:, unique_items:)
ObjectSchema(
properties:,
required:,
additional_properties:,
min_properties:,
max_properties:,
..,
) ->
ObjectSchema(
metadata: meta,
properties:,
required:,
additional_properties:,
min_properties:,
max_properties:,
)
AllOfSchema(schemas:, ..) -> AllOfSchema(metadata: meta, schemas:)
OneOfSchema(schemas:, discriminator:, ..) ->
OneOfSchema(metadata: meta, schemas:, discriminator:)
AnyOfSchema(schemas:, discriminator:, ..) ->
AnyOfSchema(metadata: meta, schemas:, discriminator:)
}
}