Current section
Files
Jump to
Current section
Files
src/json_typedef.gleam
//// <https://jsontypedef.com/>
////
//// <https://datatracker.ietf.org/doc/html/rfc8927>
import gleam/bool
import gleam/dict.{type Dict}
import gleam/dynamic.{type Dynamic}
import gleam/json.{type Json}
import gleam/list
import gleam/option.{type Option, None, Some}
import gleam/result
import gleam/set.{type Set}
import gleam/string
import justin
pub type RootSchema {
RootSchema(definitions: List(#(String, Schema)), schema: Schema)
}
pub type Type {
/// `true` or `false`
Boolean
/// JSON strings
String
/// JSON strings containing an RFC3339 timestamp
Timestamp
/// JSON numbers
Float32
/// JSON numbers
Float64
/// Whole JSON numbers that fit in a signed 8-bit integer
Int8
/// Whole JSON numbers that fit in an unsigned 8-bit integer
Uint8
/// Whole JSON numbers that fit in a signed 16-bit integer
Int16
/// Whole JSON numbers that fit in an unsigned 16-bit integer
Uint16
/// Whole JSON numbers that fit in a signed 32-bit integer
Int32
/// Whole JSON numbers that fit in an unsigned 32-bit integer
Uint32
}
pub type Schema {
/// Any value. The empty form is like a Java Object or TypeScript any.
Empty
/// A simple built-in type. The type form is like a Java or TypeScript
/// primitive type.
Type(nullable: Bool, metadata: List(#(String, Dynamic)), type_: Type)
/// One of a fixed set of strings. The enum form is like a Java or TypeScript
/// enum.
Enum(
nullable: Bool,
metadata: List(#(String, Dynamic)),
variants: List(String),
)
// The properties form is like a Java class or TypeScript interface.
Properties(
nullable: Bool,
metadata: List(#(String, Dynamic)),
schema: PropertiesSchema,
)
/// A sequence of some other form. The elements form is like a Java `List<T>`
/// or TypeScript `T[]`.
Elements(nullable: Bool, metadata: List(#(String, Dynamic)), schema: Schema)
/// A dictionary with string keys and some form values. The values form is
/// like a Java `Map<String, T>` or TypeScript `{ [key: string]: T}`.
Values(nullable: Bool, metadata: List(#(String, Dynamic)), schema: Schema)
/// The discriminator form is like a tagged union.
Discriminator(
nullable: Bool,
metadata: List(#(String, Dynamic)),
tag: String,
mapping: List(#(String, PropertiesSchema)),
)
/// The ref form is for re-using schemas, usually so you can avoid repeating
/// yourself.
Ref(nullable: Bool, metadata: List(#(String, Dynamic)), name: String)
}
pub type PropertiesSchema {
PropertiesSchema(
properties: List(#(String, Schema)),
optional_properties: List(#(String, Schema)),
additional_properties: Bool,
)
}
pub fn to_json(schema: RootSchema) -> Json {
let properties = schema_to_json(schema.schema)
let properties = case schema.definitions {
[] -> properties
definitions -> {
let definitions =
list.map(definitions, fn(definition) {
#(definition.0, json.object(schema_to_json(definition.1)))
})
[#("definitions", json.object(definitions)), ..properties]
}
}
json.object(properties)
}
fn properties_schema_to_json(schema: PropertiesSchema) -> List(#(String, Json)) {
let props_json = fn(props: List(#(String, Schema))) {
json.object(
list.map(props, fn(property) {
#(property.0, json.object(schema_to_json(property.1)))
}),
)
}
let PropertiesSchema(
properties:,
optional_properties:,
additional_properties:,
) = schema
let data = []
let data = case additional_properties {
False -> data
_ -> [#("additionalProperties", json.bool(True)), ..data]
}
let data = case optional_properties {
[] -> data
p -> [#("optionalProperties", props_json(p)), ..data]
}
let data = case properties {
[] -> data
p -> [#("properties", props_json(p)), ..data]
}
data
}
fn schema_to_json(schema: Schema) -> List(#(String, Json)) {
case schema {
Empty -> []
Ref(nullable:, metadata:, name:) ->
[#("values", json.string(name))]
|> add_nullable(nullable)
|> add_metadata(metadata)
Type(nullable:, metadata:, type_:) ->
[#("type", type_to_json(type_))]
|> add_nullable(nullable)
|> add_metadata(metadata)
Enum(nullable:, metadata:, variants:) ->
[#("enum", json.array(variants, json.string))]
|> add_nullable(nullable)
|> add_metadata(metadata)
Values(nullable:, metadata:, schema:) ->
[#("values", json.object(schema_to_json(schema)))]
|> add_nullable(nullable)
|> add_metadata(metadata)
Elements(nullable:, metadata:, schema:) ->
[#("elements", json.object(schema_to_json(schema)))]
|> add_nullable(nullable)
|> add_metadata(metadata)
Properties(nullable:, metadata:, schema:) ->
properties_schema_to_json(schema)
|> add_nullable(nullable)
|> add_metadata(metadata)
Discriminator(nullable:, tag:, mapping:, metadata: _) ->
discriminator_to_json(tag, mapping)
|> add_nullable(nullable)
}
}
fn type_to_json(t: Type) -> Json {
json.string(case t {
Boolean -> "boolean"
Float32 -> "float32"
Float64 -> "float64"
Int16 -> "int16"
Int32 -> "int32"
Int8 -> "int8"
String -> "string"
Timestamp -> "timestamp"
Uint16 -> "uint16"
Uint32 -> "uint32"
Uint8 -> "uint8"
})
}
fn discriminator_to_json(
tag: String,
mapping: List(#(String, PropertiesSchema)),
) -> List(#(String, Json)) {
let mapping =
list.map(mapping, fn(variant) {
#(variant.0, json.object(properties_schema_to_json(variant.1)))
})
[#("discriminator", json.string(tag)), #("mapping", json.object(mapping))]
}
pub fn decoder(data: Dynamic) -> Result(RootSchema, List(dynamic.DecodeError)) {
dynamic.decode2(RootSchema, fn(_) { Ok([]) }, decode_schema)(data)
}
fn decode_schema(data: Dynamic) -> Result(Schema, List(dynamic.DecodeError)) {
use data <- result.try(dynamic.dict(dynamic.string, dynamic.dynamic)(data))
let decoder =
key_decoder(data, "type", decode_type)
|> result.lazy_or(fn() { key_decoder(data, "enum", decode_enum) })
|> result.lazy_or(fn() { key_decoder(data, "ref", decode_ref) })
|> result.lazy_or(fn() { key_decoder(data, "values", decode_values) })
|> result.lazy_or(fn() { key_decoder(data, "elements", decode_elements) })
|> result.lazy_or(fn() {
key_decoder(data, "discriminator", decode_discriminator)
})
|> result.lazy_or(fn() {
key_decoder(data, "properties", decode_properties)
})
|> result.lazy_or(fn() {
key_decoder(data, "extraProperties", decode_properties)
})
|> result.lazy_or(fn() {
key_decoder(data, "additionalProperties", decode_properties)
})
|> result.unwrap(fn() { decode_empty(data) })
decoder()
}
fn key_decoder(
dict: Dict(String, Dynamic),
key: String,
constructor: fn(Dynamic, Dict(String, Dynamic)) ->
Result(t, List(dynamic.DecodeError)),
) -> Result(fn() -> Result(t, List(dynamic.DecodeError)), Nil) {
case dict.get(dict, key) {
Ok(value) -> Ok(fn() { constructor(value, dict) })
Error(e) -> Error(e)
}
}
fn decode_discriminator(
tag: Dynamic,
data: Dict(String, Dynamic),
) -> Result(Schema, List(dynamic.DecodeError)) {
use nullable <- result.try(get_nullable(data))
use metadata <- result.try(get_metadata(data))
use tag <- result.try(dynamic.string(tag) |> push_path("discriminator"))
use mapping <- result.try(case dict.get(data, "mapping") {
Ok(mapping) -> Ok(mapping)
Error(_) -> Error([dynamic.DecodeError("field", "nothing", ["mapping"])])
})
use properties <- result.try(
decode_object_as_list(mapping, decode_properties_schema)
|> push_path("mapping"),
)
Ok(Discriminator(nullable:, tag:, mapping: properties, metadata:))
}
fn decode_object_as_list(
data: Dynamic,
inner: dynamic.Decoder(t),
) -> Result(List(#(String, t)), List(dynamic.DecodeError)) {
dynamic.dict(dynamic.string, inner)(data)
|> result.map(dict.to_list)
}
fn decode_properties(
_tag: Dynamic,
data: Dict(String, Dynamic),
) -> Result(Schema, List(dynamic.DecodeError)) {
use nullable <- result.try(get_nullable(data))
use metadata <- result.try(get_metadata(data))
dynamic.from(data)
|> decode_properties_schema
|> result.map(Properties(nullable, metadata, _))
}
fn decode_properties_schema(
data: Dynamic,
) -> Result(PropertiesSchema, List(dynamic.DecodeError)) {
let field = fn(name, data) {
case dynamic.field(name, dynamic.dynamic)(data) {
Ok(d) -> decode_object_as_list(d, decode_schema) |> push_path(name)
Error(_) -> Ok([])
}
}
dynamic.decode3(
PropertiesSchema,
field("properties", _),
field("optionalProperties", _),
fn(d) {
case dynamic.field("additionalProperties", dynamic.dynamic)(d) {
Ok(d) -> dynamic.bool(d) |> push_path("additionalProperties")
Error(_) -> Ok(False)
}
},
)(data)
}
fn decode_type(
type_: Dynamic,
data: Dict(String, Dynamic),
) -> Result(Schema, List(dynamic.DecodeError)) {
use type_ <- result.try(dynamic.string(type_) |> push_path("type"))
use nullable <- result.try(get_nullable(data))
use metadata <- result.try(get_metadata(data))
case type_ {
"boolean" -> Ok(Type(nullable, metadata, Boolean))
"float32" -> Ok(Type(nullable, metadata, Float32))
"float64" -> Ok(Type(nullable, metadata, Float64))
"int16" -> Ok(Type(nullable, metadata, Int16))
"int32" -> Ok(Type(nullable, metadata, Int32))
"int8" -> Ok(Type(nullable, metadata, Int8))
"string" -> Ok(Type(nullable, metadata, String))
"timestamp" -> Ok(Type(nullable, metadata, Timestamp))
"uint16" -> Ok(Type(nullable, metadata, Uint16))
"uint32" -> Ok(Type(nullable, metadata, Uint32))
"uint8" -> Ok(Type(nullable, metadata, Uint8))
_ -> Error([dynamic.DecodeError("Type", "String", ["type"])])
}
}
fn decode_enum(
type_: Dynamic,
data: Dict(String, Dynamic),
) -> Result(Schema, List(dynamic.DecodeError)) {
use nullable <- result.try(get_nullable(data))
use metadata <- result.try(get_metadata(data))
dynamic.list(dynamic.string)(type_)
|> push_path("enum")
|> result.map(Enum(nullable, metadata, _))
}
fn decode_ref(
type_: Dynamic,
data: Dict(String, Dynamic),
) -> Result(Schema, List(dynamic.DecodeError)) {
use nullable <- result.try(get_nullable(data))
use metadata <- result.try(get_metadata(data))
dynamic.string(type_)
|> push_path("ref")
|> result.map(Ref(nullable, metadata, _))
}
fn decode_empty(
data: Dict(String, Dynamic),
) -> Result(Schema, List(dynamic.DecodeError)) {
case dict.size(data) {
0 -> Ok(Empty)
_ -> Error([dynamic.DecodeError("Schema", "Dict", [])])
}
}
fn decode_values(
values: Dynamic,
data: Dict(String, Dynamic),
) -> Result(Schema, List(dynamic.DecodeError)) {
use nullable <- result.try(get_nullable(data))
use metadata <- result.try(get_metadata(data))
decode_schema(values)
|> push_path("values")
|> result.map(Values(nullable, metadata, _))
}
fn decode_elements(
elements: Dynamic,
data: Dict(String, Dynamic),
) -> Result(Schema, List(dynamic.DecodeError)) {
use nullable <- result.try(get_nullable(data))
use metadata <- result.try(get_metadata(data))
decode_schema(elements)
|> push_path("elements")
|> result.map(Elements(nullable, metadata, _))
}
fn push_path(
result: Result(t, List(dynamic.DecodeError)),
segment: String,
) -> Result(t, List(dynamic.DecodeError)) {
result.map_error(
result,
list.map(_, fn(e) { dynamic.DecodeError(..e, path: [segment, ..e.path]) }),
)
}
fn get_metadata(
data: Dict(String, Dynamic),
) -> Result(List(#(String, Dynamic)), List(dynamic.DecodeError)) {
case dict.get(data, "metadata") {
Ok(data) ->
dynamic.dict(dynamic.string, dynamic.dynamic)(data)
|> result.map(dict.to_list)
|> push_path("metadata")
Error(_) -> Ok([])
}
}
fn get_nullable(
data: Dict(String, Dynamic),
) -> Result(Bool, List(dynamic.DecodeError)) {
case dict.get(data, "nullable") {
Ok(data) -> dynamic.bool(data) |> push_path("nullable")
Error(_) -> Ok(False)
}
}
fn metadata_value_to_json(data: Dynamic) -> Json {
let decoder =
dynamic.any([
fn(a) { dynamic.string(a) |> result.map(json.string) },
fn(a) { dynamic.int(a) |> result.map(json.int) },
fn(a) { dynamic.float(a) |> result.map(json.float) },
])
case decoder(data) {
Ok(data) -> data
Error(_) -> json.string(string.inspect(data))
}
}
fn add_metadata(
data: List(#(String, Json)),
metadata: List(#(String, Dynamic)),
) -> List(#(String, Json)) {
case metadata {
[] -> data
_ -> {
let metadata =
list.map(metadata, fn(metadata) {
#(metadata.0, metadata_value_to_json(metadata.1))
})
[#("metadata", json.object(metadata)), ..data]
}
}
}
fn add_nullable(
data: List(#(String, Json)),
nullable: Bool,
) -> List(#(String, Json)) {
case nullable {
False -> data
True -> [#("nullable", json.bool(True)), ..data]
}
}
pub opaque type Generator {
Generator(
generate_decoders: Bool,
generate_encoders: Bool,
dynamic_used: Bool,
option_used: Bool,
dict_used: Bool,
optional_properties_used: Bool,
types: Dict(String, String),
functions: Dict(String, String),
root_name: String,
constructors: Set(String),
)
}
pub fn codegen() -> Generator {
Generator(
dynamic_used: False,
option_used: False,
dict_used: False,
optional_properties_used: False,
generate_decoders: False,
generate_encoders: False,
types: dict.new(),
functions: dict.new(),
root_name: "Data",
constructors: set.new(),
)
}
pub fn root_name(gen: Generator, root_name: String) -> Generator {
Generator(..gen, root_name:)
}
pub fn generate_encoders(gen: Generator, x: Bool) -> Generator {
Generator(..gen, generate_encoders: x)
}
pub fn generate_decoders(gen: Generator, x: Bool) -> Generator {
Generator(..gen, generate_decoders: x)
}
type Out {
Out(src: String, type_name: String)
}
pub type CodegenError {
CannotConvertEmptyToJsonError
EmptyEnumError
DuplicatePropertyError(
type_name: String,
constructor_name: String,
property_name: String,
)
DuplicateConstructorError(name: String)
DuplicateTypeError(name: String)
DuplicateFunctionError(name: String)
}
pub fn generate(
gen: Generator,
schema: RootSchema,
) -> Result(String, CodegenError) {
let root = justin.pascal_case(gen.root_name)
use gen <- result.try(gen_types(gen, root, schema))
use gen <- result.try(case gen.generate_decoders {
True -> gen_decoders(gen, root, schema)
False -> Ok(gen)
})
use gen <- result.map(case gen.generate_encoders {
True -> gen_encoders(gen, root, schema)
False -> Ok(gen)
})
gen_to_string(gen)
}
fn gen_types(
gen: Generator,
root: String,
schema: RootSchema,
) -> Result(Generator, CodegenError) {
use gen <- result.try(
list.try_fold(schema.definitions, gen, fn(gen, def) {
gen_type(gen, def.0, def.1)
}),
)
gen_type(gen, root, schema.schema)
}
fn gen_encoders(
gen: Generator,
root: String,
schema: RootSchema,
) -> Result(Generator, CodegenError) {
gen_add_encoder(gen, root, schema.schema)
}
fn gen_decoders(
gen: Generator,
root: String,
schema: RootSchema,
) -> Result(Generator, CodegenError) {
gen_add_decoder(gen, root, schema.schema)
}
fn gen_type(
gen: Generator,
name: String,
schema: Schema,
) -> Result(Generator, CodegenError) {
let name = justin.pascal_case(name)
case schema {
Empty -> Ok(Generator(..gen, dynamic_used: True))
Ref(nullable:, ..) -> Ok(gen_register_nullable(gen, nullable))
Type(nullable:, ..) -> Ok(gen_register_nullable(gen, nullable))
Enum(nullable:, variants:, ..) -> {
let gen = gen_register_nullable(gen, nullable)
gen_enum_type(gen, name, variants)
}
Properties(schema:, nullable:, ..) -> {
let gen = gen_register_nullable(gen, nullable)
gen_register_properties(gen, name, schema)
}
Elements(nullable:, schema:, ..) -> {
let gen = gen_register_nullable(gen, nullable)
gen_type(gen, name <> "Element", schema)
}
Values(nullable:, schema:, ..) -> {
let gen = gen_register_nullable(gen, nullable)
let gen = Generator(..gen, dict_used: True)
gen_type(gen, name <> "Value", schema)
}
Discriminator(nullable:, metadata: _, tag:, mapping:) -> {
let gen = gen_register_nullable(gen, nullable)
gen_register_discriminator(gen, tag, name, mapping)
}
}
}
fn type_name(schema: Schema, name: String) -> String {
let nullable = case schema {
Empty -> False
Discriminator(nullable:, ..)
| Elements(nullable:, ..)
| Enum(nullable:, ..)
| Properties(nullable:, ..)
| Ref(nullable:, ..)
| Type(nullable:, ..)
| Values(nullable:, ..) -> nullable
}
let name = case schema {
Enum(..) | Properties(..) | Discriminator(..) -> name
Ref(name:, ..) -> name
Values(schema:, ..) -> "dict.Dict(" <> type_name(schema, name) <> ")"
Elements(schema:, ..) ->
"List(" <> type_name(schema, name <> "Element") <> ")"
Empty -> "dynamic.Dynamic"
Type(type_:, ..) ->
case type_ {
Boolean -> "Bool"
Float32 | Float64 -> "Float"
Int16 | Int32 | Int8 | Uint16 | Uint32 | Uint8 -> "Int"
String | Timestamp -> "String"
}
}
case nullable {
True -> "option.Option(" <> name <> ")"
False -> name
}
}
fn gen_register_discriminator(
gen: Generator,
tag: String,
name: String,
mapping: List(#(String, PropertiesSchema)),
) -> Result(Generator, CodegenError) {
let type_name = name
use #(gen, src) <- result.try(
list.try_fold(mapping, #(gen, []), fn(pair, mapping) {
let name = name <> justin.pascal_case(mapping.0)
let result =
ensure_no_duplicate_properties(mapping.1, Some(tag), type_name, name)
use _ <- result.try(result)
let result = type_variant(pair.0, name, mapping.1)
use #(gen, src) <- result.map(result)
#(gen, [src, ..pair.1])
}),
)
let src = "pub type " <> name <> " {
" <> string.join(src, "\n") <> "
}"
let type_name = name
gen_add_type(gen, type_name, src)
}
fn gen_register_properties(
gen: Generator,
name: String,
schema: PropertiesSchema,
) -> Result(Generator, CodegenError) {
use _ <- result.try(ensure_no_duplicate_properties(schema, None, name, name))
use #(gen, src) <- result.try(type_variant(gen, name, schema))
let src = "pub type " <> name <> " {
" <> src <> "
}"
let type_name = name
gen_add_type(gen, type_name, src)
}
fn ensure_no_duplicate_properties(
schema: PropertiesSchema,
extra: Option(String),
type_name: String,
constructor_name: String,
) -> Result(Nil, CodegenError) {
let names = list.append(schema.properties, schema.optional_properties)
let recorded = case extra {
None -> set.new()
Some(k) -> set.from_list([justin.snake_case(k)])
}
list.try_fold(names, recorded, fn(recorded, pair) {
let field_name = justin.snake_case(pair.0)
let before = set.size(recorded)
let recorded = set.insert(recorded, field_name)
case before == set.size(recorded) {
False -> Ok(recorded)
True ->
Error(DuplicatePropertyError(type_name, constructor_name, field_name))
}
})
|> result.replace(Nil)
}
fn type_variant(
gen: Generator,
name: String,
schema: PropertiesSchema,
) -> Result(#(Generator, String), CodegenError) {
let gen = case schema.optional_properties {
[] -> gen
_ -> Generator(..gen, optional_properties_used: True)
}
let PropertiesSchema(
properties:,
optional_properties:,
additional_properties: _,
) = schema
use gen <- result.try(
list.try_fold(properties, gen, fn(gen, prop) {
gen_type(gen, name <> justin.pascal_case(prop.0), prop.1)
}),
)
use gen <- result.try(
list.try_fold(optional_properties, gen, fn(gen, prop) {
let gen = Generator(..gen, option_used: True)
gen_type(gen, name <> justin.pascal_case(prop.0), prop.1)
}),
)
let properties =
list.append(
list.map(properties, fn(p) { #(p.0, p.1, False) }),
list.map(optional_properties, fn(p) { #(p.0, p.1, True) }),
)
|> list.sort(fn(a, b) { string.compare(a.0, b.0) })
|> list.map(fn(p) {
let n = justin.snake_case(p.0)
" " <> n <> ": " <> type_name(p.1, name <> justin.pascal_case(p.0))
})
|> string.join(",\n")
use gen <- result.try(ensure_constructor_unique(gen, name))
let src = " " <> name <> "(
" <> properties <> ",
)"
Ok(#(gen, src))
}
fn ensure_constructor_unique(
gen: Generator,
name: String,
) -> Result(Generator, CodegenError) {
let before = set.size(gen.constructors)
let constructors = set.insert(gen.constructors, name)
case before == set.size(constructors) {
False -> Ok(Generator(..gen, constructors:))
True -> Error(DuplicateConstructorError(name))
}
}
fn gen_enum_type(
gen: Generator,
name: String,
variants: List(String),
) -> Result(Generator, CodegenError) {
use <- bool.guard(when: variants == [], return: Error(EmptyEnumError))
let variants =
variants
|> list.map(fn(v) { " " <> justin.pascal_case(v) <> "\n" })
|> string.join("")
let src = "pub type " <> name <> " {\n" <> variants <> "}"
gen_add_type(gen, name, src)
}
fn gen_register_nullable(gen: Generator, nullable: Bool) -> Generator {
case nullable {
False -> gen
True -> Generator(..gen, option_used: True)
}
}
fn gen_add_encoder(
gen: Generator,
name: String,
schema: Schema,
) -> Result(Generator, CodegenError) {
use out <- result.try(en_schema(schema, Some("data"), name))
let name = justin.snake_case(name) <> "_to_json"
let src = "pub fn " <> name <> "(data: " <> out.type_name <> ") -> json.Json {
" <> out.src <> "
}"
gen_add_function(gen, name, src)
}
fn gen_add_decoder(
gen: Generator,
name: String,
schema: Schema,
) -> Result(Generator, CodegenError) {
use out <- result.try(de_schema(schema, name))
let fn_name = justin.snake_case(name) <> "_decoder"
let src =
"pub fn " <> fn_name <> "() -> decode.Decoder(" <> out.type_name <> ") {
" <> out.src <> "
}"
gen_add_function(gen, name, src)
}
fn gen_add_function(
gen: Generator,
name: String,
body: String,
) -> Result(Generator, CodegenError) {
let before = dict.size(gen.functions)
let functions = dict.insert(gen.functions, name, body)
case dict.size(functions) == before {
False -> Ok(Generator(..gen, functions:))
True -> Error(DuplicateFunctionError(name))
}
}
fn gen_add_type(
gen: Generator,
name: String,
body: String,
) -> Result(Generator, CodegenError) {
let before = dict.size(gen.types)
let types = dict.insert(gen.types, name, body)
case dict.size(types) == before {
False -> Ok(Generator(..gen, types:))
True -> Error(DuplicateTypeError(name))
}
}
fn gen_to_string(gen: Generator) -> String {
let imp = fn(used, module) {
case used {
True -> ["import " <> module]
False -> []
}
}
let imports =
[
imp(gen.generate_decoders, "decode"),
imp(gen.dict_used, "gleam/dict"),
imp(gen.dynamic_used, "gleam/dynamic"),
imp(gen.generate_encoders, "gleam/json"),
imp(gen.option_used, "gleam/option"),
]
|> list.flatten
|> string.join("\n")
let defs = fn(items: Dict(String, String)) -> String {
items
|> dict.to_list
|> list.sort(fn(a, b) { string.compare(a.0, b.0) })
|> list.map(fn(a) { a.1 })
|> string.join("\n\n")
}
let block = fn(s) {
case s {
"" -> []
_ -> [s]
}
}
let helper__optional_property = case
gen.generate_encoders && gen.optional_properties_used
{
False -> []
True -> [
"fn helper__optional_property(
object: List(#(String, json.Json)),
key: String,
value: option.Option(a),
to_json: fn(a) -> json.Json,
) -> List(#(String, json.Json)) {
case value {
option.Some(value) -> [#(key, to_json(value)), ..object]
option.None -> object
}
}",
]
}
let helper__dict_to_json = case gen.generate_encoders && gen.dict_used {
False -> []
True -> [
"fn helper__dict_to_json(
data: dict.Dict(String, t),
to_json: fn(t) -> json.Json,
) -> json.Json {
data
|> dict.to_list
|> list.map(fn(pair) { #(pair.0, to_json(pair.1)) })
|> json.object
}",
]
}
[
block(imports),
block(defs(gen.types)),
block(defs(gen.functions)),
helper__dict_to_json,
helper__optional_property,
]
|> list.flatten
|> string.join("\n\n")
}
fn en_schema(
schema: Schema,
data: Option(String),
name: String,
) -> Result(Out, CodegenError) {
case schema {
Discriminator(nullable:, metadata: _, mapping:, tag:) ->
en_discriminator(mapping, tag, nullable, data, name)
Elements(schema:, nullable:, metadata: _) ->
en_elements(schema, nullable, data, name <> "Element")
Empty -> Error(CannotConvertEmptyToJsonError)
Enum(nullable:, variants:, metadata: _) ->
en_enum(variants, nullable, data, name)
Properties(nullable:, schema:, metadata: _) ->
en_properties_schema(schema, nullable, pro_data_name(data), name, None)
Ref(nullable:, metadata: _, name:) -> en_ref(name, data, nullable)
Type(type_:, nullable:, metadata: _) -> Ok(en_type(type_, nullable, data))
Values(schema:, nullable:, metadata: _) ->
en_values(schema, nullable, data, name)
}
}
fn en_ref(
name: String,
data: Option(String),
nullable: Bool,
) -> Result(Out, CodegenError) {
let src = justin.snake_case(name) <> "_to_json"
let src = case data, nullable {
None, False -> src
None, True -> "json.nullable(_, " <> src <> ")"
Some(data), False -> src <> "(" <> data <> ")"
Some(data), True -> "json.nullable(" <> data <> ", " <> src <> ")"
}
let type_name = justin.pascal_case(name)
let type_name = case nullable {
False -> type_name
True -> "option.Option(" <> type_name <> ")"
}
Ok(Out(src:, type_name:))
}
fn de_ref(name: String, nullable: Bool) -> Result(Out, CodegenError) {
let src = justin.snake_case(name) <> "_decoder()"
let src = case nullable {
False -> src
True -> "decode.optional(" <> src <> ")"
}
let type_name = justin.pascal_case(name)
let type_name = case nullable {
False -> type_name
True -> "option.Option(" <> type_name <> ")"
}
Ok(Out(src:, type_name:))
}
fn pro_data_name(name: Option(String)) -> PropertyDataName {
case name {
None -> PropertyDataNone
Some(n) -> PropertyDataAccess(n)
}
}
fn de_schema(schema: Schema, name: String) -> Result(Out, CodegenError) {
case schema {
Discriminator(nullable:, metadata: _, mapping:, tag:) ->
de_discriminator(mapping, tag, nullable, name)
Elements(schema:, nullable:, metadata: _) ->
de_elements(schema, nullable, name <> "Element")
Empty -> Ok(Out("decode.dynamic", "dynamic.Dynamic"))
Enum(nullable:, variants:, metadata: _) -> de_enum(variants, nullable, name)
Properties(nullable:, schema:, metadata: _) ->
de_properties_schema(schema, nullable, name)
Ref(nullable:, metadata: _, name:) -> de_ref(name, nullable)
Type(type_:, nullable:, metadata: _) -> Ok(de_type(type_, nullable))
Values(schema:, nullable:, metadata: _) -> de_values(schema, nullable, name)
}
}
fn de_discriminator(
mapping: List(#(String, PropertiesSchema)),
tag: String,
nullable: Bool,
name: String,
) -> Result(Out, CodegenError) {
use mapping <- result.try(
list.try_map(mapping, fn(pair) {
let result =
de_properties_schema(pair.1, False, name <> justin.pascal_case(pair.0))
use out <- result.map(result)
#(pair.0, out.src)
}),
)
let clauses =
list.map(mapping, fn(pair) { " \"" <> pair.0 <> "\" -> " <> pair.1 })
let src = "decode.at([\"" <> tag <> "\"], decode.string)
|> decode.then(fn(tag) {
case tag {
" <> string.join(clauses, "\n") <> "
_ -> decode.fail(\"" <> name <> "\")
}
})"
let type_name = case nullable {
False -> name
True -> "option.Option(" <> name <> ")"
}
Ok(Out(src:, type_name:))
}
type PropertyDataName {
PropertyDataNone
PropertyDataDirect
PropertyDataAccess(String)
}
fn en_discriminator(
mapping: List(#(String, PropertiesSchema)),
tag: String,
nullable: Bool,
data: Option(String),
name: String,
) -> Result(Out, CodegenError) {
use properties <- result.try(
list.try_map(mapping, fn(pair) {
let name = name <> justin.pascal_case(pair.0)
let result =
en_properties_schema(
pair.1,
False,
PropertyDataDirect,
name,
Some(#(tag, pair.0)),
)
let props =
list.append(
list.map({ pair.1 }.properties, fn(p) { p.0 }),
list.map({ pair.1 }.optional_properties, fn(p) { p.0 }),
)
use Out(src:, ..) <- result.map(result)
#(pair.0, src, list.sort(props, string.compare))
}),
)
let clauses =
list.map(properties, fn(pair) {
let name = name <> justin.pascal_case(pair.0)
let args = case pair.2 {
[] -> ""
a -> "(" <> string.join(a, ":, ") <> ":)"
}
" " <> name <> args <> " -> " <> pair.1
})
let src =
"case "
<> option.unwrap(data, "data")
<> " {\n"
<> string.join(clauses, "\n")
<> "\n }"
let out = case nullable {
True -> {
let type_name = "option.Option(" <> name <> ")"
let data = option.unwrap(data, "data")
let src = "case " <> data <> " {
option.Some(data) -> " <> src <> "
option.None -> json.null()
}"
Out(src:, type_name:)
}
False -> Out(src:, type_name: name)
}
case data {
None -> Ok(Out(..out, src: "fn(data) { " <> out.src <> " }"))
Some(_) -> Ok(out)
}
}
fn en_properties_schema(
schema: PropertiesSchema,
nullable: Bool,
data: PropertyDataName,
name: String,
extra: Option(#(String, String)),
) -> Result(Out, CodegenError) {
let PropertiesSchema(
properties:,
optional_properties:,
additional_properties: _,
) = schema
let property_data = fn(field_name) {
let field_name = justin.snake_case(field_name)
case data {
PropertyDataDirect -> field_name
PropertyDataAccess(name) if !nullable -> name <> "." <> field_name
_ -> "data." <> field_name
}
}
use properties <- result.try(
list.try_map(properties, fn(p) {
let name = name <> justin.pascal_case(p.0)
let data = property_data(p.0)
use out <- result.map(en_schema(p.1, Some(data), name))
#(p.0, out.src)
}),
)
let properties =
case extra {
None -> properties
Some(#(k, v)) -> [#(k, "json.string(\"" <> v <> "\")"), ..properties]
}
|> list.map(fn(p) { "\n #(\"" <> p.0 <> "\", " <> p.1 <> ")," })
use optionals <- result.map(
list.try_map(optional_properties, fn(p) {
let n = name <> justin.pascal_case(p.0)
let d = property_data(p.0)
use Out(src: s, ..) <- result.map(en_schema(p.1, None, name))
" |> helper__optional_property("
<> d
<> ", \""
<> n
<> "\", "
<> s
<> ")"
}),
)
let src = case properties {
[] -> "[]"
p -> "[" <> string.concat(p) <> "\n ]"
}
let src = case optional_properties {
[] -> "json.object(" <> src <> ")"
_ -> {
src <> "\n" <> string.join(optionals, "\n") <> "\n |> json.object"
}
}
let src = case nullable {
True -> {
let data = case data {
PropertyDataAccess(name) -> name
PropertyDataDirect | PropertyDataNone -> "data"
}
"case " <> data <> " {
option.Some(data) -> " <> src <> "
option.None -> json.null()
}"
}
False -> src
}
let src = case data {
PropertyDataNone -> "fn(data) { " <> src <> " }"
_ -> src
}
let type_name = case nullable {
True -> "option.Option(" <> name <> ")"
False -> name
}
Out(src:, type_name:)
}
fn en_values(
schema: Schema,
nullable: Bool,
data: Option(String),
position_name: String,
) -> Result(Out, CodegenError) {
use Out(src:, type_name:) <- result.map(en_schema(schema, None, position_name))
let type_name = "dict.Dict(String, " <> type_name <> ")"
let data = option.unwrap(data, "_")
case nullable {
False -> {
let src = "helper__dict_to_json(" <> data <> ", " <> src <> ")"
Out(src:, type_name:)
}
True -> {
let type_name = "option.Option(" <> type_name <> ")"
let src = "helper__dict_to_json(_, " <> src <> ")"
let src = "json.nullable(" <> data <> ", " <> src <> ")"
Out(src:, type_name:)
}
}
}
fn en_enum(
variants: List(String),
nullable: Bool,
data: Option(String),
position_name: String,
) -> Result(Out, CodegenError) {
let type_name = position_name
let src = "json.string(case " <> option.unwrap(data, "data") <> " {\n"
let variants =
variants
|> list.map(fn(v) {
" " <> justin.pascal_case(v) <> " -> \"" <> v <> "\"\n"
})
|> string.concat
let src = src <> variants <> " })"
let src = case nullable || data == None {
True -> "fn(data) { " <> src <> " }"
False -> src
}
let out = case nullable {
True -> {
let type_name = "option.Option(" <> type_name <> ")"
let src = case data {
Some(data) -> "json.nullable(" <> data <> ", " <> src <> ")"
None -> "json.nullable(_, " <> src <> ")"
}
Out(src:, type_name:)
}
False -> Out(src:, type_name:)
}
Ok(out)
}
fn de_properties_schema(
schema: PropertiesSchema,
nullable: Bool,
name: String,
) -> Result(Out, CodegenError) {
let PropertiesSchema(
properties:,
optional_properties:,
additional_properties: _,
) = schema
let properties =
list.append(
list.map(properties, fn(p) { #(p.0, p.1, False) }),
list.map(optional_properties, fn(p) { #(p.0, p.1, True) }),
)
|> list.sort(fn(a, b) { string.compare(a.0, b.0) })
use properties <- result.try(
list.try_map(properties, fn(prop) {
use s <- result.map(de_schema(prop.1, name <> justin.pascal_case(prop.0)))
#(prop.0, s, prop.2)
}),
)
let params =
properties
|> list.map(fn(n) {
let name = justin.snake_case(n.0)
" use " <> name <> " <- decode.parameter"
})
|> string.join("\n")
let fields =
properties
|> list.map(fn(p) {
let field = case p.2 {
True -> " |> decode.optional_field(\""
False -> " |> decode.field(\""
}
field <> p.0 <> "\", " <> { p.1 }.src <> ")"
})
|> string.join("\n")
let keys =
properties
|> list.map(fn(n) { justin.snake_case(n.0) <> ":" })
|> string.join(", ")
let src = "decode.into({
" <> params <> "
" <> name <> "(" <> keys <> ")
})
" <> fields
Ok(de_nullable(src, name, nullable))
}
fn en_elements(
schema: Schema,
nullable: Bool,
data: Option(String),
position_name: String,
) -> Result(Out, CodegenError) {
use Out(src:, type_name:) <- result.map(en_schema(schema, None, position_name))
let type_name = "List(" <> type_name <> ")"
let data = option.unwrap(data, "_")
case nullable {
False -> {
let src = "json.array(" <> data <> ", " <> src <> ")"
Out(src:, type_name:)
}
True -> {
let type_name = "option.Option(" <> type_name <> ")"
let src = "json.array(_, " <> src <> ")"
let src = "json.nullable(" <> data <> ", " <> src <> ")"
Out(src:, type_name:)
}
}
}
fn de_enum(
variants: List(String),
nullable: Bool,
position_name: String,
) -> Result(Out, CodegenError) {
let type_name = position_name
let src =
"decode.then(decode.string, fn(s) {
case s {\n"
let variants =
list.map(variants, fn(v) {
" \"" <> v <> "\" -> decode.into(" <> justin.pascal_case(v) <> ")\n"
})
let src = src <> string.concat(variants)
let src = src <> " _ -> decode.fail(\"" <> type_name <> "\")\n"
let src = src <> " }\n })"
Ok(de_nullable(src, type_name, nullable))
}
fn de_values(
schema: Schema,
nullable: Bool,
position_name: String,
) -> Result(Out, CodegenError) {
use Out(src:, type_name:) <- result.map(de_schema(schema, position_name))
let type_name = "dict.Dict(String, " <> type_name <> ")"
let src = "decode.dict(decode.string, " <> src <> ")"
de_nullable(src, type_name, nullable)
}
fn de_elements(
schema: Schema,
nullable: Bool,
position_name: String,
) -> Result(Out, CodegenError) {
use Out(src:, type_name:) <- result.map(de_schema(schema, position_name))
let type_name = "List(" <> type_name <> ")"
let src = "decode.list(" <> src <> ")"
de_nullable(src, type_name, nullable)
}
fn de_type(t: Type, nullable: Bool) -> Out {
let #(src, type_name) = case t {
Boolean -> #("decode.bool", "Bool")
Float32 | Float64 -> #("decode.float", "Float")
String | Timestamp -> #("decode.string", "String")
Int16 | Int32 | Int8 | Uint16 | Uint32 | Uint8 -> #("decode.int", "Int")
}
de_nullable(src, type_name, nullable)
}
fn en_type(t: Type, nullable: Bool, data: Option(String)) -> Out {
let #(src, type_name) = case t {
Boolean -> #("json.bool", "Bool")
Float32 | Float64 -> #("json.float", "Float")
String | Timestamp -> #("json.string", "String")
Int16 | Int32 | Int8 | Uint16 | Uint32 | Uint8 -> #("json.int", "Int")
}
en_nullable(src, type_name, nullable, data)
}
fn en_nullable(
src: String,
type_name: String,
nullable: Bool,
data: Option(String),
) -> Out {
case nullable {
True -> {
let type_name = "option.Option(" <> type_name <> ")"
let src = case data {
Some(data) -> "json.nullable(" <> data <> ", " <> src <> ")"
None -> "json.nullable(_, " <> src <> ")"
}
Out(src:, type_name:)
}
False -> {
let src = case data {
Some(data) -> src <> "(" <> data <> ")"
None -> src
}
Out(src:, type_name:)
}
}
}
fn de_nullable(src: String, type_name: String, nullable: Bool) -> Out {
case nullable {
True -> {
let type_name = "option.Option(" <> type_name <> ")"
let src = "decode.optional(" <> src <> ")"
Out(src:, type_name:)
}
False -> Out(src:, type_name:)
}
}