Skip to main content

dataflow_rs/expr/
config.rs

1//! Config fields: the types a function's `input` is made of, and the context
2//! that compiles them with every issue at its own path.
3
4use super::document::Interpolated;
5use super::engine::ExprEngine;
6use super::expr::{Expr, FromValue, ResolveExpr};
7use super::output::Output;
8use crate::base::typo::unknown_key;
9use crate::base::{AuthoredPath, ErrorKind, Issue, IssueCode, PathSeg, Result, Severity, StepKind};
10use datavalue::{NumberValue, OwnedDataValue};
11use serde_json::Value;
12use std::borrow::Cow;
13use std::collections::BTreeMap;
14use std::fmt;
15
16/// What a field's value is, for a field's schema and for checking a literal.
17#[derive(Clone, Copy, Debug, PartialEq, Eq)]
18#[non_exhaustive]
19pub enum ValueKind {
20    /// Any JSON.
21    Any,
22    /// A string.
23    String,
24    /// A number.
25    Number,
26    /// A boolean.
27    Bool,
28    /// An object.
29    Object,
30    /// An array.
31    Array,
32    /// A context path, rooted at `data`, `metadata` or `temp_data`.
33    Path,
34}
35
36/// What a config field is, for tooling.
37#[derive(Clone, Copy, Debug)]
38#[non_exhaustive]
39pub enum FieldKind {
40    /// A literal of the given kind, never evaluated.
41    Literal(ValueKind),
42    /// JSONLogic resolving to the given kind.
43    Expr(ValueKind),
44    /// A document with expression holes.
45    Interpolated,
46    /// A write destination.
47    Output,
48    /// A runtime-declared set of fields.
49    Templated,
50    /// An array of the element kind.
51    List(fn() -> Self),
52    /// An object whose values are of the given kind.
53    Map(fn() -> Self),
54    /// A nested config's fields.
55    Nested(fn() -> &'static [FieldSpec]),
56}
57
58impl FieldKind {
59    /// Whether the kind is an output, or holds one at any depth of a list or
60    /// a map. The engine records every output's result whatever its field
61    /// says, so a field of such a kind records. A nested config's
62    /// own fields say so for themselves.
63    pub(crate) fn contains_output(self) -> bool {
64        match self {
65            Self::Output => true,
66            Self::List(element) | Self::Map(element) => element().contains_output(),
67            _ => false,
68        }
69    }
70}
71
72/// One field of a config.
73#[derive(Clone, Debug)]
74#[non_exhaustive]
75pub struct FieldSpec {
76    /// The authored key.
77    pub name: Cow<'static, str>,
78    /// What the field is.
79    pub kind: FieldKind,
80    /// Is the key required?
81    pub required: bool,
82    /// Does the engine record the field's result? Always for a field holding
83    /// an output.
84    pub records: bool,
85    /// The default, as JSON text.
86    pub default: Option<Cow<'static, str>>,
87    /// Is this the primary output?
88    pub primary: bool,
89    /// The field's doc comment.
90    pub description: Option<Cow<'static, str>>,
91}
92
93impl FieldSpec {
94    /// A runtime field: its kind is an expression, an interpolated document, a
95    /// literal or an output. It records when it holds an output.
96    pub fn runtime(name: impl Into<Cow<'static, str>>, kind: FieldKind, required: bool) -> Self {
97        Self {
98            name: name.into(),
99            kind,
100            required,
101            records: kind.contains_output(),
102            default: None,
103            primary: false,
104            description: None,
105        }
106    }
107
108    /// Sets the default, as JSON text.
109    #[must_use]
110    pub fn with_default(mut self, json: impl Into<Cow<'static, str>>) -> Self {
111        self.default = Some(json.into());
112        self
113    }
114}
115
116/// Receives every expression of a config, with its path.
117pub trait ExprVisitor {
118    /// An expression at `at`; `records` is set when the engine records its
119    /// result.
120    fn expr(&mut self, _at: &AuthoredPath, _raw: &Value, _records: bool) {}
121
122    /// A compiled expression at `at`; forwards to [`Self::expr`] by default.
123    #[doc(hidden)]
124    fn compiled_expr(&mut self, at: &AuthoredPath, expr: &dyn ResolveExpr, records: bool) {
125        self.expr(at, expr.raw(), records);
126    }
127
128    /// An interpolated document at `at`.
129    fn interpolated(&mut self, _at: &AuthoredPath, _doc: &Interpolated) {}
130}
131
132/// A walk over a compiled config's expressions.
133pub struct Visit<'v> {
134    visitor: &'v mut dyn ExprVisitor,
135    path: AuthoredPath,
136    records: bool,
137}
138
139impl<'v> Visit<'v> {
140    /// A walk reporting to `visitor`, with paths below `root`.
141    pub fn new(visitor: &'v mut dyn ExprVisitor, root: AuthoredPath) -> Self {
142        Self {
143            visitor,
144            path: root,
145            records: false,
146        }
147    }
148
149    /// Runs `f` one segment deeper.
150    pub fn at(&mut self, seg: impl Into<PathSeg>, f: impl FnOnce(&mut Self)) {
151        self.path.push(seg);
152        f(self);
153        self.path.pop();
154    }
155
156    /// Runs `f` with the `records` flag ORed in, restoring it after.
157    pub fn recording(&mut self, records: bool, f: impl FnOnce(&mut Self)) {
158        let before = self.records;
159        self.records |= records;
160        f(self);
161        self.records = before;
162    }
163
164    /// Reports an expression at the current path.
165    pub fn expr(&mut self, raw: &Value) {
166        self.visitor.expr(&self.path, raw, self.records);
167    }
168
169    /// Reports a compiled expression at the current path.
170    pub(crate) fn compiled_expr(&mut self, expr: &dyn ResolveExpr) {
171        self.visitor.compiled_expr(&self.path, expr, self.records);
172    }
173
174    /// Reports an interpolated document at the current path.
175    pub fn interpolated(&mut self, doc: &Interpolated) {
176        self.visitor.interpolated(&self.path, doc);
177    }
178}
179
180impl fmt::Debug for Visit<'_> {
181    /// The current path and `records` flag; the visitor is not shown.
182    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
183        f.debug_struct("Visit")
184            .field("path", &format_args!("{}", self.path))
185            .field("records", &self.records)
186            .finish_non_exhaustive()
187    }
188}
189
190/// Compiles config fields, recording each issue at its exact authored path.
191pub struct CompileCx<'a> {
192    exprs: &'a ExprEngine,
193    issues: &'a mut Vec<Issue>,
194    path: AuthoredPath,
195    root_depth: usize,
196    step: Option<(StepKind, String)>,
197    runtime_fields: Option<&'a [FieldSpec]>,
198    rejected: usize,
199}
200
201impl<'a> CompileCx<'a> {
202    /// A context rooted at the document itself.
203    pub fn new(exprs: &'a ExprEngine, issues: &'a mut Vec<Issue>) -> Self {
204        Self {
205            exprs,
206            issues,
207            path: AuthoredPath::root(),
208            root_depth: 0,
209            step: None,
210            runtime_fields: None,
211            rejected: 0,
212        }
213    }
214
215    /// Roots the walk at `path`; a field is the first key below it.
216    #[must_use]
217    pub fn at_path(mut self, path: AuthoredPath) -> Self {
218        self.root_depth = path.segs().len();
219        self.path = path;
220        self
221    }
222
223    /// Stamps the step on every issue.
224    #[must_use]
225    pub fn for_step(mut self, kind: StepKind, id: impl Into<String>) -> Self {
226        self.step = Some((kind, id.into()));
227        self
228    }
229
230    /// The fields a plugin declared at runtime.
231    #[must_use]
232    pub fn with_runtime_fields(mut self, fields: &'a [FieldSpec]) -> Self {
233        self.runtime_fields = Some(fields);
234        self
235    }
236
237    /// The engine expressions compile with.
238    pub fn exprs(&self) -> &ExprEngine {
239        self.exprs
240    }
241
242    /// The current path.
243    pub fn path(&self) -> &AuthoredPath {
244        &self.path
245    }
246
247    /// The runtime-declared fields, if any.
248    pub fn runtime_fields(&self) -> Option<&'a [FieldSpec]> {
249        self.runtime_fields
250    }
251
252    /// Records an issue at the current path, at the code's default severity.
253    pub fn issue(&mut self, code: IssueCode, message: impl Into<String>) {
254        self.issue_as(code, code.severity(), message);
255    }
256
257    /// Records an issue at the current path with an explicit severity.
258    pub fn issue_as(&mut self, code: IssueCode, severity: Severity, message: impl Into<String>) {
259        let issue = Issue::new(code, self.path.clone(), message).with_severity(severity);
260        self.record(issue);
261    }
262
263    /// Records an issue found by another walk, such as a function's own
264    /// checks, whose path is relative to the current path. The path is
265    /// prefixed with the current one, the step and the field are stamped as
266    /// [`Self::issue`] stamps them, and a Rejected issue is counted.
267    pub(crate) fn adopt(&mut self, mut issue: Issue) {
268        issue.path = self.path.extend(&issue.path);
269        self.record(issue);
270    }
271
272    /// Stamps the step and the field on `issue`, counts it when it is
273    /// Rejected, and records it.
274    fn record(&mut self, mut issue: Issue) {
275        if let Some((kind, id)) = &self.step {
276            issue = issue.with_step(*kind, Some(id));
277        }
278        if let Some(PathSeg::Key(key)) = issue.path.segs().get(self.root_depth) {
279            issue.field = Some(key.to_string());
280        }
281        if issue.severity == Severity::Rejected {
282            self.rejected += 1;
283        }
284        self.issues.push(issue);
285    }
286
287    /// Runs `f` one segment deeper.
288    pub fn at<R>(&mut self, seg: impl Into<PathSeg>, f: impl FnOnce(&mut Self) -> R) -> R {
289        self.path.push(seg);
290        let out = f(self);
291        self.path.pop();
292        out
293    }
294
295    /// How many Rejected issues this context has recorded.
296    pub fn rejected(&self) -> usize {
297        self.rejected
298    }
299
300    /// Records an unknown `key` at the current path (the caller is inside
301    /// `at(key, …)`): a Rejected `LIKELY_TYPO` naming the near key, else a
302    /// Rejected `UNKNOWN_KEY`.
303    #[doc(hidden)]
304    pub fn unknown_key(&mut self, key: &str, known: &[&str]) {
305        let (code, message) = unknown_key(key, known);
306        self.issue_as(code, Severity::Rejected, message);
307    }
308}
309
310impl fmt::Debug for CompileCx<'_> {
311    /// The current path, the step, and how many issues are recorded and
312    /// Rejected; not the issues or the engine.
313    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
314        f.debug_struct("CompileCx")
315            .field("path", &format_args!("{}", self.path))
316            .field("step", &self.step)
317            .field("issues", &self.issues.len())
318            .field("rejected", &self.rejected)
319            .finish_non_exhaustive()
320    }
321}
322
323/// What [`Resolver`] is sealed with: only the crate implements it.
324pub(crate) mod sealed {
325    /// The seal: a type the crate resolves configs with.
326    pub trait Sealed {}
327}
328
329/// What a [`Resolver`] supplies for a config's primary output in place of
330/// evaluating it.
331///
332/// A call's context answers this only when its input is resolved with the
333/// destination already known
334/// ([`Ctx::input_with_primary_output`](crate::Ctx::input_with_primary_output)
335/// and [`Ctx::input_without_primary_output`](crate::Ctx::input_without_primary_output));
336/// every other resolution, [`Ctx::input`](crate::Ctx::input) included, leaves
337/// every output to resolve as usual.
338#[derive(Clone, Debug, PartialEq)]
339#[non_exhaustive]
340pub enum OutputOverride {
341    /// The field is left out, as an optional output the input left unset is.
342    Omit,
343    /// The field resolves to this value, its expression never evaluated.
344    Value(OwnedDataValue),
345}
346
347/// What resolving a config needs from a call: an expression evaluated, and
348/// a document rendered, each to an owned value, against the context the call
349/// sees. A [`ConfigField`] calls it from [`ConfigField::resolve`]; a call's
350/// context implements it ([`Ctx`](crate::Ctx), through a shared reference).
351///
352/// Sealed: a host calls it, from its own [`ConfigField::resolve`], and never
353/// implements it, so a method can join it without a breaking change.
354///
355/// ```compile_fail
356/// use dataflow_rs::Result;
357/// use dataflow_rs::datavalue::OwnedDataValue;
358/// use dataflow_rs::expr::{Interpolated, ResolveExpr, Resolver};
359///
360/// struct Mine;
361///
362/// impl Resolver for Mine {
363///     fn eval(&mut self, _expr: &dyn ResolveExpr) -> Result<OwnedDataValue> {
364///         Ok(OwnedDataValue::Null)
365///     }
366///     fn render(&mut self, _doc: &Interpolated) -> Result<OwnedDataValue> {
367///         Ok(OwnedDataValue::Null)
368///     }
369/// }
370/// ```
371pub trait Resolver: sealed::Sealed {
372    /// `expr` evaluated, as an owned value: a number for a `u64`, `i64` or
373    /// `f64` expression, a boolean for a `bool` one, a string for a `String`
374    /// one, a path's dotted text for a `Path` one, and the value itself for
375    /// any other. A folded constant is its value, evaluated never.
376    ///
377    /// # Errors
378    ///
379    /// The evaluation's: [`ErrorKind::Budget`] when the operation ceiling is
380    /// exceeded, [`ErrorKind::Eval`] for any other failure, a result that is
381    /// not of the expression's type included.
382    fn eval(&mut self, expr: &dyn ResolveExpr) -> Result<OwnedDataValue>;
383
384    /// `doc` rendered, as an owned tree: each hole's value, or its default.
385    ///
386    /// # Errors
387    ///
388    /// None from a call's context, which is every resolver there is today;
389    /// the `Result` leaves room for one that may refuse.
390    fn render(&mut self, doc: &Interpolated) -> Result<OwnedDataValue>;
391
392    /// The value `out` resolves to in place of being evaluated, when this
393    /// resolution supplies the callee's primary output: `None` resolves it
394    /// as usual, as every resolver does but the two input methods that
395    /// supply it.
396    ///
397    /// Only the [`Output`] a config's
398    /// [`primary_output`](crate::function::FunctionConfig::primary_output)
399    /// lends is answered; every other output, a nested one included, is
400    /// resolved as usual.
401    fn output_override(&mut self, _out: &Output) -> Option<OutputOverride> {
402        None
403    }
404}
405
406/// A type a config is made of.
407///
408/// An `Option<T>` field is optional: an absent key is `None`, and so is an
409/// explicit `null`, unless `T` takes `null` as a value
410/// ([`NULL_IS_VALUE`](Self::NULL_IS_VALUE)), as an [`Expr`], an
411/// [`Interpolated`] document and any JSON value do. There the `null` is
412/// compiled as `T`: `map`'s `"logic": null` is a constant `null` expression,
413/// not a missing `logic`.
414///
415/// A host writes its own field type by implementing it: here a connector's
416/// name, a newtype over an `Expr<String>`, compiled, walked and resolved as
417/// the expression is, so a derived config holding one checks its secrets
418/// and template keys, and resolves it, as it would the expression.
419///
420/// ```
421/// # tokio::runtime::Builder::new_current_thread().build().unwrap().block_on(async {
422/// use dataflow_rs::datavalue::OwnedDataValue;
423/// use dataflow_rs::expr::{CompileCx, ConfigField, FieldKind, Resolver, ValueKind, Visit};
424/// use dataflow_rs::function::{FunctionConfig, testing};
425/// use dataflow_rs::{Ctx, Expr, Function, Message, Outcome, Result};
426/// use serde_json::{Value, json};
427///
428/// /// The connector a call reaches, by name.
429/// struct Connector(Expr<String>);
430///
431/// impl ConfigField for Connector {
432///     fn compile(raw: &Value, cx: &mut CompileCx<'_>) -> Option<Self> {
433///         Expr::<String>::compile(raw, cx).map(Self)
434///     }
435///     fn visit(&self, v: &mut Visit<'_>) {
436///         v.expr(self.0.raw());
437///     }
438///     fn resolve(&self, r: &mut dyn Resolver) -> Result<Option<OwnedDataValue>> {
439///         r.eval(&self.0).map(Some)
440///     }
441///     fn kind() -> FieldKind {
442///         FieldKind::Expr(ValueKind::String)
443///     }
444/// }
445///
446/// #[derive(FunctionConfig)]
447/// struct Query {
448///     connector: Connector,
449///     limit: Option<Expr<u64>>,
450/// }
451///
452/// /// Writes its input, as a layer or a dry run reads it, at `data.input`.
453/// struct Echo;
454///
455/// impl Function for Echo {
456///     type Config = Query;
457///
458///     async fn call(&self, ctx: &mut Ctx<'_>, _cfg: &Query) -> Result<Outcome> {
459///         let input = ctx.input()?;
460///         ctx.set(&"data.input".parse()?, input)?;
461///         Ok(Outcome::Ok)
462///     }
463/// }
464///
465/// let input = json!({"connector": {"cat": ["db-", {"var": "data.region"}]}});
466/// let data = OwnedDataValue::from(&json!({"region": "eu"}));
467/// let out = testing::call(Echo, input)
468///     .message(Message::from_parts(OwnedDataValue::Null, data, OwnedDataValue::Null))
469///     .await;
470/// assert_eq!(out.outcome.unwrap(), Outcome::Ok);
471/// // The field resolved at its key; the `Option` left unset is absent.
472/// let at = "data.input".parse().unwrap();
473/// let written = out.message.context().get(&at).unwrap();
474/// assert_eq!(serde_json::to_string(written).unwrap(), r#"{"connector":"db-eu"}"#);
475/// # });
476/// ```
477pub trait ConfigField: Sized + Send + Sync + 'static {
478    /// Whether `null` is one of the type's values. When it is not, an explicit
479    /// `null` in an `Option<Self>` field means `None`.
480    const NULL_IS_VALUE: bool = false;
481
482    /// Whether the field is an output: a destination its call writes to.
483    /// `false` unless the type is [`Output`](crate::expr::Output) or an
484    /// `Option` of one. The derive reads a field typed `Output` or marked
485    /// `#[output]` as an output and asserts that this is `true`, so a type
486    /// that merely shares the name is refused at compile time.
487    const IS_OUTPUT: bool = false;
488
489    /// Compiles `raw`; `None` after recording at least one issue.
490    fn compile(raw: &Value, cx: &mut CompileCx<'_>) -> Option<Self>;
491
492    /// The value when the key is absent and no default is declared. `None`
493    /// makes the field required.
494    fn absent() -> Option<Self> {
495        None
496    }
497
498    /// Reports the field's expressions.
499    fn visit(&self, _v: &mut Visit<'_>) {}
500
501    /// The field's value for this call, resolved through `r`, or `None` when
502    /// it has none (an `Option` the input left unset, with no default). The
503    /// value sits where [`Self::visit`] reports the field's expressions: a
504    /// list's elements at their indices, a map's values at their keys, and
505    /// an element left unset is `null`, keeping its place.
506    ///
507    /// # Errors
508    ///
509    /// The resolver's, for the first expression or document that fails.
510    fn resolve(&self, r: &mut dyn Resolver) -> Result<Option<OwnedDataValue>>;
511
512    /// What the field is.
513    fn kind() -> FieldKind;
514}
515
516/// A scalar's value: itself.
517fn owned<T: Clone + Into<OwnedDataValue>>(v: &T) -> Result<Option<OwnedDataValue>> {
518    Ok(Some(v.clone().into()))
519}
520
521fn scalar<T: serde::de::DeserializeOwned>(
522    raw: &Value,
523    cx: &mut CompileCx<'_>,
524    expected: &str,
525) -> Option<T> {
526    match T::deserialize(raw) {
527        Ok(v) => Some(v),
528        Err(_) => {
529            cx.issue(IssueCode::InputParse, format!("expected {expected}"));
530            None
531        }
532    }
533}
534
535macro_rules! scalar_field {
536    ($($t:ty => $kind:ident, $expected:expr;)*) => {$(
537        impl ConfigField for $t {
538            fn compile(raw: &Value, cx: &mut CompileCx<'_>) -> Option<Self> {
539                scalar(raw, cx, $expected)
540            }
541            fn resolve(&self, _r: &mut dyn Resolver) -> Result<Option<OwnedDataValue>> {
542                owned(self)
543            }
544            fn kind() -> FieldKind {
545                FieldKind::Literal(ValueKind::$kind)
546            }
547        }
548    )*};
549}
550
551scalar_field! {
552    bool => Bool, "a boolean";
553    String => String, "a string";
554    u8 => Number, "an unsigned integer that fits u8";
555    u16 => Number, "an unsigned integer that fits u16";
556    u32 => Number, "an unsigned integer that fits u32";
557    u64 => Number, "an unsigned integer that fits u64";
558    usize => Number, "an unsigned integer that fits usize";
559    i32 => Number, "an integer that fits i32";
560    i64 => Number, "an integer that fits i64";
561}
562
563/// A float, written as one: `2.0` stays `2.0`, as the JSON bridge keeps a
564/// float literal, where [`OwnedDataValue`]'s `From<f64>` would make it the
565/// integer `2`.
566impl ConfigField for f64 {
567    fn compile(raw: &Value, cx: &mut CompileCx<'_>) -> Option<Self> {
568        scalar(raw, cx, "a number")
569    }
570    fn resolve(&self, _r: &mut dyn Resolver) -> Result<Option<OwnedDataValue>> {
571        Ok(Some(OwnedDataValue::Number(NumberValue::Float(*self))))
572    }
573    fn kind() -> FieldKind {
574        FieldKind::Literal(ValueKind::Number)
575    }
576}
577
578impl ConfigField for OwnedDataValue {
579    const NULL_IS_VALUE: bool = true;
580    fn compile(raw: &Value, _cx: &mut CompileCx<'_>) -> Option<Self> {
581        Some(Self::from(raw))
582    }
583    fn resolve(&self, _r: &mut dyn Resolver) -> Result<Option<OwnedDataValue>> {
584        owned(self)
585    }
586    fn kind() -> FieldKind {
587        FieldKind::Literal(ValueKind::Any)
588    }
589}
590
591impl ConfigField for Value {
592    const NULL_IS_VALUE: bool = true;
593    fn compile(raw: &Value, _cx: &mut CompileCx<'_>) -> Option<Self> {
594        Some(raw.clone())
595    }
596    fn resolve(&self, _r: &mut dyn Resolver) -> Result<Option<OwnedDataValue>> {
597        Ok(Some(OwnedDataValue::from(self)))
598    }
599    fn kind() -> FieldKind {
600        FieldKind::Literal(ValueKind::Any)
601    }
602}
603
604impl<T: FromValue> ConfigField for Expr<T> {
605    /// `null` is a constant expression.
606    const NULL_IS_VALUE: bool = true;
607    fn compile(raw: &Value, cx: &mut CompileCx<'_>) -> Option<Self> {
608        match cx.exprs().compile::<T>(raw) {
609            Ok(expr) => Some(expr),
610            Err(error) => {
611                // `INVALID_PATH` is a literal string that is not a path; a
612                // non-string where a path goes is a type error.
613                let code = match error.kind() {
614                    ErrorKind::Config if T::KIND == ValueKind::Path && raw.is_string() => {
615                        IssueCode::InvalidPath
616                    }
617                    ErrorKind::Config => IssueCode::InputParse,
618                    _ => IssueCode::TemplateCompile,
619                };
620                cx.issue(code, error.message().to_owned());
621                None
622            }
623        }
624    }
625    fn visit(&self, v: &mut Visit<'_>) {
626        v.compiled_expr(self);
627    }
628    fn resolve(&self, r: &mut dyn Resolver) -> Result<Option<OwnedDataValue>> {
629        r.eval(self).map(Some)
630    }
631    fn kind() -> FieldKind {
632        FieldKind::Expr(T::KIND)
633    }
634}
635
636impl<T: ConfigField> ConfigField for Option<T> {
637    /// `null` is `None`, or `T`'s own `null`.
638    const NULL_IS_VALUE: bool = true;
639    /// An `Option<Output>` is an output too.
640    const IS_OUTPUT: bool = T::IS_OUTPUT;
641    fn compile(raw: &Value, cx: &mut CompileCx<'_>) -> Option<Self> {
642        if raw.is_null() && !T::NULL_IS_VALUE {
643            return Some(None);
644        }
645        T::compile(raw, cx).map(Some)
646    }
647    fn absent() -> Option<Self> {
648        Some(None)
649    }
650    fn visit(&self, v: &mut Visit<'_>) {
651        if let Some(inner) = self {
652            inner.visit(v);
653        }
654    }
655    fn resolve(&self, r: &mut dyn Resolver) -> Result<Option<OwnedDataValue>> {
656        match self {
657            Some(inner) => inner.resolve(r),
658            None => Ok(None),
659        }
660    }
661    fn kind() -> FieldKind {
662        T::kind()
663    }
664}
665
666impl<T: ConfigField> ConfigField for Vec<T> {
667    fn compile(raw: &Value, cx: &mut CompileCx<'_>) -> Option<Self> {
668        let Some(items) = raw.as_array() else {
669            cx.issue(IssueCode::InputParse, "expected an array");
670            return None;
671        };
672        // Every element is compiled, so every issue is reported.
673        let mut out = Self::with_capacity(items.len());
674        let mut ok = true;
675        for (i, item) in items.iter().enumerate() {
676            match cx.at(i, |cx| T::compile(item, cx)) {
677                Some(v) => out.push(v),
678                None => ok = false,
679            }
680        }
681        ok.then_some(out)
682    }
683    fn visit(&self, v: &mut Visit<'_>) {
684        for (i, item) in self.iter().enumerate() {
685            v.at(i, |v| item.visit(v));
686        }
687    }
688    fn resolve(&self, r: &mut dyn Resolver) -> Result<Option<OwnedDataValue>> {
689        let items = self
690            .iter()
691            .map(|item| Ok(item.resolve(r)?.unwrap_or(OwnedDataValue::Null)))
692            .collect::<Result<Vec<_>>>()?;
693        Ok(Some(OwnedDataValue::Array(items)))
694    }
695    fn kind() -> FieldKind {
696        FieldKind::List(T::kind)
697    }
698}
699
700impl<T: ConfigField> ConfigField for BTreeMap<String, T> {
701    fn compile(raw: &Value, cx: &mut CompileCx<'_>) -> Option<Self> {
702        let Some(entries) = raw.as_object() else {
703            cx.issue(IssueCode::InputParse, "expected an object");
704            return None;
705        };
706        let mut out = Self::new();
707        let mut ok = true;
708        for (key, value) in entries {
709            match cx.at(key.as_str(), |cx| T::compile(value, cx)) {
710                Some(v) => {
711                    out.insert(key.clone(), v);
712                }
713                None => ok = false,
714            }
715        }
716        ok.then_some(out)
717    }
718    fn visit(&self, v: &mut Visit<'_>) {
719        for (key, item) in self {
720            v.at(key.as_str(), |v| item.visit(v));
721        }
722    }
723    fn resolve(&self, r: &mut dyn Resolver) -> Result<Option<OwnedDataValue>> {
724        let entries = self
725            .iter()
726            .map(|(key, item)| {
727                let value = item.resolve(r)?.unwrap_or(OwnedDataValue::Null);
728                Ok((key.clone(), value))
729            })
730            .collect::<Result<Vec<_>>>()?;
731        Ok(Some(OwnedDataValue::Object(entries)))
732    }
733    fn kind() -> FieldKind {
734        FieldKind::Map(T::kind)
735    }
736}
737
738#[cfg(test)]
739mod tests {
740    use super::*;
741
742    #[test]
743    fn adopt_puts_an_issue_below_the_current_path_with_the_step_and_field() {
744        let engine = ExprEngine::default();
745        let mut issues = Vec::new();
746        let rejected = {
747            let mut cx = CompileCx::new(&engine, &mut issues)
748                .at_path("tasks[1].function.input".parse().unwrap())
749                .for_step(StepKind::Task, "fetch");
750            assert_eq!(cx.rejected(), 0);
751            cx.adopt(Issue::new(
752                IssueCode::InputParse,
753                "connector".parse().unwrap(),
754                "no connector",
755            ));
756            // An Advisory is adopted and not counted.
757            cx.adopt(Issue::new(
758                IssueCode::UnknownKey,
759                "retries".parse().unwrap(),
760                "unknown",
761            ));
762            // The path is restored: the adopted issues did not move it.
763            assert_eq!(cx.path().to_string(), "tasks[1].function.input");
764            cx.rejected()
765        };
766        assert_eq!(rejected, 1);
767        assert_eq!(issues.len(), 2);
768        let issue = &issues[0];
769        assert_eq!(issue.code, IssueCode::InputParse);
770        assert_eq!(issue.severity, Severity::Rejected);
771        assert_eq!(issue.path.to_string(), "tasks[1].function.input.connector");
772        assert_eq!(issue.field.as_deref(), Some("connector"));
773        assert_eq!(issue.step_kind, Some(StepKind::Task));
774        assert_eq!(issue.step_id.as_deref(), Some("fetch"));
775        assert_eq!(issue.message, "no connector");
776        assert_eq!(issues[1].severity, Severity::Advisory);
777        assert_eq!(issues[1].field.as_deref(), Some("retries"));
778    }
779}