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 #[doc(hidden)]
123 fn compiled_expr(&mut self, at: &AuthoredPath, expr: &dyn ResolveExpr, records: bool) {
124 self.expr(at, expr.raw(), records);
125 }
126
127 fn interpolated(&mut self, _at: &AuthoredPath, _doc: &Interpolated) {}
129}
130
131pub struct Visit<'v> {
133 visitor: &'v mut dyn ExprVisitor,
134 path: AuthoredPath,
135 records: bool,
136}
137
138impl<'v> Visit<'v> {
139 pub fn new(visitor: &'v mut dyn ExprVisitor, root: AuthoredPath) -> Self {
141 Self {
142 visitor,
143 path: root,
144 records: false,
145 }
146 }
147
148 pub fn at(&mut self, seg: impl Into<PathSeg>, f: impl FnOnce(&mut Self)) {
150 self.path.push(seg);
151 f(self);
152 self.path.pop();
153 }
154
155 pub fn recording(&mut self, records: bool, f: impl FnOnce(&mut Self)) {
157 let before = self.records;
158 self.records |= records;
159 f(self);
160 self.records = before;
161 }
162
163 pub fn expr(&mut self, raw: &Value) {
165 self.visitor.expr(&self.path, raw, self.records);
166 }
167
168 pub(crate) fn compiled_expr(&mut self, expr: &dyn ResolveExpr) {
170 self.visitor.compiled_expr(&self.path, expr, self.records);
171 }
172
173 pub fn interpolated(&mut self, doc: &Interpolated) {
175 self.visitor.interpolated(&self.path, doc);
176 }
177}
178
179impl fmt::Debug for Visit<'_> {
180 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
182 f.debug_struct("Visit")
183 .field("path", &format_args!("{}", self.path))
184 .field("records", &self.records)
185 .finish_non_exhaustive()
186 }
187}
188
189pub struct CompileCx<'a> {
191 exprs: &'a ExprEngine,
192 issues: &'a mut Vec<Issue>,
193 path: AuthoredPath,
194 root_depth: usize,
195 step: Option<(StepKind, String)>,
196 runtime_fields: Option<&'a [FieldSpec]>,
197 rejected: usize,
198}
199
200impl<'a> CompileCx<'a> {
201 pub fn new(exprs: &'a ExprEngine, issues: &'a mut Vec<Issue>) -> Self {
203 Self {
204 exprs,
205 issues,
206 path: AuthoredPath::root(),
207 root_depth: 0,
208 step: None,
209 runtime_fields: None,
210 rejected: 0,
211 }
212 }
213
214 #[must_use]
216 pub fn at_path(mut self, path: AuthoredPath) -> Self {
217 self.root_depth = path.segs().len();
218 self.path = path;
219 self
220 }
221
222 #[must_use]
224 pub fn for_step(mut self, kind: StepKind, id: impl Into<String>) -> Self {
225 self.step = Some((kind, id.into()));
226 self
227 }
228
229 #[must_use]
231 pub fn with_runtime_fields(mut self, fields: &'a [FieldSpec]) -> Self {
232 self.runtime_fields = Some(fields);
233 self
234 }
235
236 pub fn exprs(&self) -> &ExprEngine {
238 self.exprs
239 }
240
241 pub fn path(&self) -> &AuthoredPath {
243 &self.path
244 }
245
246 pub fn runtime_fields(&self) -> Option<&'a [FieldSpec]> {
248 self.runtime_fields
249 }
250
251 pub fn issue(&mut self, code: IssueCode, message: impl Into<String>) {
253 self.issue_as(code, code.severity(), message);
254 }
255
256 pub fn issue_as(&mut self, code: IssueCode, severity: Severity, message: impl Into<String>) {
258 let issue = Issue::new(code, self.path.clone(), message).with_severity(severity);
259 self.record(issue);
260 }
261
262 pub(crate) fn adopt(&mut self, mut issue: Issue) {
267 issue.path = self.path.extend(&issue.path);
268 self.record(issue);
269 }
270
271 fn record(&mut self, mut issue: Issue) {
274 if let Some((kind, id)) = &self.step {
275 issue = issue.with_step(*kind, Some(id));
276 }
277 if let Some(PathSeg::Key(key)) = issue.path.segs().get(self.root_depth) {
278 issue.field = Some(key.to_string());
279 }
280 if issue.severity == Severity::Rejected {
281 self.rejected += 1;
282 }
283 self.issues.push(issue);
284 }
285
286 pub fn at<R>(&mut self, seg: impl Into<PathSeg>, f: impl FnOnce(&mut Self) -> R) -> R {
288 self.path.push(seg);
289 let out = f(self);
290 self.path.pop();
291 out
292 }
293
294 pub fn rejected(&self) -> usize {
296 self.rejected
297 }
298
299 #[doc(hidden)]
303 pub fn unknown_key(&mut self, key: &str, known: &[&str]) {
304 let (code, message) = unknown_key(key, known);
305 self.issue_as(code, Severity::Rejected, message);
306 }
307}
308
309impl fmt::Debug for CompileCx<'_> {
310 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
313 f.debug_struct("CompileCx")
314 .field("path", &format_args!("{}", self.path))
315 .field("step", &self.step)
316 .field("issues", &self.issues.len())
317 .field("rejected", &self.rejected)
318 .finish_non_exhaustive()
319 }
320}
321
322pub(crate) mod sealed {
324 pub trait Sealed {}
326}
327
328pub trait Resolver: sealed::Sealed {
353 fn eval(&mut self, expr: &dyn ResolveExpr) -> Result<OwnedDataValue>;
364
365 fn render(&mut self, doc: &Interpolated) -> Result<OwnedDataValue>;
372}
373
374pub trait ConfigField: Sized + Send + Sync + 'static {
446 const NULL_IS_VALUE: bool = false;
449
450 const IS_OUTPUT: bool = false;
456
457 fn compile(raw: &Value, cx: &mut CompileCx<'_>) -> Option<Self>;
459
460 fn absent() -> Option<Self> {
463 None
464 }
465
466 fn visit(&self, _v: &mut Visit<'_>) {}
468
469 fn resolve(&self, r: &mut dyn Resolver) -> Result<Option<OwnedDataValue>>;
479
480 fn kind() -> FieldKind;
482}
483
484fn owned<T: Clone + Into<OwnedDataValue>>(v: &T) -> Result<Option<OwnedDataValue>> {
486 Ok(Some(v.clone().into()))
487}
488
489fn scalar<T: serde::de::DeserializeOwned>(
490 raw: &Value,
491 cx: &mut CompileCx<'_>,
492 expected: &str,
493) -> Option<T> {
494 match T::deserialize(raw) {
495 Ok(v) => Some(v),
496 Err(_) => {
497 cx.issue(IssueCode::InputParse, format!("expected {expected}"));
498 None
499 }
500 }
501}
502
503macro_rules! scalar_field {
504 ($($t:ty => $kind:ident, $expected:expr;)*) => {$(
505 impl ConfigField for $t {
506 fn compile(raw: &Value, cx: &mut CompileCx<'_>) -> Option<Self> {
507 scalar(raw, cx, $expected)
508 }
509 fn resolve(&self, _r: &mut dyn Resolver) -> Result<Option<OwnedDataValue>> {
510 owned(self)
511 }
512 fn kind() -> FieldKind {
513 FieldKind::Literal(ValueKind::$kind)
514 }
515 }
516 )*};
517}
518
519scalar_field! {
520 bool => Bool, "a boolean";
521 String => String, "a string";
522 u8 => Number, "an unsigned integer that fits u8";
523 u16 => Number, "an unsigned integer that fits u16";
524 u32 => Number, "an unsigned integer that fits u32";
525 u64 => Number, "an unsigned integer that fits u64";
526 usize => Number, "an unsigned integer that fits usize";
527 i32 => Number, "an integer that fits i32";
528 i64 => Number, "an integer that fits i64";
529}
530
531impl ConfigField for f64 {
535 fn compile(raw: &Value, cx: &mut CompileCx<'_>) -> Option<Self> {
536 scalar(raw, cx, "a number")
537 }
538 fn resolve(&self, _r: &mut dyn Resolver) -> Result<Option<OwnedDataValue>> {
539 Ok(Some(OwnedDataValue::Number(NumberValue::Float(*self))))
540 }
541 fn kind() -> FieldKind {
542 FieldKind::Literal(ValueKind::Number)
543 }
544}
545
546impl ConfigField for OwnedDataValue {
547 const NULL_IS_VALUE: bool = true;
548 fn compile(raw: &Value, _cx: &mut CompileCx<'_>) -> Option<Self> {
549 Some(Self::from(raw))
550 }
551 fn resolve(&self, _r: &mut dyn Resolver) -> Result<Option<OwnedDataValue>> {
552 owned(self)
553 }
554 fn kind() -> FieldKind {
555 FieldKind::Literal(ValueKind::Any)
556 }
557}
558
559impl ConfigField for Value {
560 const NULL_IS_VALUE: bool = true;
561 fn compile(raw: &Value, _cx: &mut CompileCx<'_>) -> Option<Self> {
562 Some(raw.clone())
563 }
564 fn resolve(&self, _r: &mut dyn Resolver) -> Result<Option<OwnedDataValue>> {
565 Ok(Some(OwnedDataValue::from(self)))
566 }
567 fn kind() -> FieldKind {
568 FieldKind::Literal(ValueKind::Any)
569 }
570}
571
572impl<T: FromValue> ConfigField for Expr<T> {
573 const NULL_IS_VALUE: bool = true;
575 fn compile(raw: &Value, cx: &mut CompileCx<'_>) -> Option<Self> {
576 match cx.exprs().compile::<T>(raw) {
577 Ok(expr) => Some(expr),
578 Err(error) => {
579 let code = match error.kind() {
582 ErrorKind::Config if T::KIND == ValueKind::Path && raw.is_string() => {
583 IssueCode::InvalidPath
584 }
585 ErrorKind::Config => IssueCode::InputParse,
586 _ => IssueCode::TemplateCompile,
587 };
588 cx.issue(code, error.message().to_owned());
589 None
590 }
591 }
592 }
593 fn visit(&self, v: &mut Visit<'_>) {
594 v.compiled_expr(self);
595 }
596 fn resolve(&self, r: &mut dyn Resolver) -> Result<Option<OwnedDataValue>> {
597 r.eval(self).map(Some)
598 }
599 fn kind() -> FieldKind {
600 FieldKind::Expr(T::KIND)
601 }
602}
603
604impl<T: ConfigField> ConfigField for Option<T> {
605 const NULL_IS_VALUE: bool = true;
607 const IS_OUTPUT: bool = T::IS_OUTPUT;
609 fn compile(raw: &Value, cx: &mut CompileCx<'_>) -> Option<Self> {
610 if raw.is_null() && !T::NULL_IS_VALUE {
611 return Some(None);
612 }
613 T::compile(raw, cx).map(Some)
614 }
615 fn absent() -> Option<Self> {
616 Some(None)
617 }
618 fn visit(&self, v: &mut Visit<'_>) {
619 if let Some(inner) = self {
620 inner.visit(v);
621 }
622 }
623 fn resolve(&self, r: &mut dyn Resolver) -> Result<Option<OwnedDataValue>> {
624 match self {
625 Some(inner) => inner.resolve(r),
626 None => Ok(None),
627 }
628 }
629 fn kind() -> FieldKind {
630 T::kind()
631 }
632}
633
634impl<T: ConfigField> ConfigField for Vec<T> {
635 fn compile(raw: &Value, cx: &mut CompileCx<'_>) -> Option<Self> {
636 let Some(items) = raw.as_array() else {
637 cx.issue(IssueCode::InputParse, "expected an array");
638 return None;
639 };
640 let mut out = Self::with_capacity(items.len());
642 let mut ok = true;
643 for (i, item) in items.iter().enumerate() {
644 match cx.at(i, |cx| T::compile(item, cx)) {
645 Some(v) => out.push(v),
646 None => ok = false,
647 }
648 }
649 ok.then_some(out)
650 }
651 fn visit(&self, v: &mut Visit<'_>) {
652 for (i, item) in self.iter().enumerate() {
653 v.at(i, |v| item.visit(v));
654 }
655 }
656 fn resolve(&self, r: &mut dyn Resolver) -> Result<Option<OwnedDataValue>> {
657 let items = self
658 .iter()
659 .map(|item| Ok(item.resolve(r)?.unwrap_or(OwnedDataValue::Null)))
660 .collect::<Result<Vec<_>>>()?;
661 Ok(Some(OwnedDataValue::Array(items)))
662 }
663 fn kind() -> FieldKind {
664 FieldKind::List(T::kind)
665 }
666}
667
668impl<T: ConfigField> ConfigField for BTreeMap<String, T> {
669 fn compile(raw: &Value, cx: &mut CompileCx<'_>) -> Option<Self> {
670 let Some(entries) = raw.as_object() else {
671 cx.issue(IssueCode::InputParse, "expected an object");
672 return None;
673 };
674 let mut out = Self::new();
675 let mut ok = true;
676 for (key, value) in entries {
677 match cx.at(key.as_str(), |cx| T::compile(value, cx)) {
678 Some(v) => {
679 out.insert(key.clone(), v);
680 }
681 None => ok = false,
682 }
683 }
684 ok.then_some(out)
685 }
686 fn visit(&self, v: &mut Visit<'_>) {
687 for (key, item) in self {
688 v.at(key.as_str(), |v| item.visit(v));
689 }
690 }
691 fn resolve(&self, r: &mut dyn Resolver) -> Result<Option<OwnedDataValue>> {
692 let entries = self
693 .iter()
694 .map(|(key, item)| {
695 let value = item.resolve(r)?.unwrap_or(OwnedDataValue::Null);
696 Ok((key.clone(), value))
697 })
698 .collect::<Result<Vec<_>>>()?;
699 Ok(Some(OwnedDataValue::Object(entries)))
700 }
701 fn kind() -> FieldKind {
702 FieldKind::Map(T::kind)
703 }
704}
705
706#[cfg(test)]
707mod tests {
708 use super::*;
709
710 #[test]
711 fn adopt_puts_an_issue_below_the_current_path_with_the_step_and_field() {
712 let engine = ExprEngine::default();
713 let mut issues = Vec::new();
714 let rejected = {
715 let mut cx = CompileCx::new(&engine, &mut issues)
716 .at_path("tasks[1].function.input".parse().unwrap())
717 .for_step(StepKind::Task, "fetch");
718 assert_eq!(cx.rejected(), 0);
719 cx.adopt(Issue::new(
720 IssueCode::InputParse,
721 "connector".parse().unwrap(),
722 "no connector",
723 ));
724 cx.adopt(Issue::new(
726 IssueCode::UnknownKey,
727 "retries".parse().unwrap(),
728 "unknown",
729 ));
730 assert_eq!(cx.path().to_string(), "tasks[1].function.input");
732 cx.rejected()
733 };
734 assert_eq!(rejected, 1);
735 assert_eq!(issues.len(), 2);
736 let issue = &issues[0];
737 assert_eq!(issue.code, IssueCode::InputParse);
738 assert_eq!(issue.severity, Severity::Rejected);
739 assert_eq!(issue.path.to_string(), "tasks[1].function.input.connector");
740 assert_eq!(issue.field.as_deref(), Some("connector"));
741 assert_eq!(issue.step_kind, Some(StepKind::Task));
742 assert_eq!(issue.step_id.as_deref(), Some("fetch"));
743 assert_eq!(issue.message, "no connector");
744 assert_eq!(issues[1].severity, Severity::Advisory);
745 assert_eq!(issues[1].field.as_deref(), Some("retries"));
746 }
747}