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}