Current section

Files

Jump to
oas_generator src oas generator schema.gleam
Raw

src/oas/generator/schema.gleam

//// This module builds ASTs from schemas.
//// It does not do any file generation
import glance
import gleam/dict
import gleam/int
import gleam/list
import gleam/option.{None, Some}
import oas
import oas/generator/ast
import oas/generator/lift
import oas/generator/misc
/// From a dictionary of schemas generate all custom types, type aliases, encoders and decoders.
pub fn generate(schemas) {
let module = misc.Schema
let #(internal, named) =
list.map_fold(schemas |> dict.to_list, [], fn(acc, entry) {
let #(name, schema) = entry
let #(top, _nullable, acc) = lift.do_lift(oas.Inline(schema), acc)
#(acc, #(name, top))
})
let named =
list.append(
named,
internal
|> list.reverse
|> list.index_map(fn(fields, index) {
#("internal_" <> int.to_string(index), lift.Compound(fields))
}),
)
list.fold(named, #([], [], []), fn(acc, entry) {
let #(name, top) = entry
let #(custom_types, type_aliases, fns) = acc
let fns =
list.append(fns, [
to_encode_fn(#(name, top), module),
to_decode_fn(#(name, top), module),
])
let name = ast.name_for_gleam_type(name)
let #(custom_types, type_aliases) = case top {
lift.Named(name) -> {
let type_ = to_type(lift.Named(name), module)
#(custom_types, [alias(name, type_), ..type_aliases])
}
lift.Primitive(primitive) -> {
let type_ = to_type(lift.Primitive(primitive), module)
#(custom_types, [alias(name, type_), ..type_aliases])
}
lift.Array(items) -> {
let type_ = to_type(lift.Array(items), module)
#(custom_types, [alias(name, type_), ..type_aliases])
}
lift.Tuple(items) -> {
let type_ = to_type(lift.Tuple(items), module)
#(custom_types, [alias(name, type_), ..type_aliases])
}
lift.Compound(lift.Fields(properties, additional, required)) -> {
let type_ = custom_type(name, properties, additional, required, module)
#([type_, ..custom_types], type_aliases)
}
lift.Dictionary(values) -> {
let type_ = to_type(lift.Dictionary(values), module)
#(custom_types, [alias(name, type_), ..type_aliases])
}
lift.Unsupported -> {
let type_ = to_type(lift.Unsupported, module)
#(custom_types, [alias(name, type_), ..type_aliases])
}
}
#(custom_types, type_aliases, fns)
})
}
/// Generate a custom type from an object type.
/// TODO make private
pub fn custom_type(name, properties, additional, required, module) {
let fields =
list.map(properties, fn(property) {
let #(key, #(schema, nullable)) = property
let type_ = to_type(schema, module)
glance.LabelledVariantField(
case !list.contains(required, key) || nullable {
False -> type_
True -> glance.NamedType("Option", None, [type_])
},
ast.name_for_gleam_field_or_var(key),
)
})
let fields = case additional {
Some(schema) -> {
let key = "additionalProperties"
let type_ = to_type(schema, module)
let extra =
glance.LabelledVariantField(
glance.NamedType("Dict", Some("dict"), [
glance.NamedType("String", None, []),
type_,
]),
ast.name_for_gleam_field_or_var(key),
)
list.append(fields, [extra])
}
None -> fields
}
let name = ast.name_for_gleam_type(name)
glance.CustomType(name, glance.Public, False, [], [
glance.Variant(name, fields),
])
}
/// Generate type aliases from OpenAPI lifted schemas.
/// All objects are converted to names
fn to_type(lifted, module) {
case lifted {
lift.Named("#/components/schemas/" <> inner) -> {
let mod = case module {
misc.Schema -> None
_ -> Some("schema")
}
glance.NamedType(ast.name_for_gleam_type(inner), mod, [])
}
lift.Named(ref) ->
panic as { "not referencing schema component ref: " <> ref }
lift.Primitive(primitive) -> {
case primitive {
lift.Boolean -> glance.NamedType("Bool", None, [])
lift.Integer -> glance.NamedType("Int", None, [])
lift.Number -> glance.NamedType("Float", None, [])
lift.String -> glance.NamedType("String", None, [])
lift.Null -> glance.NamedType("Null", None, [])
lift.Always -> glance.NamedType("Json", Some("json"), [])
lift.Never -> glance.NamedType("Never", None, [])
}
}
lift.Array(items) ->
glance.NamedType("List", None, [to_type(items, module)])
lift.Tuple(items) -> glance.TupleType(list.map(items, to_type(_, module)))
lift.Compound(index) -> {
let type_ = "Internal" <> int.to_string(index)
glance.NamedType(type_, None, [])
}
lift.Dictionary(values) ->
glance.NamedType("Dict", Some("dict"), [
glance.NamedType("String", None, []),
to_type(values, module),
])
lift.Unsupported -> glance.NamedType("Dynamic", Some("dynamic"), [])
}
}
/// This handles top to encoder
/// TODO make this private
pub fn to_encode_fn(entry, module) {
let #(name, top) = entry
let type_ = ast.name_for_gleam_type(name)
let arg = glance.Variable("data")
let exp = case top {
lift.Named(n) ->
to_encoder(lift.Named(n), module)
|> glance.Call([glance.UnlabelledField(arg)])
lift.Primitive(lift.Null) -> ast.call0("json", "null")
lift.Primitive(lift.Always) -> arg
lift.Primitive(lift.Never) ->
glance.Panic(Some(glance.String("never value cannot be encoded")))
lift.Primitive(p) ->
to_encoder(lift.Primitive(p), module)
|> glance.Call([glance.UnlabelledField(arg)])
lift.Array(items) -> {
ast.call2("json", "array", arg, to_encoder(items, module))
}
lift.Tuple(items) -> encode_tuple_body(items, arg, module)
lift.Compound(lift.Fields(properties, additional, required)) -> {
fields_to_encode_body(properties, required, additional, module)
}
lift.Dictionary(values) ->
ast.call2("utils", "dict", arg, to_encoder(values, module))
lift.Unsupported -> arg
}
let ignored = case top {
lift.Primitive(lift.Null) | lift.Primitive(lift.Never) -> True
lift.Compound(lift.Fields(properties, None, _)) -> list.is_empty(properties)
_ -> False
}
let assignment = case ignored {
True -> glance.Discarded("data")
False -> glance.Named("data")
}
glance.Function(
name: encode_fn(name),
publicity: glance.Public,
parameters: [
glance.FunctionParameter(
None,
assignment,
// could annotate with type returned from to custom type
Some(glance.NamedType(type_, None, [])),
),
],
return: None,
body: [glance.Expression(exp)],
location: glance.Span(0, 0),
)
}
pub fn fields_to_encode_body(properties, required, additional, module) {
ast.call1(
"utils",
"object",
glance.List(
list.map(properties, fn(property) {
let #(key, #(schema, nullable)) = property
let arg = ast.access("data", ast.name_for_gleam_field_or_var(key))
let cast = to_encoder(schema, module)
let value = case !list.contains(required, key) || nullable {
False -> glance.Call(cast, [glance.UnlabelledField(arg)])
True -> ast.call2("json", "nullable", arg, cast)
}
glance.Tuple([glance.String(key), value])
}),
case additional {
Some(values) ->
Some(ast.call1(
"dict",
"to_list",
ast.call2(
"dict",
"map_values",
ast.access("data", "additional_properties"),
glance.Fn(
[
glance.FnParameter(glance.Discarded("key"), None),
glance.FnParameter(glance.Named("value"), None),
],
None,
[
glance.Expression(
glance.Call(to_encoder(values, module), [
glance.UnlabelledField(glance.Variable("value")),
]),
),
],
),
),
))
None -> None
},
),
)
}
/// This handles a lifted encoder
pub fn to_encoder(lifted, module) {
case lifted {
lift.Named("#/components/schemas/" <> inner) -> {
let func = encode_fn(inner)
case module {
misc.Schema -> glance.Variable(func)
_ -> ast.access("schema", func)
}
}
lift.Named(..) -> panic as "unexpected ref"
lift.Primitive(lift.Boolean) -> ast.access("json", "bool")
lift.Primitive(lift.Integer) -> ast.access("json", "int")
lift.Primitive(lift.Number) -> ast.access("json", "float")
lift.Primitive(lift.String) -> ast.access("json", "string")
lift.Primitive(lift.Null) ->
glance.Fn(
[
glance.FnParameter(
glance.Discarded(""),
Some(glance.NamedType("Nil", None, [])),
),
],
None,
[glance.Expression(ast.call0("json", "null"))],
)
lift.Primitive(lift.Always) ->
glance.Fn([glance.FnParameter(glance.Named("data"), None)], None, [
glance.Expression(glance.Variable("data")),
])
lift.Primitive(lift.Never) ->
glance.Fn([glance.FnParameter(glance.Discarded("data"), None)], None, [
glance.Expression(
glance.Panic(Some(glance.String("never value cannot be encoded"))),
),
])
lift.Array(items) -> {
glance.FnCapture(None, ast.access("json", "array"), [], [
glance.UnlabelledField(to_encoder(items, module)),
])
}
lift.Tuple(items) ->
glance.Fn([glance.FnParameter(glance.Named("data"), None)], None, [
glance.Expression(encode_tuple_body(
items,
glance.Variable("data"),
module,
)),
])
lift.Compound(index) ->
glance.Variable(encode_fn("internal_" <> int.to_string(index)))
lift.Dictionary(values) ->
glance.FnCapture(None, ast.access("utils", "dict"), [], [
glance.UnlabelledField(to_encoder(values, module)),
])
lift.Unsupported ->
glance.Fn([glance.FnParameter(glance.Named("data"), None)], None, [
glance.Expression(glance.Variable("data")),
])
}
}
fn encode_tuple_body(items, arg, module) {
ast.call1(
"utils",
"merge",
glance.List(
list.index_map(items, fn(item, index) {
glance.Call(to_encoder(item, module), [
glance.UnlabelledField(glance.TupleIndex(arg, index)),
])
}),
None,
),
)
}
pub fn encode_fn(name) {
ast.name_for_gleam_field_or_var(name <> "_encode")
}
// TODO make this private
pub fn to_decode_fn(entry, module) {
let #(name, top) = entry
let body = gen_top_decoder_needs_name(name, top, module)
glance.Function(
name: decoder(name),
publicity: glance.Public,
parameters: [],
return: None,
body:,
location: glance.Span(0, 0),
)
}
// TODO make this private
pub fn gen_top_decoder_needs_name(name, top, module) {
case top {
lift.Named(n) -> [glance.Expression(to_decoder(lift.Named(n), module))]
lift.Primitive(p) -> [
glance.Expression(to_decoder(lift.Primitive(p), module)),
]
lift.Array(items) -> [
glance.Expression(to_decoder(lift.Array(items), module)),
]
lift.Tuple(items) ->
case to_decoder(lift.Tuple(items), module) {
glance.Block(statements) -> statements
other -> [glance.Expression(other)]
}
lift.Compound(lift.Fields(properties, additional, required)) -> {
let type_ = ast.name_for_gleam_type(name)
let zipped =
list.map(properties, fn(property) {
let #(key, #(schema, nullable)) = property
let is_optional = !list.contains(required, key) || nullable
let field_decoder = to_decoder(schema, module)
#(
glance.Use(
[glance.PatternVariable(ast.name_for_gleam_field_or_var(key))],
case is_optional {
False ->
ast.call2(
"decode",
"field",
glance.String(key),
field_decoder,
)
True ->
glance.Call(ast.access("decode", "optional_field"), [
glance.UnlabelledField(glance.String(key)),
glance.UnlabelledField(glance.Variable("None")),
glance.UnlabelledField(ast.call1(
"decode",
"optional",
field_decoder,
)),
])
},
),
glance.LabelledField(
ast.name_for_gleam_field_or_var(key),
glance.Variable(ast.name_for_gleam_field_or_var(key)),
),
)
})
let zipped = case additional {
Some(schema) -> {
let key = "additionalProperties"
list.append(zipped, [
#(
glance.Use(
[glance.PatternVariable(ast.name_for_gleam_field_or_var(key))],
ast.call2(
"utils",
"decode_additional",
glance.List(
list.map(properties, fn(p) {
let #(k, _) = p
glance.String(k)
}),
None,
),
to_decoder(schema, module),
),
),
glance.LabelledField(
ast.name_for_gleam_field_or_var(key),
glance.Variable(ast.name_for_gleam_field_or_var(key)),
),
),
])
}
None -> zipped
}
let #(fields, cons) = list.unzip(zipped)
let final =
glance.Expression(
ast.call1("decode", "success", case list.length(cons) {
0 -> glance.Variable(type_)
_ -> glance.Call(glance.Variable(type_), cons)
}),
)
list.append(fields, [final])
}
lift.Dictionary(values) -> [
glance.Expression(to_decoder(lift.Dictionary(values), module)),
]
lift.Unsupported -> [
glance.Expression(to_decoder(lift.Unsupported, module)),
]
}
}
// TODO make private
pub fn to_decoder(lifted, module) {
case lifted {
lift.Named("#/components/schemas/" <> name) -> {
let func = ast.name_for_gleam_field_or_var(name <> "_decoder")
case module {
misc.Schema -> glance.Call(glance.Variable(func), [])
_ -> ast.call0("schema", func)
}
}
lift.Named(ref) ->
panic as { "not referencing schema component ref: " <> ref }
lift.Primitive(primitive) ->
case primitive {
lift.Boolean -> ast.access("decode", "bool")
lift.Integer -> ast.access("decode", "int")
lift.Number -> ast.access("decode", "float")
lift.String -> ast.access("decode", "string")
lift.Null -> always_decode()
lift.Always -> ast.call0("utils", "dynamic_to_json")
lift.Never -> never_decode()
}
lift.Tuple(items) -> {
let uses =
list.index_map(items, fn(item, index) {
glance.Use(
[glance.PatternVariable("e" <> int.to_string(index))],
ast.call1("decode", "then", to_decoder(item, module)),
)
})
let vars =
list.index_map(items, fn(_, index) {
glance.Variable("e" <> int.to_string(index))
})
glance.Block(
list.append(uses, [
glance.Expression(ast.call1("decode", "success", glance.Tuple(vars))),
]),
)
}
lift.Array(items) -> ast.call1("decode", "list", to_decoder(items, module))
lift.Compound(index) -> {
let func = "internal_" <> int.to_string(index) <> "_decoder"
glance.Call(glance.Variable(func), [])
}
lift.Dictionary(values) ->
ast.call2(
"decode",
"dict",
ast.access("decode", "string"),
to_decoder(values, module),
)
lift.Unsupported -> dynamic_decode()
}
}
fn decoder(name) {
ast.name_for_gleam_field_or_var(name <> "_decoder")
}
/// Not used anywhere else
fn alias(to, type_) {
glance.TypeAlias(to, glance.Public, [], type_)
}
fn always_decode() {
ast.call2(
"decode",
"new_primitive_decoder",
glance.String("Nil"),
glance.Fn([glance.FnParameter(glance.Discarded(""), None)], None, [
glance.Expression(
glance.Call(glance.Variable("Ok"), [
glance.UnlabelledField(glance.Variable("Nil")),
]),
),
]),
)
}
fn never_decode() {
ast.call2(
"decode",
"new_primitive_decoder",
glance.String("Never"),
glance.Fn([glance.FnParameter(glance.Discarded(""), None)], None, [
glance.Expression(
glance.Call(glance.Variable("Error"), [
glance.UnlabelledField(glance.Variable("Nil")),
]),
),
]),
)
}
fn dynamic_decode() {
ast.call2(
"decode",
"new_primitive_decoder",
glance.String("Dynamic"),
glance.Fn([glance.FnParameter(glance.Named("raw"), None)], None, [
glance.Expression(
glance.Call(glance.Variable("Ok"), [
glance.UnlabelledField(glance.Variable("raw")),
]),
),
]),
)
}