use super::document::Interpolated;
use super::engine::ExprEngine;
use super::expr::{Expr, FromValue, ResolveExpr};
use crate::base::typo::unknown_key;
use crate::base::{AuthoredPath, ErrorKind, Issue, IssueCode, PathSeg, Result, Severity, StepKind};
use datavalue::{NumberValue, OwnedDataValue};
use serde_json::Value;
use std::borrow::Cow;
use std::collections::BTreeMap;
use std::fmt;
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
#[non_exhaustive]
pub enum ValueKind {
Any,
String,
Number,
Bool,
Object,
Array,
Path,
}
#[derive(Clone, Copy, Debug)]
#[non_exhaustive]
pub enum FieldKind {
Literal(ValueKind),
Expr(ValueKind),
Interpolated,
Output,
Templated,
List(fn() -> Self),
Map(fn() -> Self),
Nested(fn() -> &'static [FieldSpec]),
}
impl FieldKind {
pub(crate) fn contains_output(self) -> bool {
match self {
Self::Output => true,
Self::List(element) | Self::Map(element) => element().contains_output(),
_ => false,
}
}
}
#[derive(Clone, Debug)]
#[non_exhaustive]
pub struct FieldSpec {
pub name: Cow<'static, str>,
pub kind: FieldKind,
pub required: bool,
pub records: bool,
pub default: Option<Cow<'static, str>>,
pub primary: bool,
pub description: Option<Cow<'static, str>>,
}
impl FieldSpec {
pub fn runtime(name: impl Into<Cow<'static, str>>, kind: FieldKind, required: bool) -> Self {
Self {
name: name.into(),
kind,
required,
records: kind.contains_output(),
default: None,
primary: false,
description: None,
}
}
#[must_use]
pub fn with_default(mut self, json: impl Into<Cow<'static, str>>) -> Self {
self.default = Some(json.into());
self
}
}
pub trait ExprVisitor {
fn expr(&mut self, _at: &AuthoredPath, _raw: &Value, _records: bool) {}
#[doc(hidden)]
fn compiled_expr(&mut self, at: &AuthoredPath, expr: &dyn ResolveExpr, records: bool) {
self.expr(at, expr.raw(), records);
}
fn interpolated(&mut self, _at: &AuthoredPath, _doc: &Interpolated) {}
}
pub struct Visit<'v> {
visitor: &'v mut dyn ExprVisitor,
path: AuthoredPath,
records: bool,
}
impl<'v> Visit<'v> {
pub fn new(visitor: &'v mut dyn ExprVisitor, root: AuthoredPath) -> Self {
Self {
visitor,
path: root,
records: false,
}
}
pub fn at(&mut self, seg: impl Into<PathSeg>, f: impl FnOnce(&mut Self)) {
self.path.push(seg);
f(self);
self.path.pop();
}
pub fn recording(&mut self, records: bool, f: impl FnOnce(&mut Self)) {
let before = self.records;
self.records |= records;
f(self);
self.records = before;
}
pub fn expr(&mut self, raw: &Value) {
self.visitor.expr(&self.path, raw, self.records);
}
pub(crate) fn compiled_expr(&mut self, expr: &dyn ResolveExpr) {
self.visitor.compiled_expr(&self.path, expr, self.records);
}
pub fn interpolated(&mut self, doc: &Interpolated) {
self.visitor.interpolated(&self.path, doc);
}
}
impl fmt::Debug for Visit<'_> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("Visit")
.field("path", &format_args!("{}", self.path))
.field("records", &self.records)
.finish_non_exhaustive()
}
}
pub struct CompileCx<'a> {
exprs: &'a ExprEngine,
issues: &'a mut Vec<Issue>,
path: AuthoredPath,
root_depth: usize,
step: Option<(StepKind, String)>,
runtime_fields: Option<&'a [FieldSpec]>,
rejected: usize,
}
impl<'a> CompileCx<'a> {
pub fn new(exprs: &'a ExprEngine, issues: &'a mut Vec<Issue>) -> Self {
Self {
exprs,
issues,
path: AuthoredPath::root(),
root_depth: 0,
step: None,
runtime_fields: None,
rejected: 0,
}
}
#[must_use]
pub fn at_path(mut self, path: AuthoredPath) -> Self {
self.root_depth = path.segs().len();
self.path = path;
self
}
#[must_use]
pub fn for_step(mut self, kind: StepKind, id: impl Into<String>) -> Self {
self.step = Some((kind, id.into()));
self
}
#[must_use]
pub fn with_runtime_fields(mut self, fields: &'a [FieldSpec]) -> Self {
self.runtime_fields = Some(fields);
self
}
pub fn exprs(&self) -> &ExprEngine {
self.exprs
}
pub fn path(&self) -> &AuthoredPath {
&self.path
}
pub fn runtime_fields(&self) -> Option<&'a [FieldSpec]> {
self.runtime_fields
}
pub fn issue(&mut self, code: IssueCode, message: impl Into<String>) {
self.issue_as(code, code.severity(), message);
}
pub fn issue_as(&mut self, code: IssueCode, severity: Severity, message: impl Into<String>) {
let issue = Issue::new(code, self.path.clone(), message).with_severity(severity);
self.record(issue);
}
pub(crate) fn adopt(&mut self, mut issue: Issue) {
issue.path = self.path.extend(&issue.path);
self.record(issue);
}
fn record(&mut self, mut issue: Issue) {
if let Some((kind, id)) = &self.step {
issue = issue.with_step(*kind, Some(id));
}
if let Some(PathSeg::Key(key)) = issue.path.segs().get(self.root_depth) {
issue.field = Some(key.to_string());
}
if issue.severity == Severity::Rejected {
self.rejected += 1;
}
self.issues.push(issue);
}
pub fn at<R>(&mut self, seg: impl Into<PathSeg>, f: impl FnOnce(&mut Self) -> R) -> R {
self.path.push(seg);
let out = f(self);
self.path.pop();
out
}
pub fn rejected(&self) -> usize {
self.rejected
}
#[doc(hidden)]
pub fn unknown_key(&mut self, key: &str, known: &[&str]) {
let (code, message) = unknown_key(key, known);
self.issue_as(code, Severity::Rejected, message);
}
}
impl fmt::Debug for CompileCx<'_> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("CompileCx")
.field("path", &format_args!("{}", self.path))
.field("step", &self.step)
.field("issues", &self.issues.len())
.field("rejected", &self.rejected)
.finish_non_exhaustive()
}
}
pub(crate) mod sealed {
pub trait Sealed {}
}
pub trait Resolver: sealed::Sealed {
fn eval(&mut self, expr: &dyn ResolveExpr) -> Result<OwnedDataValue>;
fn render(&mut self, doc: &Interpolated) -> Result<OwnedDataValue>;
}
pub trait ConfigField: Sized + Send + Sync + 'static {
const NULL_IS_VALUE: bool = false;
const IS_OUTPUT: bool = false;
fn compile(raw: &Value, cx: &mut CompileCx<'_>) -> Option<Self>;
fn absent() -> Option<Self> {
None
}
fn visit(&self, _v: &mut Visit<'_>) {}
fn resolve(&self, r: &mut dyn Resolver) -> Result<Option<OwnedDataValue>>;
fn kind() -> FieldKind;
}
fn owned<T: Clone + Into<OwnedDataValue>>(v: &T) -> Result<Option<OwnedDataValue>> {
Ok(Some(v.clone().into()))
}
fn scalar<T: serde::de::DeserializeOwned>(
raw: &Value,
cx: &mut CompileCx<'_>,
expected: &str,
) -> Option<T> {
match T::deserialize(raw) {
Ok(v) => Some(v),
Err(_) => {
cx.issue(IssueCode::InputParse, format!("expected {expected}"));
None
}
}
}
macro_rules! scalar_field {
($($t:ty => $kind:ident, $expected:expr;)*) => {$(
impl ConfigField for $t {
fn compile(raw: &Value, cx: &mut CompileCx<'_>) -> Option<Self> {
scalar(raw, cx, $expected)
}
fn resolve(&self, _r: &mut dyn Resolver) -> Result<Option<OwnedDataValue>> {
owned(self)
}
fn kind() -> FieldKind {
FieldKind::Literal(ValueKind::$kind)
}
}
)*};
}
scalar_field! {
bool => Bool, "a boolean";
String => String, "a string";
u8 => Number, "an unsigned integer that fits u8";
u16 => Number, "an unsigned integer that fits u16";
u32 => Number, "an unsigned integer that fits u32";
u64 => Number, "an unsigned integer that fits u64";
usize => Number, "an unsigned integer that fits usize";
i32 => Number, "an integer that fits i32";
i64 => Number, "an integer that fits i64";
}
impl ConfigField for f64 {
fn compile(raw: &Value, cx: &mut CompileCx<'_>) -> Option<Self> {
scalar(raw, cx, "a number")
}
fn resolve(&self, _r: &mut dyn Resolver) -> Result<Option<OwnedDataValue>> {
Ok(Some(OwnedDataValue::Number(NumberValue::Float(*self))))
}
fn kind() -> FieldKind {
FieldKind::Literal(ValueKind::Number)
}
}
impl ConfigField for OwnedDataValue {
const NULL_IS_VALUE: bool = true;
fn compile(raw: &Value, _cx: &mut CompileCx<'_>) -> Option<Self> {
Some(Self::from(raw))
}
fn resolve(&self, _r: &mut dyn Resolver) -> Result<Option<OwnedDataValue>> {
owned(self)
}
fn kind() -> FieldKind {
FieldKind::Literal(ValueKind::Any)
}
}
impl ConfigField for Value {
const NULL_IS_VALUE: bool = true;
fn compile(raw: &Value, _cx: &mut CompileCx<'_>) -> Option<Self> {
Some(raw.clone())
}
fn resolve(&self, _r: &mut dyn Resolver) -> Result<Option<OwnedDataValue>> {
Ok(Some(OwnedDataValue::from(self)))
}
fn kind() -> FieldKind {
FieldKind::Literal(ValueKind::Any)
}
}
impl<T: FromValue> ConfigField for Expr<T> {
const NULL_IS_VALUE: bool = true;
fn compile(raw: &Value, cx: &mut CompileCx<'_>) -> Option<Self> {
match cx.exprs().compile::<T>(raw) {
Ok(expr) => Some(expr),
Err(error) => {
let code = match error.kind() {
ErrorKind::Config if T::KIND == ValueKind::Path && raw.is_string() => {
IssueCode::InvalidPath
}
ErrorKind::Config => IssueCode::InputParse,
_ => IssueCode::TemplateCompile,
};
cx.issue(code, error.message().to_owned());
None
}
}
}
fn visit(&self, v: &mut Visit<'_>) {
v.compiled_expr(self);
}
fn resolve(&self, r: &mut dyn Resolver) -> Result<Option<OwnedDataValue>> {
r.eval(self).map(Some)
}
fn kind() -> FieldKind {
FieldKind::Expr(T::KIND)
}
}
impl<T: ConfigField> ConfigField for Option<T> {
const NULL_IS_VALUE: bool = true;
const IS_OUTPUT: bool = T::IS_OUTPUT;
fn compile(raw: &Value, cx: &mut CompileCx<'_>) -> Option<Self> {
if raw.is_null() && !T::NULL_IS_VALUE {
return Some(None);
}
T::compile(raw, cx).map(Some)
}
fn absent() -> Option<Self> {
Some(None)
}
fn visit(&self, v: &mut Visit<'_>) {
if let Some(inner) = self {
inner.visit(v);
}
}
fn resolve(&self, r: &mut dyn Resolver) -> Result<Option<OwnedDataValue>> {
match self {
Some(inner) => inner.resolve(r),
None => Ok(None),
}
}
fn kind() -> FieldKind {
T::kind()
}
}
impl<T: ConfigField> ConfigField for Vec<T> {
fn compile(raw: &Value, cx: &mut CompileCx<'_>) -> Option<Self> {
let Some(items) = raw.as_array() else {
cx.issue(IssueCode::InputParse, "expected an array");
return None;
};
let mut out = Self::with_capacity(items.len());
let mut ok = true;
for (i, item) in items.iter().enumerate() {
match cx.at(i, |cx| T::compile(item, cx)) {
Some(v) => out.push(v),
None => ok = false,
}
}
ok.then_some(out)
}
fn visit(&self, v: &mut Visit<'_>) {
for (i, item) in self.iter().enumerate() {
v.at(i, |v| item.visit(v));
}
}
fn resolve(&self, r: &mut dyn Resolver) -> Result<Option<OwnedDataValue>> {
let items = self
.iter()
.map(|item| Ok(item.resolve(r)?.unwrap_or(OwnedDataValue::Null)))
.collect::<Result<Vec<_>>>()?;
Ok(Some(OwnedDataValue::Array(items)))
}
fn kind() -> FieldKind {
FieldKind::List(T::kind)
}
}
impl<T: ConfigField> ConfigField for BTreeMap<String, T> {
fn compile(raw: &Value, cx: &mut CompileCx<'_>) -> Option<Self> {
let Some(entries) = raw.as_object() else {
cx.issue(IssueCode::InputParse, "expected an object");
return None;
};
let mut out = Self::new();
let mut ok = true;
for (key, value) in entries {
match cx.at(key.as_str(), |cx| T::compile(value, cx)) {
Some(v) => {
out.insert(key.clone(), v);
}
None => ok = false,
}
}
ok.then_some(out)
}
fn visit(&self, v: &mut Visit<'_>) {
for (key, item) in self {
v.at(key.as_str(), |v| item.visit(v));
}
}
fn resolve(&self, r: &mut dyn Resolver) -> Result<Option<OwnedDataValue>> {
let entries = self
.iter()
.map(|(key, item)| {
let value = item.resolve(r)?.unwrap_or(OwnedDataValue::Null);
Ok((key.clone(), value))
})
.collect::<Result<Vec<_>>>()?;
Ok(Some(OwnedDataValue::Object(entries)))
}
fn kind() -> FieldKind {
FieldKind::Map(T::kind)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn adopt_puts_an_issue_below_the_current_path_with_the_step_and_field() {
let engine = ExprEngine::default();
let mut issues = Vec::new();
let rejected = {
let mut cx = CompileCx::new(&engine, &mut issues)
.at_path("tasks[1].function.input".parse().unwrap())
.for_step(StepKind::Task, "fetch");
assert_eq!(cx.rejected(), 0);
cx.adopt(Issue::new(
IssueCode::InputParse,
"connector".parse().unwrap(),
"no connector",
));
cx.adopt(Issue::new(
IssueCode::UnknownKey,
"retries".parse().unwrap(),
"unknown",
));
assert_eq!(cx.path().to_string(), "tasks[1].function.input");
cx.rejected()
};
assert_eq!(rejected, 1);
assert_eq!(issues.len(), 2);
let issue = &issues[0];
assert_eq!(issue.code, IssueCode::InputParse);
assert_eq!(issue.severity, Severity::Rejected);
assert_eq!(issue.path.to_string(), "tasks[1].function.input.connector");
assert_eq!(issue.field.as_deref(), Some("connector"));
assert_eq!(issue.step_kind, Some(StepKind::Task));
assert_eq!(issue.step_id.as_deref(), Some("fetch"));
assert_eq!(issue.message, "no connector");
assert_eq!(issues[1].severity, Severity::Advisory);
assert_eq!(issues[1].field.as_deref(), Some("retries"));
}
}