Skip to main content

ergo_runtime/source/
mod.rs

1//! source
2//!
3//! Purpose:
4//! - Define kernel source primitive types, manifests, validation errors, and
5//!   registry helpers.
6//!
7//! Owns:
8//! - `SourceValidationError` as the typed registration/manifest failure surface
9//!   for source primitives.
10//! - Source type metadata and registry-facing source declarations.
11//!
12//! Does not own:
13//! - Catalog-level wrapper errors or product-facing diagnostics.
14//! - Host orchestration over validated source primitives.
15//!
16//! Connects to:
17//! - `catalog.rs`, which wraps source registration failures.
18//! - Source primitive implementations under `implementations/`.
19//!
20//! Safety notes:
21//! - `Display` renders the `ErrorInfo` summary plus rule id so higher layers can
22//!   chain source validation failures without inventing new wording.
23
24use std::collections::HashMap;
25
26use std::borrow::Cow;
27use std::fmt;
28
29use crate::common::{doc_anchor_for_rule, ErrorInfo, Phase, Value, ValueType};
30use crate::runtime::ExecutionContext;
31
32pub mod implementations;
33pub mod registry;
34
35#[derive(Debug, Clone, PartialEq)]
36pub enum SourceKind {
37    Source,
38}
39
40#[derive(Debug, Clone, PartialEq)]
41pub enum ParameterType {
42    Int,
43    Number,
44    Bool,
45    String,
46    Enum,
47}
48
49#[derive(Debug, Clone, PartialEq)]
50pub enum ParameterValue {
51    Int(i64),
52    Number(f64),
53    Bool(bool),
54    String(String),
55    Enum(String),
56}
57
58impl ParameterValue {
59    pub fn value_type(&self) -> ParameterType {
60        match self {
61            ParameterValue::Int(_) => ParameterType::Int,
62            ParameterValue::Number(_) => ParameterType::Number,
63            ParameterValue::Bool(_) => ParameterType::Bool,
64            ParameterValue::String(_) => ParameterType::String,
65            ParameterValue::Enum(_) => ParameterType::Enum,
66        }
67    }
68}
69
70#[derive(Debug, Clone, PartialEq)]
71pub enum Cadence {
72    Continuous,
73}
74
75#[derive(Debug, Clone, PartialEq)]
76pub struct InputSpec {
77    pub name: String,
78    pub value_type: ValueType,
79    pub required: bool,
80}
81
82#[derive(Debug, Clone, PartialEq)]
83pub struct OutputSpec {
84    pub name: String,
85    pub value_type: ValueType,
86}
87
88#[derive(Debug, Clone, PartialEq)]
89pub struct ParameterSpec {
90    pub name: String,
91    pub value_type: ParameterType,
92    pub default: Option<ParameterValue>,
93    pub bounds: Option<String>,
94}
95
96#[derive(Debug, Clone, PartialEq)]
97pub struct SourceRequires {
98    pub context: Vec<ContextRequirement>,
99}
100
101#[derive(Debug, Clone, PartialEq)]
102pub struct ContextRequirement {
103    pub name: String,
104    pub ty: ValueType,
105    pub required: bool,
106}
107
108#[derive(Debug, Clone, PartialEq)]
109pub struct ExecutionSpec {
110    pub deterministic: bool,
111    pub cadence: Cadence,
112}
113
114#[derive(Debug, Clone, PartialEq)]
115pub struct StateSpec {
116    pub allowed: bool,
117}
118
119#[derive(Debug, Clone, PartialEq)]
120pub struct SourcePrimitiveManifest {
121    pub id: String,
122    pub version: String,
123    pub kind: SourceKind,
124    pub inputs: Vec<InputSpec>,
125    pub outputs: Vec<OutputSpec>,
126    pub parameters: Vec<ParameterSpec>,
127    pub requires: SourceRequires,
128    pub execution: ExecutionSpec,
129    pub state: StateSpec,
130    pub side_effects: bool,
131}
132
133#[derive(Debug, Clone, PartialEq)]
134#[non_exhaustive]
135pub enum SourceValidationError {
136    InvalidId {
137        id: String,
138    },
139    InvalidVersion {
140        version: String,
141    },
142    WrongKind {
143        expected: SourceKind,
144        got: SourceKind,
145    },
146    InputsNotAllowed,
147    DuplicateOutput {
148        name: String,
149        first_index: usize,
150        second_index: usize,
151    },
152    SideEffectsNotAllowed,
153    NonDeterministicExecution,
154    InvalidCadence,
155    StateNotAllowed,
156    DuplicateId(String),
157    InvalidParameterType {
158        parameter: String,
159        expected: ParameterType,
160        got: ParameterType,
161    },
162    InvalidOutputType {
163        output: String,
164        expected: ValueType,
165        got: ValueType,
166    },
167    OutputsRequired,
168    UnboundContextKeyReference {
169        name: String,
170        referenced_param: String,
171    },
172    ContextKeyReferenceNotString {
173        name: String,
174        referenced_param: String,
175    },
176}
177
178impl ErrorInfo for SourceValidationError {
179    fn rule_id(&self) -> &'static str {
180        match self {
181            Self::InvalidId { .. } => "SRC-1",
182            Self::InvalidVersion { .. } => "SRC-2",
183            Self::WrongKind { .. } => "SRC-3",
184            Self::InputsNotAllowed => "SRC-4",
185            Self::OutputsRequired => "SRC-5",
186            Self::DuplicateOutput { .. } => "SRC-6",
187            Self::InvalidOutputType { .. } => "SRC-7",
188            Self::StateNotAllowed => "SRC-8",
189            Self::SideEffectsNotAllowed => "SRC-9",
190            Self::NonDeterministicExecution => "SRC-12",
191            Self::InvalidCadence => "SRC-13",
192            Self::DuplicateId(_) => "SRC-14",
193            Self::InvalidParameterType { .. } => "SRC-15",
194            Self::UnboundContextKeyReference { .. } => "SRC-16",
195            Self::ContextKeyReferenceNotString { .. } => "SRC-17",
196        }
197    }
198
199    fn phase(&self) -> Phase {
200        Phase::Registration
201    }
202
203    fn doc_anchor(&self) -> &'static str {
204        doc_anchor_for_rule(self.rule_id())
205    }
206
207    fn summary(&self) -> Cow<'static, str> {
208        match self {
209            Self::InvalidId { id } => Cow::Owned(format!("Invalid source ID: '{}'", id)),
210            Self::InvalidVersion { version } => {
211                Cow::Owned(format!("Invalid version: '{}'", version))
212            }
213            Self::WrongKind { expected, got } => Cow::Owned(format!(
214                "Wrong kind: expected {:?}, got {:?}",
215                expected, got
216            )),
217            Self::InputsNotAllowed => Cow::Borrowed("Sources cannot declare inputs"),
218            Self::OutputsRequired => Cow::Borrowed("Source must declare at least one output"),
219            Self::DuplicateOutput { name, .. } => {
220                Cow::Owned(format!("Duplicate output name: '{}'", name))
221            }
222            Self::InvalidOutputType {
223                output,
224                expected,
225                got,
226            } => Cow::Owned(format!(
227                "Output '{}' has invalid type: expected {:?}, got {:?}",
228                output, expected, got
229            )),
230            Self::StateNotAllowed => Cow::Borrowed("Source state is not allowed"),
231            Self::SideEffectsNotAllowed => Cow::Borrowed("Source side effects are not allowed"),
232            Self::NonDeterministicExecution => {
233                Cow::Borrowed("Source execution must be deterministic")
234            }
235            Self::InvalidCadence => Cow::Borrowed("Source cadence must be continuous"),
236            Self::DuplicateId(_) => Cow::Borrowed("Duplicate source ID: already registered"),
237            Self::InvalidParameterType {
238                parameter,
239                expected,
240                got,
241            } => Cow::Owned(format!(
242                "Parameter '{}' has invalid type: expected {:?}, got {:?}",
243                parameter, expected, got
244            )),
245            Self::UnboundContextKeyReference {
246                name,
247                referenced_param,
248            } => Cow::Owned(format!(
249                "Context key '{}' references undefined parameter '{}'",
250                name, referenced_param
251            )),
252            Self::ContextKeyReferenceNotString {
253                name,
254                referenced_param,
255            } => Cow::Owned(format!(
256                "Context key '{}' references parameter '{}' which is not String type",
257                name, referenced_param
258            )),
259        }
260    }
261
262    fn path(&self) -> Option<Cow<'static, str>> {
263        match self {
264            Self::InvalidId { .. } => Some(Cow::Borrowed("$.id")),
265            Self::InvalidVersion { .. } => Some(Cow::Borrowed("$.version")),
266            Self::WrongKind { .. } => Some(Cow::Borrowed("$.kind")),
267            Self::InputsNotAllowed => Some(Cow::Borrowed("$.inputs")),
268            Self::OutputsRequired => Some(Cow::Borrowed("$.outputs")),
269            Self::DuplicateOutput { second_index, .. } => {
270                Some(Cow::Owned(format!("$.outputs[{}].name", second_index)))
271            }
272            Self::InvalidOutputType { .. } => Some(Cow::Borrowed("$.outputs[].type")),
273            Self::StateNotAllowed => Some(Cow::Borrowed("$.state.allowed")),
274            Self::SideEffectsNotAllowed => Some(Cow::Borrowed("$.side_effects")),
275            Self::NonDeterministicExecution => Some(Cow::Borrowed("$.execution.deterministic")),
276            Self::InvalidCadence => Some(Cow::Borrowed("$.execution.cadence")),
277            Self::DuplicateId(_) => Some(Cow::Borrowed("$.id")),
278            Self::InvalidParameterType { .. } => Some(Cow::Borrowed("$.parameters[].default")),
279            Self::UnboundContextKeyReference { .. } => {
280                Some(Cow::Borrowed("$.requires.context[].name"))
281            }
282            Self::ContextKeyReferenceNotString { .. } => {
283                Some(Cow::Borrowed("$.requires.context[].name"))
284            }
285        }
286    }
287
288    fn fix(&self) -> Option<Cow<'static, str>> {
289        match self {
290            Self::InvalidId { .. } => Some(Cow::Borrowed(
291                "ID must start with lowercase letter and contain only lowercase letters, digits, and underscores",
292            )),
293            Self::DuplicateId(_) => Some(Cow::Borrowed("Choose a unique ID not already registered")),
294            Self::InvalidVersion { .. } => Some(Cow::Borrowed(
295                "Version must be valid semver (e.g., '1.0.0')",
296            )),
297            Self::WrongKind { .. } => Some(Cow::Borrowed("Set kind: source")),
298            Self::InputsNotAllowed => Some(Cow::Borrowed("Remove inputs from source manifest")),
299            Self::OutputsRequired => Some(Cow::Borrowed("Add at least one output")),
300            Self::DuplicateOutput { name, .. } => Some(Cow::Owned(format!(
301                "Rename output '{}' to a unique value",
302                name
303            ))),
304            Self::InvalidOutputType { .. } => Some(Cow::Borrowed(
305                "Use a valid output type: number, bool, string, or series",
306            )),
307            Self::StateNotAllowed => Some(Cow::Borrowed("Set state.allowed: false")),
308            Self::SideEffectsNotAllowed => Some(Cow::Borrowed("Set side_effects: false")),
309            Self::NonDeterministicExecution => {
310                Some(Cow::Borrowed("Set execution.deterministic: true"))
311            }
312            Self::InvalidCadence => Some(Cow::Borrowed("Set cadence: continuous")),
313            Self::InvalidParameterType { .. } => Some(Cow::Borrowed(
314                "Change parameter default value to match the declared parameter type",
315            )),
316            Self::UnboundContextKeyReference {
317                referenced_param, ..
318            } => Some(Cow::Owned(format!(
319                "Add parameter '{}' to the source manifest",
320                referenced_param
321            ))),
322            Self::ContextKeyReferenceNotString {
323                referenced_param, ..
324            } => Some(Cow::Owned(format!(
325                "Change parameter '{}' type to String",
326                referenced_param
327            ))),
328        }
329    }
330}
331
332impl fmt::Display for SourceValidationError {
333    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
334        write!(f, "{} ({})", self.summary(), self.rule_id())
335    }
336}
337
338impl std::error::Error for SourceValidationError {}
339
340/// A source primitive that produces values for graph evaluation.
341///
342/// Statelessness is enforced at registration by manifest validation
343/// (`SRC-8`, `state.allowed == false`) and at runtime by capture/replay.
344/// Structural enforcement on top (derive macros, marker traits, newtype
345/// wrappers) was considered and rejected; see
346/// `docs/ledger/decisions/rejected-structural-enforcement-of-statelessness.md`.
347pub trait SourcePrimitive {
348    fn manifest(&self) -> &SourcePrimitiveManifest;
349
350    fn produce(
351        &self,
352        parameters: &HashMap<String, ParameterValue>,
353        ctx: &ExecutionContext,
354    ) -> HashMap<String, Value>;
355}
356
357pub use implementations::{
358    boolean, context_bool, context_number, context_series, context_string, number, string,
359    BooleanSource, ContextBoolSource, ContextNumberSource, ContextSeriesSource,
360    ContextStringSource, NumberSource, StringSource,
361};
362pub use registry::SourceRegistry;
363
364#[cfg(test)]
365mod tests;