Current section
Files
Jump to
Current section
Files
src/oas/generator.gleam
import glance
import glance_printer
import gleam/bit_array
import gleam/dict
import gleam/dynamic/decode
import gleam/http
import gleam/int
import gleam/io
import gleam/json
import gleam/list
import gleam/option.{None, Some}
import gleam/result.{try}
import gleam/string
import justin
import oas
import oas/generator/lift
import simplifile
import snag
pub type Module {
Schema
Operations
}
fn concat_path(match, root) {
let #(str, rev) =
list.fold(match, #(root, []), fn(acc, segment) {
let #(current, prev) = acc
case segment {
oas.FixedSegment(s) -> #(current <> "/" <> s, prev)
oas.MatchSegment(name, schema) -> {
case schema {
oas.String(..) -> {
let prev = [glance.String(current <> "/"), ..prev]
let prev = [
glance.Variable(name_for_gleam_field_or_var(name)),
..prev
]
#("", prev)
}
oas.Integer(..) -> {
let prev = [glance.String(current <> "/"), ..prev]
let prev = [
call1(
"int",
"to_string",
glance.Variable(name_for_gleam_field_or_var(name)),
),
..prev
]
#("", prev)
}
_ -> {
let prev = [glance.String(current <> "/"), ..prev]
let prev = [
glance.Variable(name_for_gleam_field_or_var(name)),
..prev
]
#("", prev)
}
}
}
}
})
let parts = case str {
"" -> rev
_ -> [glance.String(str), ..rev]
}
let assert Ok(a) =
list.reduce(parts, fn(post, pre) {
glance.BinaryOperator(glance.Concatenate, pre, post)
})
a
}
fn alias(to, type_) {
glance.TypeAlias(to, glance.Public, [], type_)
}
fn encode_fn(name) {
name_for_gleam_field_or_var(name <> "_encode")
}
fn noop1(message) {
glance.Fn([glance.FnParameter(glance.Discarded(""), None)], None, [
glance.Expression(glance.Panic(Some(glance.String(message)))),
])
}
fn to_encoder(lifted) {
case lifted {
lift.Named("#/components/schemas/" <> inner) ->
glance.Variable(encode_fn(inner))
lift.Named(..) -> panic as "unexpected ref"
lift.Primitive(lift.Boolean) -> access("json", "bool")
lift.Primitive(lift.Integer) -> access("json", "int")
lift.Primitive(lift.Number) -> access("json", "float")
lift.Primitive(lift.String) -> access("json", "string")
lift.Primitive(lift.Null) ->
glance.Fn(
[
glance.FnParameter(
glance.Discarded(""),
Some(glance.NamedType("Nil", None, [])),
),
],
None,
[glance.Expression(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) -> {
call2("json", "array", glance.Variable("_"), to_encoder(items))
}
lift.Tuple(items) ->
glance.Fn([glance.FnParameter(glance.Named("data"), None)], None, [
glance.Expression(encode_tuple_body(items, glance.Variable("data"))),
])
lift.Compound(index) ->
glance.Variable(encode_fn("internal_" <> int.to_string(index)))
lift.Dictionary(values) ->
call2("utils", "dict", glance.Variable("_"), to_encoder(values))
lift.Unsupported ->
glance.Fn([glance.FnParameter(glance.Named("data"), None)], None, [
glance.Expression(glance.Variable("data")),
])
}
}
fn schema_to_encoder(entry) {
let #(name, top) = entry
let type_ = name_for_gleam_type(name)
let arg = glance.Variable("data")
let exp = case top {
lift.Named(n) ->
to_encoder(lift.Named(n)) |> glance.Call([glance.UnlabelledField(arg)])
lift.Primitive(lift.Null) -> 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))
|> glance.Call([glance.UnlabelledField(arg)])
lift.Array(items) -> {
call2("json", "array", arg, to_encoder(items))
}
lift.Tuple(items) -> encode_tuple_body(items, arg)
lift.Compound(lift.Fields(properties, additional, required)) -> {
call1(
"json",
"object",
glance.List(
list.map(properties, fn(property) {
let #(key, #(schema, nullable)) = property
let arg = access("data", name_for_gleam_field_or_var(key))
let cast = to_encoder(schema)
let value = case !list.contains(required, key) || nullable {
False -> glance.Call(cast, [glance.UnlabelledField(arg)])
True -> call2("json", "nullable", arg, cast)
}
glance.Tuple([glance.String(key), value])
}),
case additional {
Some(values) ->
Some(call1(
"dict",
"to_list",
call2(
"dict",
"map_values",
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), [
glance.UnlabelledField(glance.Variable("value")),
]),
),
],
),
),
))
None -> None
},
),
)
}
lift.Dictionary(values) -> call2("utils", "dict", arg, to_encoder(values))
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),
)
}
fn encode_tuple_body(items, arg) {
call1(
"utils",
"merge",
glance.List(
list.index_map(items, fn(item, index) {
glance.Call(to_encoder(item), [
glance.UnlabelledField(glance.TupleIndex(arg, index)),
])
}),
None,
),
)
}
fn decoder(name) {
name_for_gleam_field_or_var(name <> "_decoder")
}
fn always_decode() {
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() {
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() {
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")),
]),
),
]),
)
}
fn schema_to_decode_fn(entry) {
let #(name, top) = entry
let body = gen_top_decoder_needs_name(name, top, Schema)
glance.Function(
name: decoder(name),
publicity: glance.Public,
parameters: [],
return: None,
body:,
location: glance.Span(0, 0),
)
}
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_ = 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(name_for_gleam_field_or_var(key))],
case is_optional {
False ->
call2("decode", "field", glance.String(key), field_decoder)
True ->
glance.Call(access("decode", "optional_field"), [
glance.UnlabelledField(glance.String(key)),
glance.UnlabelledField(glance.Variable("None")),
glance.UnlabelledField(call1(
"decode",
"optional",
field_decoder,
)),
])
},
),
glance.LabelledField(
name_for_gleam_field_or_var(key),
glance.Variable(name_for_gleam_field_or_var(key)),
),
)
})
let zipped = case additional {
Some(schema) -> {
let key = "additionalProperties"
list.append(zipped, [
#(
glance.Use(
[glance.PatternVariable(name_for_gleam_field_or_var(key))],
call2(
"utils",
"decode_additional",
glance.List(
list.map(properties, fn(p) {
let #(k, _) = p
glance.String(k)
}),
None,
),
to_decoder(schema, module),
),
),
glance.LabelledField(
name_for_gleam_field_or_var(key),
glance.Variable(name_for_gleam_field_or_var(key)),
),
),
])
}
None -> zipped
}
let #(fields, cons) = list.unzip(zipped)
let final =
glance.Expression(
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)),
]
}
}
fn gen_schema(schemas) {
let module = 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, [
schema_to_encoder(#(name, top)),
schema_to_decode_fn(#(name, top)),
])
let name = 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)
})
}
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_])
},
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_,
]),
name_for_gleam_field_or_var(key),
)
list.append(fields, [extra])
}
None -> fields
}
let name = name_for_gleam_type(name)
glance.CustomType(name, glance.Public, False, [], [
glance.Variant(name, fields),
])
}
fn to_type(lifted, module) {
case lifted {
lift.Named("#/components/schemas/" <> inner) -> {
let mod = case module {
Schema -> None
_ -> Some("schema")
}
glance.NamedType(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("Dynamic", Some("dynamic"), [])
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"), [])
}
}
fn access(object_or_mod, field) {
glance.FieldAccess(glance.Variable(object_or_mod), field)
}
fn call0(m, f) {
glance.Call(access(m, f), [])
}
fn call1(m, f, a) {
glance.Call(access(m, f), [glance.UnlabelledField(a)])
}
fn call2(m, f, a, b) {
glance.Call(access(m, f), [
glance.UnlabelledField(a),
glance.UnlabelledField(b),
])
}
fn pipe(a, b) {
glance.BinaryOperator(glance.Pipe, a, b)
}
fn query_to_parts(parameters, components: oas.Components) {
list.map(parameters, fn(p) {
let #(key, required, schema) = p
let schema = oas.fetch_schema(schema, components.schemas)
let key = name_for_gleam_field_or_var(key)
let arg = glance.FunctionParameter(Some(key), glance.Named(key), None)
let var = glance.Variable(key)
let mapper = case schema {
oas.Boolean(..) -> Some(access("bool", "to_string"))
oas.Integer(..) -> Some(access("int", "to_string"))
oas.Number(..) -> Some(access("float", "to_string"))
oas.String(..) -> None
_ -> Some(noop1("query parameter is not supported"))
}
let value = case required {
True ->
call1("option", "Some", case mapper {
Some(m) -> glance.Call(m, [glance.UnlabelledField(var)])
None -> var
})
False ->
case mapper {
Some(mapper) -> call2("option", "map", var, mapper)
None -> var
}
}
let tuple = glance.Tuple([glance.String(key), value])
#(arg, tuple)
})
|> list.unzip
}
fn gen_method(method) {
case method {
http.Get -> access("http", "Get")
http.Post -> access("http", "Post")
http.Head -> access("http", "Head")
http.Put -> access("http", "Put")
http.Delete -> access("http", "Delete")
http.Trace -> access("http", "Trace")
http.Connect -> access("http", "Connect")
http.Options -> access("http", "Options")
http.Patch -> access("http", "Patch")
http.Other(other) -> call1("http", "Other", glance.String(other))
}
}
fn let_(var, value) {
glance.Assignment(glance.Let, glance.PatternVariable(var), None, value)
}
fn path_args(match) {
list.filter_map(match, fn(segment) {
case segment {
oas.FixedSegment(_) -> Error(Nil)
oas.MatchSegment(name, _schema) ->
Ok(glance.FunctionParameter(
None,
glance.Named(name_for_gleam_field_or_var(name)),
None,
))
}
})
}
fn get_structure_json_media(content: dict.Dict(String, oas.MediaType)) {
dict.fold(content, #([], []), fn(acc, key, value) {
let #(known, unknown) = acc
case key {
"application/json" -> #([#(None, value), ..known], unknown)
"application/" <> rest ->
case string.split_once(rest, "+json") {
Ok(#(pre, "")) -> #([#(Some(pre), value), ..known], unknown)
_ -> #(known, [#(key, value), ..unknown])
}
_ -> #(known, [#(key, value), ..unknown])
}
})
}
fn gen_request_for_op(
op_entry,
pattern,
path_parameters,
components: oas.Components,
) {
let #(method, op) = op_entry
let oas.Operation(operation_id: id, parameters: op_parameters, ..) = op
let id = name_for_gleam_field_or_var(id)
let parameters = list.append(op_parameters, path_parameters)
let parameters =
list.map(parameters, oas.fetch_parameter(_, components.parameters))
let #(q_args, q_params) =
query_to_parts(oas.query_parameters(parameters), components)
let match = case oas.gather_match(pattern, parameters, components) {
Ok(match) -> match
Error(_reason) -> {
// #(reason, pattern, parameters)
panic as "could not find all parameters in match"
}
}
let body = case op.request_body {
Some(body) -> {
let oas.RequestBody(content: content, ..) =
oas.fetch_request_body(body, components.request_bodies)
let #(known, unknown) = get_structure_json_media(content)
case known, unknown {
[#(_, oas.MediaType(schema))], _ ->
case schema {
oas.Ref(ref: "#/components/schemas/" <> name, ..) -> {
let arg = name_for_gleam_field_or_var(name)
let encode =
call1(
"utils",
"json_to_bits",
call1("schema", encode_fn(name), glance.Variable(arg)),
)
Some(#(arg, encode))
}
_ -> {
Some(#("data", glance.Variable("data")))
}
}
// No content
[], [] -> None
[], [#(unknown, _)] -> {
io.println("unknown content type: " <> unknown)
None
}
_, _ -> {
io.println("multiple content types not supported")
None
}
}
}
None -> None
}
let parameters =
path_args(match)
|> list.append(case body {
Some(#(arg, _)) -> [
glance.FunctionParameter(None, glance.Named(arg), None),
]
None -> []
})
|> list.append(q_args)
let op_request = id <> "_request"
let op_response = id <> "_response"
let req_fn =
glance.Function(
name: op_request,
publicity: glance.Public,
parameters: [
glance.FunctionParameter(None, glance.Named("base"), None),
..parameters
],
return: None,
body: [
let_("method", gen_method(method)),
let_("path", concat_path(match, "")),
let_("query", glance.List(q_params, None)),
..case body {
Some(#(_arg, encode)) -> [
let_("body", encode),
glance.Expression(pipe(
glance.Variable("base"),
pipe(
call1("utils", "set_method", glance.Variable("method")),
pipe(
call1("utils", "append_path", glance.Variable("path")),
pipe(
call1("utils", "set_query", glance.Variable("query")),
call2(
"utils",
"set_body",
glance.String("application/json"),
glance.Variable("body"),
),
),
),
),
)),
]
None -> [
glance.Expression(pipe(
glance.Variable("base"),
pipe(
call1("utils", "set_method", glance.Variable("method")),
pipe(
call1("utils", "append_path", glance.Variable("path")),
call1("utils", "set_query", glance.Variable("query")),
),
),
)),
]
}
],
location: glance.Span(0, 0),
)
let fn_ =
glance.Function(
name: id,
publicity: glance.Public,
parameters: [
glance.FunctionParameter(None, glance.Named("token"), None),
..parameters
],
return: None,
body: [
let_(
"request",
glance.Call(glance.Variable("base_request"), [
glance.UnlabelledField(glance.Variable("token")),
]),
),
let_(
"request",
glance.Call(access("operations", op_request), [
glance.UnlabelledField(glance.Variable("request")),
..list.map(parameters, fn(p) {
let assert glance.FunctionParameter(name: glance.Named(name), ..) =
p
glance.UnlabelledField(glance.Variable(name))
})
]),
),
glance.Use(
[glance.PatternVariable("response")],
call1("t", "do", call1("t", "fetch", glance.Variable("request"))),
),
glance.Use(
[glance.PatternVariable("data")],
call1(
"t",
"try",
glance.Call(glance.Variable("handle_errors"), [
glance.UnlabelledField(call1(
"operations",
op_response,
glance.Variable("response"),
)),
]),
),
),
glance.Expression(call1("t", "Done", glance.Variable("data"))),
],
location: glance.Span(0, 0),
)
#(fn_, req_fn)
}
fn replace_gleam_keywords(key) {
case key {
"type" -> "type_"
"auto" -> "auto_"
"import" -> "import_"
// used in places
"base" -> "base_"
"path" -> "path_"
"method" -> "method_"
"token" -> "token_"
_ -> key
}
// case key {
// "_" <> key -> key
// _ -> key
// }
// // github is weird
}
fn replace_disallowed_charachters(in) {
in
|> string.replace("/", "_")
|> string.replace("+", "_")
// this is part of kebab casing
// |> string.replace("-", "Minus")
}
fn prefix_numbers(in) {
let needs_prefix =
list.any(
["1", "2", "3", "4", "5", "6", "7", "8", "9", "0"],
string.starts_with(in, _),
)
case needs_prefix {
True -> "n" <> in
False -> in
}
}
fn prefix_signs(in) {
let in = case string.starts_with(in, "-") {
True -> "negative" <> in
False -> in
}
case string.starts_with(in, "+") {
True -> "positive" <> in
False -> in
}
}
pub fn name_for_gleam_type(in) {
in
|> prefix_signs
|> replace_disallowed_charachters
|> justin.pascal_case()
|> prefix_numbers
}
pub fn name_for_gleam_field_or_var(in) {
in
|> prefix_signs
// calling snake case might remove a leading `_` and expose numbers
|> justin.snake_case()
|> prefix_numbers
|> replace_disallowed_charachters
|> replace_gleam_keywords()
}
fn gen_content_handling(operation_id, content, wrapper, internal) {
let module = Operations
let #(known, unknown) = get_structure_json_media(content)
case known, unknown {
[#(_, oas.MediaType(schema))], _ -> {
let #(lifted, _nullable, internal) = lift.do_lift(schema, internal)
let #(decoder, resp_type) = case lifted {
lift.Named(n) -> #(to_decoder(lift.Named(n), module), None)
lift.Primitive(p) -> #(to_decoder(lift.Primitive(p), module), None)
lift.Array(a) -> #(to_decoder(lift.Array(a), module), None)
lift.Tuple(of) -> #(to_decoder(lift.Tuple(of), module), None)
lift.Compound(lift.Fields(parameters, additional, required)) as top -> {
let name = operation_id <> "_response"
let type_ =
custom_type(name, parameters, additional, required, Operations)
let decoder =
glance.Block(gen_top_decoder_needs_name(name, top, module))
#(decoder, Some(type_))
}
lift.Dictionary(values) -> #(
to_decoder(lift.Dictionary(values), module),
None,
)
lift.Unsupported -> #(to_decoder(lift.Unsupported, module), None)
}
let action =
call2("json", "parse_bits", glance.Variable("body"), decoder)
|> pipe(call1("result", "map", glance.Variable(wrapper)))
// True because the body is used
#(#(action, resp_type, True), internal)
// gen_json_content_handling(operation_id, schema, wrapper)
}
// No content
[], [] -> #(just_return_ok_nil(wrapper), internal)
[], [#(unknown, _)] -> {
io.println("unknown content type: " <> unknown)
#(just_return_ok_nil(wrapper), internal)
}
_, _ -> {
io.println("multiple content types not supported")
#(just_return_ok_nil(wrapper), internal)
}
}
}
fn to_decoder(lifted, module) {
case lifted {
lift.Named("#/components/schemas/" <> name) -> {
let func = name_for_gleam_field_or_var(name <> "_decoder")
case module {
Schema -> glance.Call(glance.Variable(func), [])
_ -> call0("schema", func)
}
}
lift.Named(ref) ->
panic as { "not referencing schema component ref: " <> ref }
lift.Primitive(primitive) ->
case primitive {
lift.Boolean -> access("decode", "bool")
lift.Integer -> access("decode", "int")
lift.Number -> access("decode", "float")
lift.String -> access("decode", "string")
lift.Null -> always_decode()
lift.Always -> dynamic_decode()
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))],
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(call1("decode", "success", glance.Tuple(vars))),
]),
)
}
lift.Array(items) -> 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) ->
call2(
"decode",
"dict",
access("decode", "string"),
to_decoder(values, module),
)
lift.Unsupported -> dynamic_decode()
}
}
fn just_return_ok_nil(wrapper) {
let thing =
glance.Call(glance.Variable(wrapper), [
glance.UnlabelledField(glance.Variable("Nil")),
])
|> pipe(glance.Variable("Ok"))
#(thing, None, False)
}
fn status_range(responses, above_equal, bellow) {
list.filter_map(responses, fn(response) {
let #(status, response) = response
case status {
oas.Status(i) if above_equal <= i && i < bellow -> Ok(#(i, response))
_ -> Error(Nil)
}
})
}
fn gen_response(operation, components: oas.Components, internal) {
let #(_method, op) = operation
let op: oas.Operation = op
let responses = op.responses |> dict.to_list
let default =
list.find_map(responses, fn(response) {
let #(status, response) = response
case status {
oas.Default -> Ok(response)
_ -> Error(Nil)
}
})
let #(#(default_branch, resp_type, used), internal) = case default {
Ok(response) -> {
let oas.Response(content: content, ..) =
oas.fetch_response(response, components.responses)
gen_content_handling(op.operation_id, content, "Error", internal)
}
Error(Nil) -> {
case status_range(responses, 400, 600) {
[#(status, first), ..rest] -> {
case
list.try_fold(rest, [status], fn(acc, this) {
let #(status, this) = this
case first, this {
oas.Ref(ref: a, ..), oas.Ref(ref: b, ..) if a == b ->
Ok([status, ..acc])
oas.Inline(oas.Response(content: a, ..)),
oas.Inline(oas.Response(content: b, ..))
if a == b
-> Ok([status, ..acc])
_, _ -> Error(Nil)
}
})
{
Ok(_statuses) -> {
let oas.Response(content: content, ..) =
oas.fetch_response(first, components.responses)
gen_content_handling(op.operation_id, content, "Error", internal)
}
Error(_) -> {
let branch =
glance.Variable("response")
|> pipe(glance.Variable("Error"))
|> pipe(glance.Variable("Ok"))
#(#(branch, None, False), internal)
}
}
}
[] -> {
let branch =
glance.Variable("response")
|> pipe(glance.Variable("Error"))
|> pipe(glance.Variable("Ok"))
#(#(branch, None, False), internal)
}
}
}
}
let default_clause =
glance.Clause([[glance.PatternDiscard("")]], None, default_branch)
let #(#(used, response_type, expected_clauses), internal) = case
status_range(responses, 200, 300)
|> list.sort(fn(ra, rb) { int.compare(ra.0, rb.0) })
{
[] -> #(#(used, resp_type, []), internal)
[#(status, first), ..more] -> {
case more {
[] -> Nil
_ -> {
io.print("Doesn't support multiple ok statuses")
Nil
}
}
let oas.Response(content: content, ..) =
oas.fetch_response(first, components.responses)
let #(#(branch, resp_type, u), internal) =
gen_content_handling(op.operation_id, content, "Ok", internal)
let clause =
glance.Clause(
[[glance.PatternInt(int.to_string(status))]],
None,
branch,
)
#(#(used || u, resp_type, [clause]), internal)
}
}
let response_handler =
glance.Function(
name: name_for_gleam_field_or_var(op.operation_id <> "_response"),
publicity: glance.Public,
parameters: [
glance.FunctionParameter(None, glance.Named("response"), None),
],
return: None,
body: [
glance.Assignment(
glance.Let,
glance.PatternConstructor(
Some("response"),
// This is broken in glance printer
".Response",
[
glance.ShorthandField("status"),
..case used {
True -> [glance.ShorthandField("body")]
False -> []
}
],
True,
),
None,
glance.Variable("response"),
),
glance.Expression(glance.Case(
[glance.Variable("status")],
list.append(expected_clauses, [default_clause]),
)),
],
location: glance.Span(0, 0),
)
#(#(response_handler, response_type), internal)
}
// This returns functions for the top level and operations file
fn gen_fns(key, path_item: oas.PathItem, components, exclude, internal) {
let operations =
list.filter(path_item.operations, fn(op) {
!list.contains(exclude, { op.1 }.operation_id)
})
list.map_fold(operations, internal, fn(internal, op) {
let #(fn_, req_fn) =
gen_request_for_op(op, key, path_item.parameters, components)
let #(#(response_handler, response_type), internal) =
gen_response(op, components, internal)
#(internal, #(#([response_handler, req_fn], response_type), fn_))
})
}
fn gen_operations(op, components, exclude, internal) {
let #(key, path) = op
gen_fns(key, path, components, exclude, internal)
}
fn defs(xs) {
list.map(xs, glance.Definition([], _))
}
pub fn build(spec_src, project_path, provider, exclude) {
let module_path = project_path <> "/src/" <> provider
use file <- try(
simplifile.read(spec_src)
|> snag.map_error(simplifile.describe_error)
|> snag.context("Could not read file " <> spec_src),
)
use spec <- try(
json.parse(file, oas.decoder())
|> snag.map_error(json_decode_error_to_string),
)
let #(operations, entry) =
gen_operations_and_top_files(spec, provider, exclude)
let operations_module_file = module_path <> "/operations.gleam"
use Nil <- try(
simplifile.write_bits(
operations_module_file,
operations |> bit_array.from_string,
)
|> snag.map_error(simplifile.describe_error)
|> snag.context("Could not write file " <> operations_module_file),
)
use mod <- try(
simplifile.read(module_path <> ".gleam")
|> snag.map_error(simplifile.describe_error)
|> snag.context("Could not read file " <> module_path <> ".gleam"),
)
let split = "// GENERATED ---"
let pre = case string.split_once(mod, split) {
Ok(#(pre, _)) -> pre
Error(Nil) -> mod
}
let content = <<pre:utf8, split:utf8, "----------\n\n", entry:utf8>>
use Nil <- try(
simplifile.write_bits(module_path <> ".gleam", content)
|> snag.map_error(simplifile.describe_error)
|> snag.context("Could not write file " <> module_path <> ".gleam"),
)
let content =
gen_schema_file(spec.components.schemas, provider)
|> bit_array.from_string()
let schema_module_file = module_path <> "/schema.gleam"
use Nil <- try(
simplifile.write_bits(schema_module_file, content)
|> snag.map_error(simplifile.describe_error)
|> snag.context("Could not write file " <> schema_module_file),
)
Ok(Nil)
}
pub fn gen_operations_and_top_files(spec: oas.Document, provider, exclude) {
let paths = dict.to_list(spec.paths)
let internal = []
let #(internal, fs) =
list.map_fold(paths, internal, fn(internal, path) {
gen_operations(path, spec.components, exclude, internal)
})
let #(internal_types, internal_decoders) =
list.index_map(internal, fn(fields, index) {
let lift.Fields(properties, additional, required) = fields
let name = "Internal" <> int.to_string(index)
let type_ =
custom_type(name, properties, additional, required, Operations)
let encoder = schema_to_decode_fn(#(name, lift.Compound(fields)))
#(type_, encoder)
})
|> list.unzip
let #(operation_functions, top) = list.unzip(list.flatten(fs))
let #(operation_functions, operation_types) = list.unzip(operation_functions)
let operation_types =
list.filter_map(operation_types, option.to_result(_, Nil))
let operation_functions = list.flatten(operation_functions)
let modules = [
provider <> "/utils",
provider <> "/schema",
"gleam/http",
"gleam/http/response",
"gleam/int",
"gleam/float",
"gleam/json",
"gleam/dynamic/decode",
"gleam/result",
"gleam/bool",
// "gleam/option",
]
let operations =
glance.Module(
defs([
glance.Import(
"gleam/option",
None,
[glance.UnqualifiedImport("Option", None)],
[glance.UnqualifiedImport("None", None)],
),
..list.map(modules, glance.Import(_, None, [], []))
]),
defs(list.append(internal_types, operation_types)),
[],
[],
list.map(
list.append(internal_decoders, operation_functions),
glance.Definition([], _),
),
)
|> glance_printer.print
let entry =
glance.Module([], [], [], [], list.map(top, glance.Definition([], _)))
|> glance_printer.print
#(operations, entry)
}
pub fn gen_schema_file(schemas, provider) {
let #(custom_types, type_aliases, functions) = gen_schema(schemas)
glance.Module(
[
glance.Definition([], glance.Import("gleam/dynamic/decode", None, [], [])),
glance.Definition([], glance.Import("gleam/dynamic", None, [], [])),
glance.Definition([], glance.Import("gleam/json", None, [], [])),
glance.Definition([], glance.Import(provider <> "/utils", None, [], [])),
glance.Definition(
[],
glance.Import(
"gleam/option",
None,
[glance.UnqualifiedImport("Option", None)],
[glance.UnqualifiedImport("None", None)],
),
),
],
defs(custom_types),
defs(type_aliases),
[],
defs(functions),
)
|> glance_printer.print
}
pub fn json_decode_error_to_string(error: json.DecodeError) -> String {
case error {
json.UnexpectedEndOfInput -> "UnexpectedEndOfInput"
json.UnexpectedByte(str) -> "UnexpectedByte " <> str
json.UnexpectedSequence(str) -> "UnexpectedSequence " <> str
json.UnableToDecode(errors) ->
"UnableToDecode " <> print_decode_errors(errors)
}
}
fn print_decode_errors(errors) {
list.map(errors, fn(error) {
let decode.DecodeError(expected:, found:, path:) = error
"expected "
<> expected
<> " found "
<> found
<> " at "
<> string.join(path, "/")
})
|> string.join("\n")
}