Current section

Files

Jump to
gladvent src gladvent internal runners.gleam
Raw

src/gladvent/internal/runners.gleam

import filepath
import gladvent/internal/cmd
import gladvent/internal/parse.{type Day}
import gladvent/internal/util.{defer}
import gleam
import gleam/bool
import gleam/dict
import gleam/dynamic.{type Dynamic}
import gleam/erlang/atom
import gleam/int
import gleam/json
import gleam/list
import gleam/option.{type Option}
import gleam/package_interface
import gleam/result
import gleam/string
import shellout
import simplifile
import snag.{type Result}
import spinner
import tom
pub type PartRunner =
fn(Dynamic) -> Dynamic
pub type DayRunner =
#(PartRunner, PartRunner, Option(fn(String) -> Dynamic))
const package_interface_path = "build/.gladvent/pkg.json"
type PkgInterfaceErr {
FailedToGleamBuild(String)
FailedToReadGleamToml(simplifile.FileError)
FailedToDecodeGleamToml(tom.ParseError)
FailedToGetPackageName(tom.GetError)
FailedToGeneratePackageInterface(String)
FailedToReadPackageInterface(simplifile.FileError)
FailedToClearBuildCache(simplifile.FileError)
FailedToDecodePackageInterface(json.DecodeError)
}
fn package_interface_error_to_snag(e: PkgInterfaceErr) -> snag.Snag {
case e {
FailedToGleamBuild(s) ->
snag.new(s)
|> snag.layer("failed to build gleam project")
FailedToDecodeGleamToml(e) ->
snag.new(string.inspect(e))
|> snag.layer("failed to decode gleam.toml")
FailedToGetPackageName(e) ->
snag.new(string.inspect(e))
|> snag.layer("failed to get package name")
FailedToReadGleamToml(e) ->
snag.new(string.inspect(e))
|> snag.layer("failed to read gleam.toml")
FailedToClearBuildCache(e) ->
snag.new(string.inspect(e))
|> snag.layer("failed to clear build cache")
FailedToGeneratePackageInterface(s) ->
snag.new(s)
|> snag.layer("failed to generate " <> package_interface_path)
FailedToReadPackageInterface(e) ->
snag.new(string.inspect(e))
|> snag.layer("failed to read " <> package_interface_path)
FailedToDecodePackageInterface(e) ->
snag.new(string.inspect(e))
|> snag.layer("failed to decode package interface json")
}
}
pub fn pkg_interface() -> Result(package_interface.Package) {
use <- snagify_error(with: package_interface_error_to_snag)
let spinner =
spinner.new("initializing package interface")
|> spinner.start()
use <- defer(do: fn() { spinner.stop(spinner) })
let root = cmd.root()
spinner.set_text(spinner, "reading gleam.toml")
use gleam_toml <- result.try(
simplifile.read(filepath.join(root, "gleam.toml"))
|> result.map_error(FailedToReadGleamToml),
)
spinner.set_text(spinner, "decoding gleam.toml")
use toml <- result.try(
tom.parse(gleam_toml)
|> result.map_error(FailedToDecodeGleamToml),
)
spinner.set_text(spinner, "fetching project name")
use name <- result.try(
tom.get_string(toml, ["name"])
|> result.map_error(FailedToGetPackageName),
)
spinner.set_text(spinner, "clearing build cache")
use _ <- result.try(
["build/prod/erlang", "build/dev/erlang"]
|> list.try_map(fn(cache) {
filepath.join(root, cache)
|> filepath.join(name)
|> simplifile.delete
})
|> result.try_recover(fn(e) {
case e {
simplifile.Enoent -> Ok([])
_ -> Error(FailedToClearBuildCache(e))
}
}),
)
spinner.set_text(spinner, "rebuilding project")
use _ <- result.try(
shellout.command("gleam", ["build"], root, [])
|> result.map_error(fn(e) { FailedToGeneratePackageInterface(e.1) }),
)
spinner.set_text(spinner, "generating package interface file")
use _ <- result.try(
shellout.command(
"gleam",
["export", "package-interface", "--out", package_interface_path],
root,
[],
)
|> result.map_error(fn(e) { FailedToGeneratePackageInterface(e.1) }),
)
spinner.set_text(spinner, "reading " <> package_interface_path)
use pkg_interface_contents <- result.try(
simplifile.read(filepath.join(cmd.root(), package_interface_path))
|> result.map_error(FailedToReadPackageInterface),
)
spinner.set_text(spinner, "decoding package interface JSON")
use pkg_interface_details <- result.try(
json.decode(from: pkg_interface_contents, using: package_interface.decoder)
|> result.map_error(FailedToDecodePackageInterface),
)
Ok(pkg_interface_details)
}
pub type RunnerRetrievalErr {
ModuleNotFound(String)
ParseFunctionInvalid(String)
FunctionNotFound(module: String, function: String)
IncorrectInputParameters(
function: String,
expected: String,
got: List(package_interface.Type),
)
}
pub fn runner_retrieval_error_to_snag(e: RunnerRetrievalErr) -> snag.Snag {
case e {
ModuleNotFound(m) -> snag.new("module " <> m <> " not found")
ParseFunctionInvalid(f) ->
snag.new(f)
|> snag.layer("parse function invalid")
FunctionNotFound(m, f) ->
snag.new("module " <> m <> " does not export function " <> f)
IncorrectInputParameters(f, e, g) ->
{
"function '"
<> f
<> "' has parameter(s) "
<> type_list_to_string(g)
<> ", but should only have one parameter and it must be of type "
<> e
}
|> snag.new
}
}
fn snagify_error(
do f: fn() -> gleam.Result(out, err),
with m: fn(err) -> snag.Snag,
) -> Result(out) {
f()
|> result.map_error(m)
}
pub fn get_day(
package: package_interface.Package,
year: Int,
day: Day,
) -> Result(DayRunner) {
use <- snagify_error(with: runner_retrieval_error_to_snag)
let module_name =
"aoc_" <> int.to_string(year) <> "/day_" <> int.to_string(day)
// get the module for the specified year + day
use module <- result.try(
dict.get(package.modules, module_name)
|> result.replace_error(ModuleNotFound(module_name)),
)
// get the optional parse function
let parse = dict.get(module.functions, "parse")
use runner_param_type <- result.try(case parse {
Error(Nil) -> Ok(string)
Ok(package_interface.Function(parameters: [param], return: return, ..)) if param.type_
== string -> Ok(return)
_ ->
Error(ParseFunctionInvalid(
"parse function must have 1 input parameter of type String",
))
})
let retrieve_runner = retrieve_runner(
module_name,
module,
_,
runner_param_type,
)
use pt_1 <- result.try(retrieve_runner("pt_1"))
use pt_2 <- result.try(retrieve_runner("pt_2"))
Ok(#(
pt_1,
pt_2,
parse
|> result.replace(parse_function(module_name))
|> option.from_result,
))
}
fn retrieve_runner(
module_name: String,
module: package_interface.Module,
function_name: String,
runner_param_type: package_interface.Type,
) -> gleam.Result(fn(Dynamic) -> Dynamic, RunnerRetrievalErr) {
use pt_1 <- result.try(
module.functions
|> dict.get(function_name)
|> result.replace_error(FunctionNotFound(module_name, function_name)),
)
use <- bool.guard(
when: case pt_1.parameters {
[param] -> param.type_ != runner_param_type
_ -> True
},
return: Error(IncorrectInputParameters(
function: function_name,
expected: type_to_string(runner_param_type),
got: list.map(pt_1.parameters, fn(p) { p.type_ }),
)),
)
Ok(function_arity_one(
atom.create_from_string(to_erlang_module_name(module_name)),
atom.create_from_string(function_name),
))
}
fn to_erlang_module_name(name) {
string.replace(name, "/", "@")
}
@external(erlang, "gladvent_ffi", "function_arity_one")
fn function_arity_one(
module: atom.Atom,
function: atom.Atom,
) -> fn(Dynamic) -> Dynamic
fn parse_function(module: String) -> fn(String) -> Dynamic {
do_parse_function(atom.create_from_string(to_erlang_module_name(module)))
}
@external(erlang, "gladvent_ffi", "parse_function")
fn do_parse_function(module: atom.Atom) -> fn(String) -> Dynamic
const string = package_interface.Named(
name: "String",
module: "gleam",
package: "",
parameters: [],
)
fn type_to_string(t: package_interface.Type) -> String {
case t {
package_interface.Tuple(elements: elements) ->
"#(" <> type_list_to_string(elements) <> ")"
package_interface.Fn(parameters: parameters, return: return) ->
"fn("
<> type_list_to_string(parameters)
<> ") -> "
<> type_to_string(return)
package_interface.Variable(id: id) -> int.to_string(id)
package_interface.Named(
name: name,
package: _,
module: module,
parameters: parameters,
) ->
case parameters {
[] -> module <> "." <> name
_ ->
module <> "." <> name <> "(" <> type_list_to_string(parameters) <> ")"
}
}
}
fn type_list_to_string(lt: List(package_interface.Type)) -> String {
lt
|> list.map(type_to_string)
|> string.join(", ")
}