Skip to main content

alba_core/
model.rs

1//! The typed project model that `alba-core` evaluates a Beamfile's AST
2//! into: [`Project`], its [`Beam`]s, and the small value types they carry.
3//!
4//! Scope boundary: a [`Beam`]'s `needs` list is converted straight from
5//! the AST's `BeamRef`s (namespaced by [`crate::loader::load_project`]
6//! when it came from an `import`) without validating that the referenced
7//! beam actually exists — that is [`crate::validate_graph`]'s job, not
8//! this module's.
9
10use std::path::PathBuf;
11
12use alba_syntax::{Span, Spanned, StringTemplate};
13
14use crate::eval::Scope;
15
16/// Identifies which source file a [`Beam`] or [`crate::CoreError`] came
17/// from, for pointing diagnostics at the right source text. Assigned by
18/// [`crate::loader::load_project`] (the root file is always `SourceId(0)`,
19/// each import gets the next one in load order) and resolvable back to a
20/// path and source text via [`crate::loader::SourceMap::get`].
21/// `load_str`'s single-file loading always produces `SourceId(0)`.
22#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
23pub struct SourceId(pub usize);
24
25/// A fully namespaced beam identifier, e.g. `"api:build"` for a beam
26/// imported under the alias `api`, `"api:db:migrate"` for one imported
27/// transitively through a chain of aliases, or `"build"` for a local one.
28#[derive(Debug, Clone, PartialEq, Eq, Hash)]
29pub struct BeamId(pub String);
30
31/// How a beam's commands run.
32#[derive(Debug, Clone, PartialEq)]
33pub enum ExecutorKind {
34    /// The default, when a beam declares no `executor` field at all.
35    /// Which shell implementation actually runs a beam is the engine's
36    /// concern, not this model's.
37    Shell,
38    /// `executor system_shell`: the host shell, the per-beam opt-out from
39    /// the embedded shell.
40    SystemShell,
41    /// `executor docker { image "..." volumes [...] workdir "..." }`: the
42    /// engine runs this beam's commands inside a container built from
43    /// `image`, with `volumes` bind-mounted (`host:container` each) and
44    /// `workdir` as the container's working directory when set.
45    Docker {
46        image: String,
47        volumes: Vec<String>,
48        workdir: Option<String>,
49    },
50    /// `executor <name> { ... }` for any name that is not `shell`,
51    /// `system_shell`, or `docker`. Not an error: `name` is resolved at
52    /// plan time to an external `alba-executor-<name>` binary on the
53    /// `PATH`, and `options` is handed to it verbatim.
54    Plugin {
55        name: String,
56        options: Vec<(String, OptionValue)>,
57    },
58}
59
60/// The value half of an executor option, once rendered (its `{expr}`
61/// interpolation resolved, for the `Str`/`List` cases) at load time. Mirrors
62/// [`alba_syntax::ExecutorOptionValue`], minus the interpolation itself.
63#[derive(Debug, Clone, PartialEq)]
64pub enum OptionValue {
65    Str(String),
66    Bool(bool),
67    List(Vec<String>),
68}
69
70/// A value produced by evaluating an [`alba_syntax::Expr`]: either of the
71/// DSL's two runtime types.
72#[derive(Debug, Clone, PartialEq)]
73pub enum Value {
74    Str(String),
75    Bool(bool),
76}
77
78impl Value {
79    /// A short, human-readable name for this value's type, for error
80    /// messages ("expected a boolean, found a string").
81    pub(crate) fn type_name(&self) -> &'static str {
82        match self {
83            Value::Str(_) => "a string",
84            Value::Bool(_) => "a boolean",
85        }
86    }
87
88    /// This value as it appears when interpolated into a template:
89    /// strings verbatim, booleans as `true`/`false`.
90    pub(crate) fn as_display(&self) -> &str {
91        match self {
92            Value::Str(s) => s.as_str(),
93            Value::Bool(true) => "true",
94            Value::Bool(false) => "false",
95        }
96    }
97}
98
99/// A single beam: the unit of work produced by evaluating a `beam { ... }`
100/// declaration.
101///
102/// Load-time vs. schedule-time rendering: `description`, `inputs`,
103/// `outputs`, `cwd`, and the executor's options are rendered once, at load
104/// time, into plain values — referencing a beam parameter there is a load
105/// error. `run` and `env` are kept as [`StringTemplate`]s and rendered at
106/// schedule time (via [`crate::render_template`]), once the beam's
107/// parameters are bound into a [`Scope`] built from [`Beam::scope`] via
108/// [`Scope::with_params`].
109#[derive(Debug, Clone)]
110pub struct Beam {
111    pub id: BeamId,
112    pub description: Option<String>,
113    /// Each entry keeps the span of the reference that produced it, so a
114    /// `needs` entry that does not resolve is underlined exactly, rather
115    /// than the whole beam declaration containing it.
116    pub needs: Vec<Spanned<BeamId>>,
117    pub params: Vec<String>,
118    /// Evaluated but inert in this crate: the cache (out of scope here) is
119    /// the only intended consumer.
120    pub inputs: Vec<String>,
121    pub outputs: Vec<String>,
122    pub run: Vec<StringTemplate>,
123    pub env: Vec<(String, StringTemplate)>,
124    pub cwd: Option<String>,
125    pub executor: ExecutorKind,
126    pub allow_failure: bool,
127    /// The directory of the defining Beamfile, used as the `cwd` base.
128    /// Always absolute for a project built by `crate::loader::load_project`
129    /// (it derives this via `std::path::absolute`, not the file's
130    /// possibly-relative path, so it stays correct even if the process's
131    /// current directory changes between loading and running) — except for
132    /// `load_str`'s single-file front door, which has no real file on disk
133    /// to resolve against and uses `"."` (see its doc comment).
134    pub dir: PathBuf,
135    pub span: Span,
136    /// Which source file this beam was defined in, for diagnostics.
137    pub source: SourceId,
138    /// The defining file's evaluated file-level `let` bindings. The
139    /// engine composes this with the beam's own parameter values at
140    /// schedule time — `beam.scope.with_params(&beam.params, &args)` —
141    /// before rendering `run`/`env`.
142    pub scope: Scope,
143}
144
145/// A git hook bound to a beam: `hook pre-commit { beam check }`. Only the
146/// root Beamfile's hooks make it here; an import's are ignored, as its
147/// `default` is.
148#[derive(Debug, Clone, PartialEq)]
149pub struct Hook {
150    /// The git hook name (`pre-commit`), already checked against
151    /// [`crate::git::KNOWN_HOOKS`].
152    pub name: String,
153    /// The beam the hook runs, with the span of the reference that named
154    /// it, for pointing at an unresolved one.
155    pub beam: Spanned<BeamId>,
156    /// How many arguments git passes to this hook; the beam may declare at
157    /// most that many parameters.
158    pub arity: usize,
159    pub span: Span,
160    pub source: SourceId,
161}
162
163/// A fully evaluated Beamfile (single-file loading only; see the module
164/// doc comment): every beam it declares, and which one runs by default.
165#[derive(Debug, Clone)]
166pub struct Project {
167    pub beams: Vec<Beam>,
168    /// The beam a bare `alba` runs, with the span of the `default`
169    /// declaration that named it — [`crate::validate_graph`] checks it
170    /// resolves and points at that span when it does not.
171    pub default: Option<Spanned<BeamId>>,
172    /// The root Beamfile's hook declarations, in source order.
173    pub hooks: Vec<Hook>,
174}