///|
pub fn load_project(
root? : String = ".",
config_path? : String = "Moonforge.toml",
) -> Project raise MoonforgeError {
let root = normalize_path(root)
let config_path = resolve_config_path(root, config_path)
guard @fs.path_exists(config_path) else {
config_error("config file not found: \{config_path}")
}
let source = read_text_file(config_path)
let parsed = @toml.parse(source) catch {
err =>
config_error("failed to parse \{config_path}: \{@debug.to_string(err)}")
}
let tasks = parse_tasks(parsed)
{ root, config_path, tasks }
}
///|
fn resolve_config_path(root : String, config_path : String) -> String {
let normalized_root = normalize_path(root)
let normalized_config = normalize_path(config_path)
if normalized_config.has_prefix("/") || normalized_config.contains(":/") {
normalized_config
} else if normalized_root != "." &&
(
normalized_config == normalized_root ||
normalized_config.has_prefix("\{normalized_root}/")
) {
normalized_config
} else {
join_path(normalized_root, normalized_config)
}
}
///|
fn parse_tasks(
value : @toml.TomlValue,
) -> Map[String, Task] raise MoonforgeError {
guard value is @toml.TomlTable(root) else {
config_error("Moonforge config must be a TOML table")
}
let tasks_value = root.get("tasks")
guard tasks_value is Some(@toml.TomlTable(tasks_table)) else {
config_error("Moonforge config must define a [tasks] table")
}
let tasks : Map[String, Task] = Map([])
for name, task_value in tasks_table {
tasks[name] = parse_task(name, task_value)
}
guard !tasks.is_empty() else {
config_error("Moonforge config must define at least one task")
}
tasks
}
///|
fn parse_task(
name : String,
value : @toml.TomlValue,
) -> Task raise MoonforgeError {
guard value is @toml.TomlTable(table) else {
config_error("task '\{name}' must be a TOML table")
}
let cmd = required_string_field(table, task_name=name, field="cmd")
let deps = string_array_field(table, task_name=name, field="deps")
let inputs = string_array_field(table, task_name=name, field="inputs")
let outputs = string_array_field(table, task_name=name, field="outputs")
let phony = bool_field(table, task_name=name, field="phony", default=false)
let desc = optional_string_field(table, task_name=name, field="desc")
{
name,
cmd,
deps: deps.map(normalize_path),
inputs: inputs.map(normalize_path),
outputs: outputs.map(normalize_path),
phony,
desc,
}
}
///|
fn required_string_field(
table : Map[String, @toml.TomlValue],
task_name~ : String,
field~ : String,
) -> String raise MoonforgeError {
match table.get(field) {
Some(@toml.TomlString(value)) => value
Some(_) =>
config_error("task '\{task_name}' field '\{field}' must be a string")
None => config_error("task '\{task_name}' is missing '\{field}'")
}
}
///|
fn optional_string_field(
table : Map[String, @toml.TomlValue],
task_name~ : String,
field~ : String,
) -> String? raise MoonforgeError {
match table.get(field) {
Some(@toml.TomlString(value)) => Some(value)
Some(_) =>
config_error("task '\{task_name}' field '\{field}' must be a string")
None => None
}
}
///|
fn bool_field(
table : Map[String, @toml.TomlValue],
task_name~ : String,
field~ : String,
default~ : Bool,
) -> Bool raise MoonforgeError {
match table.get(field) {
Some(@toml.TomlBoolean(value)) => value
Some(_) =>
config_error("task '\{task_name}' field '\{field}' must be a boolean")
None => default
}
}
///|
fn string_array_field(
table : Map[String, @toml.TomlValue],
task_name~ : String,
field~ : String,
) -> Array[String] raise MoonforgeError {
match table.get(field) {
None => []
Some(@toml.TomlArray(items)) => {
let result = []
for item in items {
match item {
@toml.TomlString(value) => result.push(value)
_ =>
config_error(
"task '\{task_name}' field '\{field}' must contain strings only",
)
}
}
result
}
Some(_) =>
config_error("task '\{task_name}' field '\{field}' must be an array")
}
}