1use 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
340pub 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;