Skip to main content

dataflow_rs/compat/
mod.rs

1//! Compatibility with v3 definitions and behaviour.
2//!
3//! Every v3 definition loads through the compat reader, which
4//! [`parse`](crate::def::parse) runs first when
5//! [`ParseOptions::compat`](crate::def::ParseOptions::compat) is on (the
6//! default). It rewrites each v3 spelling into v4's and reports the rewrite
7//! as a `V3_SPELLING` Advisory at the v3 key:
8//!
9//! | Where | v3 spelling | v4 spelling |
10//! | --- | --- | --- |
11//! | Workflow | `continue_on_error: true` / `false` | `on_failure: continue` / `fail` |
12//! | Task | `continue_on_error: true` | `on_failure: continue`, `on_rejection: continue` |
13//! | Task | `halt_on: "failure"` | `on_failure: fail`, `on_rejection: halt` |
14//! | Task | both | `on_failure: halt`, `on_rejection: halt` |
15//! | Task | `halt_on: "never"`, `continue_on_error: false` | the defaults, still reported |
16//! | `function.name` | `validate` | `validation` |
17//!
18//! It moves two kinds of key into a node's `ext`, reporting each as the key
19//! it is:
20//!
21//! - a key a built-in's input does not know, which v3's configs ignored, as
22//!   `function.input.<key>`, and an unknown key of a `map` mapping or a
23//!   `validation` rule, as `function.input.mappings[0].<key>` (an
24//!   `UNKNOWN_KEY`, or a `LIKELY_TYPO`);
25//! - a group's `continue_on_error` (a `GROUP_CONTINUE_ON_ERROR` when `true`,
26//!   which v3 reported and never honoured, otherwise an `UNKNOWN_KEY`).
27//!
28//! It refuses a `continue_on_error` that is not a bool and a `halt_on` that
29//! is not `"never"` or `"failure"`. It refuses a v3 key written beside a v4
30//! one, at the v3 key: on one node, any v3 policy key (`continue_on_error`,
31//! `halt_on`) beside any v4 policy key (`on_failure`, `on_rejection`).
32//!
33//! With the reader off, nothing is rewritten. The v3 keys of a workflow, a
34//! task and a group (`continue_on_error`, `halt_on`) are unknown keys, and
35//! Rejected as every unknown key then is. `validate` and a built-in
36//! input's stray keys are not keys of the definition: they stay
37//! in the task's `function` as written, for the function's lookup and config
38//! to refuse when the workflow is compiled.
39//!
40//! [`migrate_v3`] parses with the reader on and writes the definition in the
41//! v4 spelling, which parses with the reader off to an equal definition.
42//!
43//! ```
44//! use dataflow_rs::compat::migrate_v3;
45//! use serde_json::json;
46//!
47//! let migrated = migrate_v3(&json!({
48//!     "id": "w",
49//!     "tasks": [{"id": "t", "continue_on_error": true, "function": {"name": "f", "input": {}}}]
50//! }))
51//! .unwrap();
52//! assert_eq!(
53//!     migrated.json,
54//!     json!({"id": "w", "tasks": [{
55//!         "id": "t", "on_failure": "continue", "function": {"name": "f", "input": {}}
56//!     }]})
57//! );
58//! assert_eq!(migrated.rewrites[0].path.to_string(), "tasks[0].continue_on_error");
59//! assert_eq!(
60//!     migrated.rewrites[0].kind,
61//!     dataflow_rs::compat::RewriteKind::Policy {
62//!         v3: "continue_on_error: true".into(),
63//!         v4: vec![
64//!             ("on_failure".into(), "continue".into()),
65//!             ("on_rejection".into(), "continue".into()),
66//!         ],
67//!     }
68//! );
69//! assert_eq!(
70//!     migrated.rewrites[0].to_string(),
71//!     "`continue_on_error: true` is a v3 spelling, read as `on_failure: continue, on_rejection: continue`"
72//! );
73//! ```
74
75pub use crate::def::reader::{Rewrite, RewriteKind};
76
77use crate::base::any_rejected;
78use crate::def::{ParseOptions, parse_with_rewrites};
79use crate::{Issue, WorkflowDef};
80use serde_json::Value;
81
82/// A v3 definition in the v4 spelling: what [`migrate_v3`] returns.
83#[derive(Clone, Debug, PartialEq)]
84#[non_exhaustive]
85pub struct Migrated {
86    /// The definition, parsed: what [`json`](Self::json) parses to with the
87    /// reader off, so a host loading it need not parse it again. A host that
88    /// wants the migrated definition's *text* serializes this
89    /// (`serde_json::to_string(&migrated.def)`), which is why the structured
90    /// form below is a `Value` and not a `String`: it keeps the definition's
91    /// key order, is inspectable without a re-parse, and is exactly what this
92    /// field serializes to.
93    pub def: WorkflowDef,
94    /// The definition in the v4 spelling, its keys in the definition's
95    /// order: `serde_json::to_value(&def)`, usable on its own without a
96    /// [`WorkflowDef`]. See [`def`](Self::def) for the text contract.
97    pub json: Value,
98    /// Each v3 spelling rewritten, in document order.
99    pub rewrites: Vec<Rewrite>,
100    /// Every issue the definition has, none Rejected: the rewrites' own
101    /// `V3_SPELLING`s among them.
102    pub issues: Vec<Issue>,
103}
104
105/// Parse with compat on and serialize the definition in the v4 spelling. Err
106/// holds every issue when any is Rejected; otherwise `issues` holds the rest.
107pub fn migrate_v3(json: &Value) -> std::result::Result<Migrated, Vec<Issue>> {
108    let (parsed, rewrites) = parse_with_rewrites(json, ParseOptions::default().compat(true));
109    let Some(def) = parsed.def else {
110        debug_assert!(any_rejected(&parsed.issues));
111        return Err(parsed.issues);
112    };
113    let json =
114        serde_json::to_value(&def).expect("a definition serializes to JSON: every key is a string");
115    Ok(Migrated {
116        def,
117        json,
118        rewrites,
119        issues: parsed.issues,
120    })
121}