Current section

Files

Jump to
oas src oas.gleam
Raw

src/oas.gleam

import gleam/dict.{type Dict}
import gleam/dynamic
import gleam/http
import gleam/int
import gleam/io
import gleam/list
import gleam/option.{type Option, None, Some}
import gleam/result.{try}
import gleam/string
fn default_field(key, decoder, default) {
optional_field(key, decoder)
|> with_default(default)
}
fn optional_field(key, decoder) {
fn(raw) {
let decoder = dynamic.optional_field(key, dynamic.optional(decoder))
use decoded <- try(decoder(raw))
case decoded {
Some(Some(value)) -> Some(value)
_ -> None
}
|> Ok
}
}
fn with_default(decoder, value) {
fn(raw) {
use decoded <- try(decoder(raw))
Ok(option.unwrap(decoded, value))
}
}
/// This is the root object of the OpenAPI document.
pub type Document {
Document(
openapi: String,
info: Info,
json_schema_dialect: Option(String),
servers: List(Server),
paths: Dict(String, PathItem),
// webhooks:
components: Components,
// security:
// tags:
// externalDocs:
)
}
pub fn decoder(top) {
dynamic.decode6(
Document,
dynamic.field("openapi", dynamic.string),
dynamic.field("info", info_decoder),
optional_field("jsonSchemaDialect", dynamic.string),
default_field("servers", dynamic.list(server_decoder), []),
default_field("paths", paths_decoder, dict.new()),
default_field(
"components",
components_decoder,
Components(dict.new(), dict.new(), dict.new(), dict.new()),
),
)(top)
}
pub type Ref(t) {
Ref(ref: String)
Inline(value: t)
}
fn ref_decoder(of: dynamic.Decoder(t)) -> dynamic.Decoder(Ref(t)) {
dynamic.any([
dynamic.decode1(Ref, dynamic.field("$ref", dynamic.string)),
dynamic.decode1(Inline, of),
])
}
/// The object provides metadata about the API. The metadata MAY be used by the clients if needed, and MAY be presented in editing or documentation generation tools for convenience.
pub type Info {
Info(
title: String,
summary: Option(String),
description: Option(String),
terms_of_service: Option(String),
contact: Option(Contact),
license: Option(Licence),
version: String,
)
}
fn info_decoder(raw) {
dynamic.decode7(
Info,
dynamic.field("title", dynamic.string),
optional_field("summary", dynamic.string),
optional_field("description", dynamic.string),
optional_field("termsOfService", dynamic.string),
optional_field("contact", contact_decoder),
optional_field("license", license_decoder),
dynamic.field("version", dynamic.string),
)(raw)
}
pub type Contact {
Contact(name: Option(String), url: Option(String), email: Option(String))
}
fn contact_decoder(raw) {
dynamic.decode3(
Contact,
optional_field("name", dynamic.string),
optional_field("url", dynamic.string),
optional_field("email", dynamic.string),
)(raw)
}
pub type Licence {
Licence(name: String, identifier: Option(String), url: Option(String))
}
fn license_decoder(raw) {
dynamic.decode3(
Licence,
dynamic.field("name", dynamic.string),
optional_field("identifier", dynamic.string),
optional_field("url", dynamic.string),
)(raw)
}
pub type Server {
Server(
url: String,
description: Option(String),
variables: Dict(String, ServerVariable),
)
}
fn server_decoder(raw) {
dynamic.decode3(
Server,
dynamic.field("url", dynamic.string),
optional_field("description", dynamic.string),
default_field(
"variables",
dynamic.dict(dynamic.string, server_variable_decoder),
dict.new(),
),
)(raw)
}
pub type ServerVariable {
ServerVariable(
enum: List(String),
default: String,
description: Option(String),
)
}
fn server_variable_decoder(raw) {
dynamic.decode3(
ServerVariable,
dynamic.field("enum", dynamic.list(dynamic.string)),
dynamic.field("default", dynamic.string),
optional_field("description", dynamic.string),
)(raw)
}
/// Describes the operations available on a single path.
pub type PathItem {
PathItem(
summary: Option(String),
description: Option(String),
parameters: List(Ref(Parameter)),
operations: List(#(http.Method, Operation)),
)
}
fn paths_decoder(raw) {
dynamic.dict(dynamic.string, path_decoder)(raw)
}
fn path_decoder(raw) {
dynamic.decode4(
PathItem,
optional_field("summary", dynamic.string),
optional_field("description", dynamic.string),
default_field(
"parameters",
dynamic.list(ref_decoder(parameter_decoder)),
[],
),
fn(raw) {
use maybe_operations <- try(
[
#("get", http.Get),
#("put", http.Put),
#("post", http.Post),
#("delete", http.Delete),
#("options", http.Options),
#("head", http.Head),
#("patch", http.Patch),
#("trace", http.Trace),
]
|> list.map(fn(lookup) {
let #(key, method) = lookup
use maybe <- try(dynamic.optional_field(key, operation_decoder)(raw))
Ok(#(method, maybe))
})
|> result.all(),
)
list.filter_map(maybe_operations, fn(maybe) {
let #(method, maybe) = maybe
case maybe {
option.Some(value) -> Ok(#(method, value))
option.None -> Error(Nil)
}
})
|> Ok
},
)(raw)
}
pub type Components {
Components(
schemas: Dict(String, Schema),
responses: Dict(String, Ref(Response)),
parameters: Dict(String, Ref(Parameter)),
request_bodies: Dict(String, Ref(RequestBody)),
)
}
fn components_decoder(raw) {
dynamic.decode4(
Components,
default_field(
"schemas",
dynamic.dict(dynamic.string, schema_decoder),
dict.new(),
),
default_field(
"responses",
dynamic.dict(dynamic.string, ref_decoder(response_decoder)),
dict.new(),
),
default_field(
"parameters",
dynamic.dict(dynamic.string, ref_decoder(parameter_decoder)),
dict.new(),
),
default_field(
"requestBodies",
dynamic.dict(dynamic.string, ref_decoder(request_body_decoder)),
dict.new(),
),
)(raw)
}
pub type Parameter {
QueryParameter(
name: String,
description: Option(String),
required: Bool,
schema: Ref(Schema),
)
PathParameter(name: String, schema: Ref(Schema))
HeaderParameter(
name: String,
description: Option(String),
required: Bool,
schema: Ref(Schema),
)
CookieParameter(
name: String,
description: Option(String),
required: Bool,
schema: Ref(Schema),
)
}
fn parameter_decoder(raw) {
dynamic.any([
dynamic.field("in", fn(field) {
use in <- try(dynamic.string(field))
case in {
"query" -> {
dynamic.decode4(
QueryParameter,
dynamic.field("name", dynamic.string),
dynamic.optional_field("description", dynamic.string),
dynamic.optional_field("required", dynamic.bool)
|> with_default(False),
dynamic.field("schema", ref_decoder(schema_decoder)),
)(raw)
}
"header" -> {
dynamic.decode4(
HeaderParameter,
dynamic.field("name", dynamic.string),
dynamic.optional_field("description", dynamic.string),
dynamic.optional_field("required", dynamic.bool)
|> with_default(False),
dynamic.field("schema", ref_decoder(schema_decoder)),
)(raw)
}
"path" ->
dynamic.decode2(
PathParameter,
dynamic.field("name", dynamic.string),
dynamic.field("schema", ref_decoder(schema_decoder)),
)(raw)
"cookie" ->
dynamic.decode4(
HeaderParameter,
dynamic.field("name", dynamic.string),
dynamic.optional_field("description", dynamic.string),
dynamic.optional_field("required", dynamic.bool)
|> with_default(False),
dynamic.field("schema", ref_decoder(schema_decoder)),
)(raw)
_ -> Error([dynamic.DecodeError("valid in field", in, [])])
}
}),
])(raw)
}
pub type Operation {
Operation(
tags: List(String),
summary: Option(String),
description: Option(String),
operation_id: String,
parameters: List(Ref(Parameter)),
request_body: Option(Ref(RequestBody)),
responses: Dict(Status, Ref(Response)),
)
}
fn operation_decoder(raw) {
dynamic.decode7(
Operation,
default_field("tags", dynamic.list(dynamic.string), []),
optional_field("summary", dynamic.string),
optional_field("description", dynamic.string),
dynamic.field("operationId", dynamic.string),
default_field(
"parameters",
dynamic.list(ref_decoder(parameter_decoder)),
[],
),
optional_field("requestBody", ref_decoder(request_body_decoder)),
dynamic.field(
"responses",
dynamic.dict(status_decoder, ref_decoder(response_decoder)),
),
)(raw)
}
pub type RequestBody {
RequestBody(
description: Option(String),
content: Dict(String, MediaType),
required: Bool,
)
}
fn request_body_decoder(raw) {
dynamic.decode3(
RequestBody,
dynamic.optional_field("description", dynamic.string),
dynamic.field("content", content_decoder),
dynamic.optional_field("required", dynamic.bool)
|> with_default(False),
)(raw)
}
fn content_decoder(raw) {
dynamic.dict(dynamic.string, media_type_decoder)(raw)
}
pub type Status {
Default
Status(Int)
}
fn status_decoder(raw) {
use key <- try(dynamic.string(raw))
case key {
"default" -> Ok(Default)
key ->
case int.parse(key) {
Ok(i) -> Ok(Status(i))
Error(Nil) -> Error([dynamic.DecodeError("integer", key, [])])
}
}
}
pub type Response {
Response(
description: Option(String),
headers: Dict(String, Ref(Header)),
content: Dict(String, MediaType),
)
}
fn response_decoder(raw) {
dynamic.decode3(
Response,
optional_field("description", dynamic.string),
default_field(
"headers",
dynamic.dict(dynamic.string, ref_decoder(decode_header)),
dict.new(),
),
default_field("content", content_decoder, dict.new()),
)(raw)
}
pub type Header {
Header(description: Option(String), required: Bool, schema: Schema)
}
fn decode_header(raw) {
dynamic.decode3(
Header,
dynamic.optional_field("description", dynamic.string),
dynamic.optional_field("required", dynamic.bool)
|> with_default(False),
dynamic.field("schema", schema_decoder),
)(raw)
}
pub type MediaType {
MediaType(schema: Ref(Schema))
}
fn media_type_decoder(raw) {
dynamic.decode1(
MediaType,
dynamic.field("schema", ref_decoder(schema_decoder)),
)(raw)
}
pub type Schema {
Boolean
Integer
Number
String
Array(Ref(Schema))
Object(properties: Dict(String, Ref(Schema)))
AllOf(List(Ref(Dict(String, Ref(Schema)))))
AnyOf(List(Ref(Schema)))
OneOf(List(Ref(Schema)))
}
fn schema_decoder(raw) {
dynamic.any([
dynamic.field("type", fn(field) {
use type_ <- try(dynamic.string(field))
case type_ {
"boolean" -> Ok(Boolean)
"integer" -> Ok(Integer)
"number" -> Ok(Number)
"string" -> Ok(String)
"array" -> {
use items <- try(dynamic.field("items", ref_decoder(schema_decoder))(
raw,
))
Ok(Array(items))
}
"object" -> decode_object(raw)
_ -> {
Error([dynamic.DecodeError("json type", type_, [])])
}
}
}),
dynamic.field(
"allOf",
dynamic.decode1(AllOf, dynamic.list(ref_decoder(decode_properties))),
),
dynamic.field(
"anyOf",
dynamic.decode1(AnyOf, dynamic.list(ref_decoder(schema_decoder))),
),
dynamic.field(
"oneOf",
dynamic.decode1(OneOf, dynamic.list(ref_decoder(schema_decoder))),
),
])(raw)
}
fn decode_object(raw) {
dynamic.decode1(Object, decode_properties)(raw)
}
fn decode_properties(raw) {
default_field(
"properties",
dynamic.dict(dynamic.string, ref_decoder(schema_decoder)),
dict.new(),
)(raw)
}
// --------------------------------------------------------------------
// UTILS
pub type Segment {
FixedSegment(content: String)
MatchSegment(name: String, schema: Schema)
}
fn get_parameter(parameters, key) {
list.find(parameters, fn(p) {
case p {
PathParameter(name: name, ..) if name == key -> True
_ -> False
}
})
}
pub fn query_parameters(parameters) {
list.filter_map(parameters, fn(p) {
case p {
QueryParameter(name: n, required: r, schema: s, ..) -> Ok(#(n, r, s))
_ -> Error(Nil)
}
})
}
pub fn gather_match(pattern, parameters, components: Components) {
case pattern {
"/" <> pattern -> {
string.split(pattern, "/")
|> list.try_map(fn(segment) {
case segment {
"{" <> rest -> {
let label = string.drop_right(rest, 1)
case get_parameter(parameters, label) {
Ok(PathParameter(schema: schema, ..)) -> {
let schema = fetch_schema(schema, components.schemas)
Ok(MatchSegment(label, schema))
}
_ -> Error(Nil)
}
}
name -> Ok(FixedSegment(name))
}
})
}
_ -> Error(Nil)
}
}
pub fn fetch_schema(ref, schemas) {
case ref {
Inline(schema) -> schema
Ref("#/components/schemas/" <> name) -> {
let assert Ok(schema) = dict.get(schemas, name)
schema
}
Ref(x) -> {
io.debug(x)
panic as "not valid ref"
}
}
}
pub fn fetch_parameter(ref, parameters) {
case ref {
Inline(parameter) -> parameter
Ref("#/components/parameters/" <> name) -> {
let assert Ok(Inline(parameter)) = dict.get(parameters, name)
parameter
}
Ref(x) -> {
io.debug(x)
panic as "not valid ref"
}
}
}
pub fn fetch_request_body(ref, request_bodies) {
case ref {
Inline(request_body) -> request_body
Ref("#/components/requestBodies/" <> name) -> {
let assert Ok(Inline(request_body)) = dict.get(request_bodies, name)
request_body
}
Ref(x) -> {
io.debug(x)
panic as "not valid ref"
}
}
}
pub fn fetch_response(ref, responses) {
case ref {
Inline(response) -> response
Ref("#/components/responses/" <> name) -> {
let assert Ok(Inline(response)) = dict.get(responses, name)
response
}
Ref(x) -> {
io.debug(x)
panic as "not valid ref"
}
}
}