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" } } }