Current section

Files

Jump to
oaspec src oaspec openapi resolver.gleam
Raw

src/oaspec/openapi/resolver.gleam

import gleam/dict
import gleam/list
import gleam/option.{None, Some}
import gleam/result
import gleam/set.{type Set}
import gleam/string
import oaspec/openapi/schema.{
type SchemaObject, type SchemaRef, AllOfSchema, AnyOfSchema, ArraySchema,
Forbidden, Inline, ObjectSchema, OneOfSchema, Reference, Typed, Untyped,
}
import oaspec/openapi/spec.{type OpenApiSpec, type Resolved}
/// Errors during reference resolution.
pub type ResolveError {
UnresolvedRef(ref: String)
CircularRef(ref: String)
}
/// Resolve a $ref string to its schema name.
/// Example: "#/components/schemas/User" -> "User"
pub fn ref_to_name(ref: String) -> String {
ref
|> string.split("/")
|> list.last
|> result.unwrap("Unknown")
}
/// Resolve a SchemaRef to a SchemaObject, looking up $ref in components.
/// Tracks seen refs to detect circular references.
pub fn resolve_schema_ref(
schema_ref: SchemaRef,
spec: OpenApiSpec(Resolved),
) -> Result(SchemaObject, ResolveError) {
resolve_schema_ref_with_seen(schema_ref, spec, set.new())
}
/// Internal resolver that tracks visited refs for cycle detection.
fn resolve_schema_ref_with_seen(
schema_ref: SchemaRef,
spec: OpenApiSpec(Resolved),
seen: Set(String),
) -> Result(SchemaObject, ResolveError) {
case schema_ref {
Inline(schema) -> Ok(schema)
Reference(ref:, name:) -> {
case set.contains(seen, ref) {
True -> Error(CircularRef(ref:))
False -> {
let new_seen = set.insert(seen, ref)
case spec.components {
Some(components) -> {
case dict.get(components.schemas, name) {
Ok(Inline(schema)) -> Ok(schema)
Ok(Reference(ref: inner_ref, name: inner_name)) ->
resolve_schema_ref_with_seen(
Reference(ref: inner_ref, name: inner_name),
spec,
new_seen,
)
Error(_) -> Error(UnresolvedRef(ref:))
}
}
None -> Error(UnresolvedRef(ref:))
}
}
}
}
}
}
/// Resolve all $ref in a schema object's nested schemas (one level).
pub fn resolve_schema_refs_in_schema(
schema: SchemaObject,
spec: OpenApiSpec(Resolved),
) -> SchemaObject {
case schema {
ObjectSchema(properties:, additional_properties:, ..) as obj -> {
let resolved_props =
dict.map_values(properties, fn(_k, v) { resolve_one_ref(v, spec) })
let resolved_ap = case additional_properties {
Typed(ap) -> Typed(resolve_one_ref(ap, spec))
Untyped -> Untyped
Forbidden -> Forbidden
}
ObjectSchema(
..obj,
properties: resolved_props,
additional_properties: resolved_ap,
)
}
ArraySchema(items:, ..) as arr ->
ArraySchema(..arr, items: resolve_one_ref(items, spec))
AllOfSchema(metadata:, schemas:) -> {
let resolved = list_map_ref(schemas, spec)
AllOfSchema(metadata:, schemas: resolved)
}
OneOfSchema(metadata:, schemas:, discriminator:) -> {
let resolved = list_map_ref(schemas, spec)
OneOfSchema(metadata:, schemas: resolved, discriminator:)
}
AnyOfSchema(metadata:, schemas:, discriminator:) -> {
let resolved = list_map_ref(schemas, spec)
AnyOfSchema(metadata:, schemas: resolved, discriminator:)
}
other -> other
}
}
/// Try to resolve a single ref, keeping it as-is if resolution fails.
fn resolve_one_ref(
schema_ref: SchemaRef,
spec: OpenApiSpec(Resolved),
) -> SchemaRef {
case schema_ref {
Reference(..) ->
case resolve_schema_ref(schema_ref, spec) {
Ok(schema) -> Inline(schema)
Error(_) -> schema_ref
}
inline -> inline
}
}
/// Map a list of schema refs, resolving each.
fn list_map_ref(
refs: List(SchemaRef),
spec: OpenApiSpec(Resolved),
) -> List(SchemaRef) {
list.map(refs, fn(r) { resolve_one_ref(r, spec) })
}