Current section

Files

Jump to
glyph_codegen src codegen codegen.gleam
Raw

src/codegen/codegen.gleam

import gleam/list
import gleam/int
import gleam/io
import gleam/dict
import gleam/string
import gleam/option.{type Option, None, Some}
import argv
import gleam/bbmustache as templ
import simplifile
import shellout
import glance
import justin
const imports_template = "
import gleam/dynamic
import gleam/list
import {{module}}
fn all_errors(result: Result(a, dynamic.DecodeErrors)) -> dynamic.DecodeErrors {
case result {
Ok(_) -> []
Error(errors) -> errors
}
}
"
const decoder_template = "
pub fn decode_{{type_snake_case}}(
dyn: dynamic.Dynamic,
) -> Result(discord.{{type_name}}, dynamic.DecodeErrors) {
case
{{#fields}}{{func_name}}(\"{{key}}\", {{value}})(dyn){{comma}}{{/fields}}
{
{{#fields}}Ok({{index}}){{comma}}{{/fields}} ->
Ok(discord.{{type_name}}({{#fields}}{{index}}{{comma}}{{/fields}}
))
{{#fields}}{{index}}{{comma}}{{/fields}} ->
Error(list.concat([
{{#fields}}all_errors({{index}}){{comma}}{{/fields}}
]))
}
}
"
pub fn main() {
case simplifile.verify_is_file("./gleam.toml") {
Ok(exists) if exists == True -> Nil
_ -> panic as "not in project root"
}
let assert Args(..) as args = parse_args(argv.load().arguments)
let assert Ok(source) =
simplifile.read("../src/" <> args.import_module <> ".gleam")
let assert Ok(module) = glance.module(source)
let output_path = args.out_file
case simplifile.delete(output_path) {
Ok(_) -> Nil
Error(simplifile.Enoent) -> Nil
_ -> panic as "cannot delete a old file"
}
let rendered =
do_template(imports_template, [
#("module", templ.string(args.import_module)),
])
let assert Ok(_) = simplifile.write(to: output_path, contents: rendered)
list.each(args.types, fn(type_name) {
io.debug("looking for the type: " <> type_name)
let assert Ok(found) =
module.custom_types
|> list.find(fn(x) { x.definition.name == type_name })
let assert Ok(first_variant) =
found.definition.variants
|> list.at(0)
let fields_decoders = get_fields(first_variant.fields)
let rendered = generate(type_name, fields_decoders)
let assert Ok(_) = simplifile.append(to: output_path, contents: rendered)
})
io.debug("formatting...")
let assert Ok(_) =
shellout.command(run: "gleam", in: ".", with: ["format", output_path], opt: [
shellout.LetBeStdout,
])
}
fn get_fields(fields: List(glance.Field(glance.Type))) -> List(FieldDecoder) {
case fields {
[field, ..fields] -> {
[convert_type_to_decoder(field), ..get_fields(fields)]
}
[] -> []
}
}
pub type FieldDecoder {
Field(name: String, arg: String)
OptionalField(name: String, arg: String)
}
fn convert_type_to_decoder(field: glance.Field(glance.Type)) -> FieldDecoder {
let id = case field.label {
Some(id) -> id
_ -> panic as "unknown id"
}
io.debug("parsing field: " <> id)
io.debug(field)
case field.item {
// List
glance.NamedType(
"List",
module: _,
parameters: [glance.NamedType("String", module: _, parameters: [])],
) -> Field(id, "dynamic.list(dynamic.string)")
glance.NamedType(
"List",
module: _,
parameters: [glance.NamedType("Int", module: _, parameters: [])],
) -> Field(id, "dynamic.list(dynamic.int)")
glance.NamedType(
"List",
module: _,
parameters: [glance.NamedType("Snowflake", module: _, parameters: [])],
) -> Field(id, "dynamic.list(dynamic.string)")
glance.NamedType(
"List",
module: _,
parameters: [glance.NamedType(name, module: _, parameters: [])],
) if name != "String" && name != "Int" && name != "Bool" ->
Field(id, format("dynamic.list(decode_%)", [justin.snake_case(name)]))
// Option
glance.NamedType(
"Option",
module: _,
parameters: [glance.NamedType("Float", module: _, parameters: [])],
) -> OptionalField(id, "dynamic.float")
glance.NamedType(
"Option",
module: _,
parameters: [glance.NamedType("Nil", module: _, parameters: [])],
) -> OptionalField(id, "dynamic.string")
glance.NamedType(
"Option",
module: _,
parameters: [glance.NamedType("Snowflake", module: _, parameters: [])],
) -> OptionalField(id, "dynamic.string")
glance.NamedType(
"Option",
module: _,
parameters: [glance.NamedType("String", module: _, parameters: [])],
) -> OptionalField(id, "dynamic.string")
glance.NamedType(
"Option",
module: _,
parameters: [glance.NamedType("Int", module: _, parameters: [])],
) -> OptionalField(id, "dynamic.int")
glance.NamedType(
"Option",
module: _,
parameters: [glance.NamedType("Bool", module: _, parameters: [])],
) -> OptionalField(id, "dynamic.bool")
glance.NamedType(
"Option",
module: _,
parameters: [glance.NamedType(name, module: _, parameters: [])],
) if name != "String" && name != "Int" && name != "Bool" ->
OptionalField(id, format("decode_%", [justin.snake_case(name)]))
// Option Dict
glance.NamedType(
"Option",
module: _,
parameters: [
glance.NamedType(
"Dict",
module: _,
parameters: [
glance.NamedType("Snowflake", module: _, parameters: []),
glance.NamedType(p2, module: _, parameters: []),
],
),
],
) ->
OptionalField(
id,
format("dynamic.dict(dynamic.string, decode_%)", [justin.snake_case(p2)]),
)
// Option(List)
glance.NamedType(
"Option",
module: _,
parameters: [
glance.NamedType(
"List",
module: _,
parameters: [glance.NamedType("Snowflake", module: _, parameters: [])],
),
],
) ->
OptionalField(
id,
"dynamic.list(dynamic.string)",
)
glance.NamedType(
"Option",
module: _,
parameters: [
glance.NamedType(
"List",
module: _,
parameters: [glance.NamedType(name, module: _, parameters: [])],
),
],
) if name != "String" && name != "Int" && name != "Bool" ->
OptionalField(
id,
format("dynamic.list(decode_%)", [justin.snake_case(name)]),
)
glance.NamedType(
"Option",
module: _,
parameters: [
glance.NamedType(
"List",
module: _,
parameters: [glance.NamedType("Int", module: _, parameters: [])],
),
],
) -> OptionalField(id, "dynamic.list(dynamic.int)")
glance.NamedType(
"Option",
module: _,
parameters: [
glance.NamedType(
"List",
module: _,
parameters: [glance.NamedType("String", module: _, parameters: [])],
),
],
) -> OptionalField(id, "dynamic.list(dynamic.string)")
// dynamic.Dynamic
glance.NamedType("dynamic.Dynamic", module: _, parameters: []) ->
Field(id, "dynamic.dynamic")
// String
glance.NamedType(name, module: _, parameters: []) if name == "Snowflake"
|| name == "String" -> Field(id, "dynamic.string")
// Int
glance.NamedType("Int", module: _, parameters: []) ->
Field(id, "dynamic.int")
glance.NamedType("Dynamic", module: _, parameters: []) ->
Field(id, "dynamic.dynamic")
glance.NamedType("Bool", module: _, parameters: []) ->
Field(id, "dynamic.bool")
glance.NamedType(name, module: _, parameters: []) ->
Field(id, format("decode_%", [justin.snake_case(name)]))
// Dict
glance.NamedType(
"Dict",
module: _,
parameters: [
glance.NamedType(p1, module: _, parameters: []),
glance.NamedType(p2, module: _, parameters: []),
],
) -> Field(id, format("dynamic.dict(dynamic.%, dynamic.%)", [
justin.snake_case(p1), justin.snake_case(p2)
]))
_ -> panic as "unsupported type"
}
}
type Args {
Args(
types: List(String),
out_file: String,
import_module: String,
rename: dict.Dict(String, String),
)
}
fn parse_args(arguments: List(String)) -> Args {
case arguments {
["--import-module", ..rest] -> {
let assert #([val], rest) = list.split(rest, 1)
let assert Args(..) as args = parse_args(rest)
Args(..args, import_module: val)
}
["--type", ..rest] -> {
let assert #([val], rest) = list.split(rest, 1)
let assert Args(..) as args = parse_args(rest)
Args(
..args,
types: args.types
|> list.prepend(val),
)
}
["--out-file", ..rest] -> {
let assert #([val], rest) = list.split(rest, 1)
let assert Args(..) as args = parse_args(rest)
Args(..args, out_file: val)
}
["--rename", ..rest] -> {
let assert #([v1, v2], rest) = list.split(rest, 2)
let assert Args(..) as args = parse_args(rest)
Args(
..args,
rename: args.rename
|> dict.insert(v1, v2),
)
}
[] -> {
Args(types: [], out_file: "", import_module: "", rename: dict.new())
}
_ -> panic as "parsing error"
}
}
fn generate(type_name, fields: List(FieldDecoder)) {
let fields =
fields
|> list.index_map(fn(x, idx) {
let comma = case idx + 1 == list.length(fields) {
True -> ""
_ -> ","
}
let #(func_name, name, arg) = case x {
Field(name, arg) -> #("dynamic.field", name, arg)
OptionalField(name, arg) -> #("dynamic.optional_field", name, arg)
}
templ.object([
#(
"index",
templ.string(
"a"
<> idx + 1
|> int.to_string,
),
),
#("func_name", templ.string(func_name)),
#("key", templ.string(name)),
#("value", templ.string(arg)),
#("comma", templ.string(comma)),
])
})
do_template(decoder_template, [
#("fields", templ.list(fields)),
#("type_name", templ.string(type_name)),
#("type_snake_case", templ.string(justin.snake_case(type_name))),
])
}
fn do_template(templ, args) -> String {
let assert Ok(compiled) = templ.compile(templ)
templ.render(compiled, args)
}
fn format(fmt: String, args: List(String)) -> String {
case string.split_once(fmt, "%") {
Ok(#(left, right)) -> {
case args {
[] -> ""
[e1] -> left <> e1 <> right
[e1, ..rest] -> left <> e1 <> format(right, rest)
}
}
Error(_) -> ""
}
}