Current section
Files
Jump to
Current section
Files
src/graded/internal/config.gleam
// Per-package graded configuration, parsed from `gleam.toml`. Each Gleam
// package can declare:
//
// ```toml
// [tools.graded]
// spec_file = "myapp.graded" # default: "<name>.graded"
// cache_dir = "build/.graded" # default: "build/.graded"
// ```
//
// Both fields are optional. If `[tools.graded]` is missing entirely, both
// defaults apply. The `name` field at the top of `gleam.toml` provides the
// package name used to derive the default `spec_file`.
import filepath
import gleam/result
import gleam/string
import simplifile
import tom
// Resolved per-package configuration. Use `read` to load it from a
// `gleam.toml` path; use `defaults_for` to construct one without IO when
// the package name is already known (for tests or in-memory fixtures).
pub type GradedConfig {
GradedConfig(
// Package name as declared in `gleam.toml`'s top-level `name` field.
package_name: String,
// Path to the package's spec file, relative to the package root.
// Contains the public-API effects, `check` invariants, `external`
// hints, and `type` field annotations that ship to consumers.
spec_file: String,
// Path to the per-module cache directory, relative to the package
// root. Contains all-function inferred effects, regenerated by
// `graded infer`. Should be inside `build/` so it's gitignored.
cache_dir: String,
)
}
pub type ConfigError {
// `gleam.toml` could not be read.
TomlReadError(path: String, cause: simplifile.FileError)
// `gleam.toml` parsed but `name` field is missing or not a string.
MissingPackageName(path: String)
// `gleam.toml` could not be parsed as valid TOML.
TomlParseError(path: String)
}
// Read configuration for the project at `<directory>/gleam.toml`. Falls
// back to defaults for any field not present in `[tools.graded]`.
pub fn read(gleam_toml_path: String) -> Result(GradedConfig, ConfigError) {
use content <- result.try(
simplifile.read(gleam_toml_path)
|> result.map_error(TomlReadError(gleam_toml_path, _)),
)
use toml <- result.try(
tom.parse(content)
|> result.replace_error(TomlParseError(gleam_toml_path)),
)
use package_name <- result.try(
tom.get_string(toml, ["name"])
|> result.replace_error(MissingPackageName(gleam_toml_path)),
)
let spec_file = case tom.get_string(toml, ["tools", "graded", "spec_file"]) {
Ok(value) -> value
Error(_) -> default_spec_file(package_name)
}
let cache_dir = case tom.get_string(toml, ["tools", "graded", "cache_dir"]) {
Ok(value) -> value
Error(_) -> default_cache_dir()
}
Ok(GradedConfig(package_name:, spec_file:, cache_dir:))
}
// Build a config using all defaults for a known package name. Useful in
// tests where there's no real `gleam.toml` to read.
pub fn defaults_for(package_name: String) -> GradedConfig {
GradedConfig(
package_name:,
spec_file: default_spec_file(package_name),
cache_dir: default_cache_dir(),
)
}
pub fn default_spec_file(package_name: String) -> String {
package_name <> ".graded"
}
pub fn default_cache_dir() -> String {
"build/.graded"
}
// Resolve the full path to a package's spec file given its root directory and
// package name. Reads the package's own `[tools.graded].spec_file`, falling
// back to `<package_name>.graded` when the config is missing or unreadable.
// Single source of truth for "where does package X keep its spec file".
pub fn spec_file_for(package_root: String, package_name: String) -> String {
let spec_file = case read(filepath.join(package_root, "gleam.toml")) {
Ok(cfg) -> cfg.spec_file
Error(_) -> default_spec_file(package_name)
}
filepath.join(package_root, spec_file)
}
// Compute the dotted module name (as it appears in `import` statements) for a
// `.gleam` file under a given source directory. For example,
// `module_path_for_source("src/app/router.gleam", "src")` returns
// `"app/router"` — the same string `import` statements use, so the project's
// dependency graph can be built by intersecting the two.
pub fn module_path_for_source(
gleam_path: String,
source_directory: String,
) -> String {
let prefix = source_directory <> "/"
let relative = case string.starts_with(gleam_path, prefix) {
True -> string.drop_start(gleam_path, string.length(prefix))
False -> gleam_path
}
filepath.strip_extension(relative)
}