1use super::document::Interpolated;
5use super::engine::ExprEngine;
6use super::expr::{Expr, FromValue, ResolveExpr};
7use crate::base::typo::unknown_key;
8use crate::base::{AuthoredPath, ErrorKind, Issue, IssueCode, PathSeg, Result, Severity, StepKind};
9use datavalue::{NumberValue, OwnedDataValue};
10use serde_json::Value;
11use std::borrow::Cow;
12use std::collections::BTreeMap;
13use std::fmt;
14
15#[derive(Clone, Copy, Debug, PartialEq, Eq)]
17#[non_exhaustive]
18pub enum ValueKind {
19 Any,
21 String,
23 Number,
25 Bool,
27 Object,
29 Array,
31 Path,
33}
34
35#[derive(Clone, Copy, Debug)]
37#[non_exhaustive]
38pub enum FieldKind {
39 Literal(ValueKind),
41 Expr(ValueKind),
43 Interpolated,
45 Output,
47 Templated,
49 List(fn() -> Self),
51 Map(fn() -> Self),
53 Nested(fn() -> &'static [FieldSpec]),
55}
56
57impl FieldKind {
58 pub(crate) fn contains_output(self) -> bool {
63 match self {
64 Self::Output => true,
65 Self::List(element) | Self::Map(element) => element().contains_output(),
66 _ => false,
67 }
68 }
69}
70
71#[derive(Clone, Debug)]
73#[non_exhaustive]
74pub struct FieldSpec {
75 pub name: Cow<'static, str>,
77 pub kind: FieldKind,
79 pub required: bool,
81 pub records: bool,
84 pub default: Option<Cow<'static, str>>,
86 pub primary: bool,
88 pub description: Option<Cow<'static, str>>,
90}
91
92impl FieldSpec {
93 pub fn runtime(name: impl Into<Cow<'static, str>>, kind: FieldKind, required: bool) -> Self {
96 Self {
97 name: name.into(),
98 kind,
99 required,
100 records: kind.contains_output(),
101 default: None,
102 primary: false,
103 description: None,
104 }
105 }
106
107 #[must_use]
109 pub fn with_default(mut self, json: impl Into<Cow<'static, str>>) -> Self {
110 self.default = Some(json.into());
111 self
112 }
113}
114
115pub trait ExprVisitor {
117 fn expr(&mut self, _at: &AuthoredPath, _raw: &Value, _records: bool) {}
120
121 fn interpolated(&mut self, _at: &AuthoredPath, _doc: &Interpolated) {}
123}
124
125pub struct Visit<'v> {
127 visitor: &'v mut dyn ExprVisitor,
128 path: AuthoredPath,
129 records: bool,
130}
131
132impl<'v> Visit<'v> {
133 pub fn new(visitor: &'v mut dyn ExprVisitor, root: AuthoredPath) -> Self {
135 Self {
136 visitor,
137 path: root,
138 records: false,
139 }
140 }
141
142 pub fn at(&mut self, seg: impl Into<PathSeg>, f: impl FnOnce(&mut Self)) {
144 self.path.push(seg);
145 f(self);
146 self.path.pop();
147 }
148
149 pub fn recording(&mut self, records: bool, f: impl FnOnce(&mut Self)) {
151 let before = self.records;
152 self.records |= records;
153 f(self);
154 self.records = before;
155 }
156
157 pub fn expr(&mut self, raw: &Value) {
159 self.visitor.expr(&self.path, raw, self.records);
160 }
161
162 pub fn interpolated(&mut self, doc: &Interpolated) {
164 self.visitor.interpolated(&self.path, doc);
165 }
166}
167
168impl fmt::Debug for Visit<'_> {
169 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
171 f.debug_struct("Visit")
172 .field("path", &format_args!("{}", self.path))
173 .field("records", &self.records)
174 .finish_non_exhaustive()
175 }
176}
177
178pub struct CompileCx<'a> {
180 exprs: &'a ExprEngine,
181 issues: &'a mut Vec<Issue>,
182 path: AuthoredPath,
183 root_depth: usize,
184 step: Option<(StepKind, String)>,
185 runtime_fields: Option<&'a [FieldSpec]>,
186 rejected: usize,
187}
188
189impl<'a> CompileCx<'a> {
190 pub fn new(exprs: &'a ExprEngine, issues: &'a mut Vec<Issue>) -> Self {
192 Self {
193 exprs,
194 issues,
195 path: AuthoredPath::root(),
196 root_depth: 0,
197 step: None,
198 runtime_fields: None,
199 rejected: 0,
200 }
201 }
202
203 #[must_use]
205 pub fn at_path(mut self, path: AuthoredPath) -> Self {
206 self.root_depth = path.segs().len();
207 self.path = path;
208 self
209 }
210
211 #[must_use]
213 pub fn for_step(mut self, kind: StepKind, id: impl Into<String>) -> Self {
214 self.step = Some((kind, id.into()));
215 self
216 }
217
218 #[must_use]
220 pub fn with_runtime_fields(mut self, fields: &'a [FieldSpec]) -> Self {
221 self.runtime_fields = Some(fields);
222 self
223 }
224
225 pub fn exprs(&self) -> &ExprEngine {
227 self.exprs
228 }
229
230 pub fn path(&self) -> &AuthoredPath {
232 &self.path
233 }
234
235 pub fn runtime_fields(&self) -> Option<&'a [FieldSpec]> {
237 self.runtime_fields
238 }
239
240 pub fn issue(&mut self, code: IssueCode, message: impl Into<String>) {
242 self.issue_as(code, code.severity(), message);
243 }
244
245 pub fn issue_as(&mut self, code: IssueCode, severity: Severity, message: impl Into<String>) {
247 let issue = Issue::new(code, self.path.clone(), message).with_severity(severity);
248 self.record(issue);
249 }
250
251 pub(crate) fn adopt(&mut self, mut issue: Issue) {
256 issue.path = self.path.extend(&issue.path);
257 self.record(issue);
258 }
259
260 fn record(&mut self, mut issue: Issue) {
263 if let Some((kind, id)) = &self.step {
264 issue = issue.with_step(*kind, Some(id));
265 }
266 if let Some(PathSeg::Key(key)) = issue.path.segs().get(self.root_depth) {
267 issue.field = Some(key.to_string());
268 }
269 if issue.severity == Severity::Rejected {
270 self.rejected += 1;
271 }
272 self.issues.push(issue);
273 }
274
275 pub fn at<R>(&mut self, seg: impl Into<PathSeg>, f: impl FnOnce(&mut Self) -> R) -> R {
277 self.path.push(seg);
278 let out = f(self);
279 self.path.pop();
280 out
281 }
282
283 pub fn rejected(&self) -> usize {
285 self.rejected
286 }
287
288 #[doc(hidden)]
292 pub fn unknown_key(&mut self, key: &str, known: &[&str]) {
293 let (code, message) = unknown_key(key, known);
294 self.issue_as(code, Severity::Rejected, message);
295 }
296}
297
298impl fmt::Debug for CompileCx<'_> {
299 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
302 f.debug_struct("CompileCx")
303 .field("path", &format_args!("{}", self.path))
304 .field("step", &self.step)
305 .field("issues", &self.issues.len())
306 .field("rejected", &self.rejected)
307 .finish_non_exhaustive()
308 }
309}
310
311pub(crate) mod sealed {
313 pub trait Sealed {}
315}
316
317pub trait Resolver: sealed::Sealed {
342 fn eval(&mut self, expr: &dyn ResolveExpr) -> Result<OwnedDataValue>;
353
354 fn render(&mut self, doc: &Interpolated) -> Result<OwnedDataValue>;
361}
362
363pub trait ConfigField: Sized + Send + Sync + 'static {
435 const NULL_IS_VALUE: bool = false;
438
439 const IS_OUTPUT: bool = false;
445
446 fn compile(raw: &Value, cx: &mut CompileCx<'_>) -> Option<Self>;
448
449 fn absent() -> Option<Self> {
452 None
453 }
454
455 fn visit(&self, _v: &mut Visit<'_>) {}
457
458 fn resolve(&self, r: &mut dyn Resolver) -> Result<Option<OwnedDataValue>>;
468
469 fn kind() -> FieldKind;
471}
472
473fn owned<T: Clone + Into<OwnedDataValue>>(v: &T) -> Result<Option<OwnedDataValue>> {
475 Ok(Some(v.clone().into()))
476}
477
478fn scalar<T: serde::de::DeserializeOwned>(
479 raw: &Value,
480 cx: &mut CompileCx<'_>,
481 expected: &str,
482) -> Option<T> {
483 match T::deserialize(raw) {
484 Ok(v) => Some(v),
485 Err(_) => {
486 cx.issue(IssueCode::InputParse, format!("expected {expected}"));
487 None
488 }
489 }
490}
491
492macro_rules! scalar_field {
493 ($($t:ty => $kind:ident, $expected:expr;)*) => {$(
494 impl ConfigField for $t {
495 fn compile(raw: &Value, cx: &mut CompileCx<'_>) -> Option<Self> {
496 scalar(raw, cx, $expected)
497 }
498 fn resolve(&self, _r: &mut dyn Resolver) -> Result<Option<OwnedDataValue>> {
499 owned(self)
500 }
501 fn kind() -> FieldKind {
502 FieldKind::Literal(ValueKind::$kind)
503 }
504 }
505 )*};
506}
507
508scalar_field! {
509 bool => Bool, "a boolean";
510 String => String, "a string";
511 u8 => Number, "an unsigned integer that fits u8";
512 u16 => Number, "an unsigned integer that fits u16";
513 u32 => Number, "an unsigned integer that fits u32";
514 u64 => Number, "an unsigned integer that fits u64";
515 usize => Number, "an unsigned integer that fits usize";
516 i32 => Number, "an integer that fits i32";
517 i64 => Number, "an integer that fits i64";
518}
519
520impl ConfigField for f64 {
524 fn compile(raw: &Value, cx: &mut CompileCx<'_>) -> Option<Self> {
525 scalar(raw, cx, "a number")
526 }
527 fn resolve(&self, _r: &mut dyn Resolver) -> Result<Option<OwnedDataValue>> {
528 Ok(Some(OwnedDataValue::Number(NumberValue::Float(*self))))
529 }
530 fn kind() -> FieldKind {
531 FieldKind::Literal(ValueKind::Number)
532 }
533}
534
535impl ConfigField for OwnedDataValue {
536 const NULL_IS_VALUE: bool = true;
537 fn compile(raw: &Value, _cx: &mut CompileCx<'_>) -> Option<Self> {
538 Some(Self::from(raw))
539 }
540 fn resolve(&self, _r: &mut dyn Resolver) -> Result<Option<OwnedDataValue>> {
541 owned(self)
542 }
543 fn kind() -> FieldKind {
544 FieldKind::Literal(ValueKind::Any)
545 }
546}
547
548impl ConfigField for Value {
549 const NULL_IS_VALUE: bool = true;
550 fn compile(raw: &Value, _cx: &mut CompileCx<'_>) -> Option<Self> {
551 Some(raw.clone())
552 }
553 fn resolve(&self, _r: &mut dyn Resolver) -> Result<Option<OwnedDataValue>> {
554 Ok(Some(OwnedDataValue::from(self)))
555 }
556 fn kind() -> FieldKind {
557 FieldKind::Literal(ValueKind::Any)
558 }
559}
560
561impl<T: FromValue> ConfigField for Expr<T> {
562 const NULL_IS_VALUE: bool = true;
564 fn compile(raw: &Value, cx: &mut CompileCx<'_>) -> Option<Self> {
565 match cx.exprs().compile::<T>(raw) {
566 Ok(expr) => Some(expr),
567 Err(error) => {
568 let code = match error.kind() {
571 ErrorKind::Config if T::KIND == ValueKind::Path && raw.is_string() => {
572 IssueCode::InvalidPath
573 }
574 ErrorKind::Config => IssueCode::InputParse,
575 _ => IssueCode::TemplateCompile,
576 };
577 cx.issue(code, error.message().to_owned());
578 None
579 }
580 }
581 }
582 fn visit(&self, v: &mut Visit<'_>) {
583 v.expr(self.raw());
584 }
585 fn resolve(&self, r: &mut dyn Resolver) -> Result<Option<OwnedDataValue>> {
586 r.eval(self).map(Some)
587 }
588 fn kind() -> FieldKind {
589 FieldKind::Expr(T::KIND)
590 }
591}
592
593impl<T: ConfigField> ConfigField for Option<T> {
594 const NULL_IS_VALUE: bool = true;
596 const IS_OUTPUT: bool = T::IS_OUTPUT;
598 fn compile(raw: &Value, cx: &mut CompileCx<'_>) -> Option<Self> {
599 if raw.is_null() && !T::NULL_IS_VALUE {
600 return Some(None);
601 }
602 T::compile(raw, cx).map(Some)
603 }
604 fn absent() -> Option<Self> {
605 Some(None)
606 }
607 fn visit(&self, v: &mut Visit<'_>) {
608 if let Some(inner) = self {
609 inner.visit(v);
610 }
611 }
612 fn resolve(&self, r: &mut dyn Resolver) -> Result<Option<OwnedDataValue>> {
613 match self {
614 Some(inner) => inner.resolve(r),
615 None => Ok(None),
616 }
617 }
618 fn kind() -> FieldKind {
619 T::kind()
620 }
621}
622
623impl<T: ConfigField> ConfigField for Vec<T> {
624 fn compile(raw: &Value, cx: &mut CompileCx<'_>) -> Option<Self> {
625 let Some(items) = raw.as_array() else {
626 cx.issue(IssueCode::InputParse, "expected an array");
627 return None;
628 };
629 let mut out = Self::with_capacity(items.len());
631 let mut ok = true;
632 for (i, item) in items.iter().enumerate() {
633 match cx.at(i, |cx| T::compile(item, cx)) {
634 Some(v) => out.push(v),
635 None => ok = false,
636 }
637 }
638 ok.then_some(out)
639 }
640 fn visit(&self, v: &mut Visit<'_>) {
641 for (i, item) in self.iter().enumerate() {
642 v.at(i, |v| item.visit(v));
643 }
644 }
645 fn resolve(&self, r: &mut dyn Resolver) -> Result<Option<OwnedDataValue>> {
646 let items = self
647 .iter()
648 .map(|item| Ok(item.resolve(r)?.unwrap_or(OwnedDataValue::Null)))
649 .collect::<Result<Vec<_>>>()?;
650 Ok(Some(OwnedDataValue::Array(items)))
651 }
652 fn kind() -> FieldKind {
653 FieldKind::List(T::kind)
654 }
655}
656
657impl<T: ConfigField> ConfigField for BTreeMap<String, T> {
658 fn compile(raw: &Value, cx: &mut CompileCx<'_>) -> Option<Self> {
659 let Some(entries) = raw.as_object() else {
660 cx.issue(IssueCode::InputParse, "expected an object");
661 return None;
662 };
663 let mut out = Self::new();
664 let mut ok = true;
665 for (key, value) in entries {
666 match cx.at(key.as_str(), |cx| T::compile(value, cx)) {
667 Some(v) => {
668 out.insert(key.clone(), v);
669 }
670 None => ok = false,
671 }
672 }
673 ok.then_some(out)
674 }
675 fn visit(&self, v: &mut Visit<'_>) {
676 for (key, item) in self {
677 v.at(key.as_str(), |v| item.visit(v));
678 }
679 }
680 fn resolve(&self, r: &mut dyn Resolver) -> Result<Option<OwnedDataValue>> {
681 let entries = self
682 .iter()
683 .map(|(key, item)| {
684 let value = item.resolve(r)?.unwrap_or(OwnedDataValue::Null);
685 Ok((key.clone(), value))
686 })
687 .collect::<Result<Vec<_>>>()?;
688 Ok(Some(OwnedDataValue::Object(entries)))
689 }
690 fn kind() -> FieldKind {
691 FieldKind::Map(T::kind)
692 }
693}
694
695#[cfg(test)]
696mod tests {
697 use super::*;
698
699 #[test]
700 fn adopt_puts_an_issue_below_the_current_path_with_the_step_and_field() {
701 let engine = ExprEngine::default();
702 let mut issues = Vec::new();
703 let rejected = {
704 let mut cx = CompileCx::new(&engine, &mut issues)
705 .at_path("tasks[1].function.input".parse().unwrap())
706 .for_step(StepKind::Task, "fetch");
707 assert_eq!(cx.rejected(), 0);
708 cx.adopt(Issue::new(
709 IssueCode::InputParse,
710 "connector".parse().unwrap(),
711 "no connector",
712 ));
713 cx.adopt(Issue::new(
715 IssueCode::UnknownKey,
716 "retries".parse().unwrap(),
717 "unknown",
718 ));
719 assert_eq!(cx.path().to_string(), "tasks[1].function.input");
721 cx.rejected()
722 };
723 assert_eq!(rejected, 1);
724 assert_eq!(issues.len(), 2);
725 let issue = &issues[0];
726 assert_eq!(issue.code, IssueCode::InputParse);
727 assert_eq!(issue.severity, Severity::Rejected);
728 assert_eq!(issue.path.to_string(), "tasks[1].function.input.connector");
729 assert_eq!(issue.field.as_deref(), Some("connector"));
730 assert_eq!(issue.step_kind, Some(StepKind::Task));
731 assert_eq!(issue.step_id.as_deref(), Some("fetch"));
732 assert_eq!(issue.message, "no connector");
733 assert_eq!(issues[1].severity, Severity::Advisory);
734 assert_eq!(issues[1].field.as_deref(), Some("retries"));
735 }
736}