use serde::Deserialize;
use std::collections::HashMap;
#[derive(Debug, Clone, serde::Serialize, Deserialize)]
pub struct RuntimeCapabilities {
pub runtime: String, pub version: String, pub tools: Vec<String>,
pub features: Vec<String>,
}
impl RuntimeCapabilities {
pub fn local() -> Self {
Self {
runtime: "local".to_string(),
version: "noetl-runtime/1".to_string(),
tools: vec![
"shell".to_string(),
"http".to_string(),
"duckdb".to_string(),
"rhai".to_string(),
"playbook".to_string(),
"auth".to_string(),
"sink".to_string(),
],
features: vec![
"case_v1".to_string(),
"case_v2".to_string(), "loop_v1".to_string(),
"vars_v1".to_string(),
"jinja2".to_string(),
],
}
}
#[allow(dead_code)]
pub fn distributed() -> Self {
Self {
runtime: "distributed".to_string(),
version: "noetl-runtime/1".to_string(),
tools: vec![
"shell".to_string(),
"http".to_string(),
"postgres".to_string(),
"duckdb".to_string(),
"python".to_string(),
"playbook".to_string(),
"iterator".to_string(),
],
features: vec![
"case_v1".to_string(),
"case_v2".to_string(),
"loop_v1".to_string(),
"loop_v2".to_string(), "vars_v1".to_string(),
"vars_v2".to_string(), "sink_v1".to_string(),
"jinja2".to_string(),
"event_sourcing".to_string(),
],
}
}
}
#[derive(Debug, Deserialize)]
pub struct Playbook {
#[serde(rename = "apiVersion")]
pub api_version: String,
#[allow(dead_code)]
pub kind: String,
pub metadata: Metadata,
#[serde(default)]
pub executor: Option<Executor>,
pub workload: Option<HashMap<String, serde_yaml::Value>>,
pub workflow: Vec<Step>,
}
#[derive(Debug, Deserialize, Default)]
pub struct Executor {
#[serde(default = "default_profile")]
pub profile: String,
#[serde(default = "default_version")]
pub version: String,
#[serde(default)]
pub requires: Option<ExecutorRequires>,
#[serde(default)]
pub spec: Option<ExecutorSpec>,
}
#[derive(Debug, Deserialize, Default, Clone)]
pub struct ExecutorSpec {
#[serde(default)]
pub entry_step: Option<String>,
#[serde(default)]
pub final_step: Option<String>,
#[serde(default)]
pub no_next_is_error: Option<bool>,
}
pub fn default_profile() -> String {
"auto".to_string()
}
pub fn default_version() -> String {
"noetl-runtime/1".to_string()
}
#[derive(Debug, Deserialize, Default)]
pub struct ExecutorRequires {
#[serde(default)]
pub tools: Vec<String>,
#[serde(default)]
pub features: Vec<String>,
}
#[derive(Debug, Deserialize)]
pub struct Metadata {
pub name: String,
#[allow(dead_code)]
pub path: Option<String>,
}
#[derive(Debug, Deserialize)]
pub struct Step {
pub step: String,
pub desc: Option<String>,
#[serde(rename = "when")]
pub when_guard: Option<String>,
#[serde(default)]
pub input: Option<HashMap<String, serde_yaml::Value>>,
pub tool: Option<Tool>,
#[serde(default)]
pub next: Option<serde_yaml::Value>,
#[serde(rename = "case")]
pub case: Option<Vec<CaseCondition>>,
#[serde(rename = "loop")]
#[allow(dead_code)]
pub loop_config: Option<LoopConfig>,
pub vars: Option<HashMap<String, String>>,
#[serde(default)]
pub spec: Option<StepSpec>,
}
#[derive(Debug, Deserialize, Default, Clone)]
pub struct StepSpec {
#[serde(default)]
pub next_mode: Option<NextMode>,
}
#[derive(Debug, Deserialize, Clone, Default)]
#[serde(rename_all = "lowercase")]
pub enum NextMode {
#[default]
Exclusive,
Inclusive,
}
#[derive(Debug, Clone)]
pub struct NextRouterSpec {
pub mode: Option<String>,
}
#[derive(Debug, Clone)]
pub struct NextArc {
pub step: String,
pub when_condition: Option<String>,
pub args: Option<HashMap<String, serde_yaml::Value>>,
}
#[derive(Debug)]
pub enum NextFormat {
Router {
spec: Option<NextRouterSpec>,
arcs: Vec<NextArc>,
},
Array(Vec<NextStep>),
}
impl NextFormat {
pub fn from_yaml_value(value: &serde_yaml::Value) -> Option<NextFormat> {
match value {
serde_yaml::Value::Sequence(_arr) => {
let steps: Vec<NextStep> = serde_yaml::from_value(value.clone()).ok()?;
Some(NextFormat::Array(steps))
}
serde_yaml::Value::Mapping(map) => {
let spec = map.get(&serde_yaml::Value::String("spec".to_string())).and_then(|v| {
if let serde_yaml::Value::Mapping(spec_map) = v {
let mode = spec_map
.get(&serde_yaml::Value::String("mode".to_string()))
.and_then(|m| m.as_str().map(|s| s.to_string()));
Some(NextRouterSpec { mode })
} else {
None
}
});
let arcs = map.get(&serde_yaml::Value::String("arcs".to_string())).and_then(|v| {
if let serde_yaml::Value::Sequence(arcs_arr) = v {
let arcs: Vec<NextArc> = arcs_arr
.iter()
.filter_map(|arc_val| {
if let serde_yaml::Value::Mapping(arc_map) = arc_val {
let step = arc_map
.get(&serde_yaml::Value::String("step".to_string()))
.and_then(|s| s.as_str().map(|s| s.to_string()))?;
let when_condition = arc_map
.get(&serde_yaml::Value::String("when".to_string()))
.and_then(|w| w.as_str().map(|s| s.to_string()));
let args = arc_map
.get(&serde_yaml::Value::String("args".to_string()))
.and_then(|a| serde_yaml::from_value(a.clone()).ok());
Some(NextArc {
step,
when_condition,
args,
})
} else {
None
}
})
.collect();
Some(arcs)
} else {
None
}
})?;
Some(NextFormat::Router { spec, arcs })
}
_ => None,
}
}
}
#[derive(Debug, Deserialize)]
#[serde(untagged)]
pub enum ThenBlock {
Single(serde_yaml::Value),
List(Vec<NextStep>),
}
#[derive(Debug, Deserialize)]
pub struct CaseCondition {
#[serde(flatten)]
pub when: WhenCondition,
pub then: ThenBlock,
#[serde(rename = "else")]
pub else_steps: Option<Vec<NextStep>>,
}
#[derive(Debug, Deserialize)]
#[serde(untagged)]
pub enum WhenCondition {
Rhai {
#[serde(alias = "when_rhai")]
rhai: String,
},
Simple { when: String },
}
#[derive(Debug, Deserialize)]
#[serde(tag = "kind", rename_all = "lowercase")]
pub enum Tool {
Shell {
#[serde(default)]
cmds: CmdsList,
},
Http {
#[serde(default = "default_method")]
method: String,
url: String,
#[serde(default)]
headers: HashMap<String, String>,
#[serde(default)]
params: HashMap<String, String>,
body: Option<String>,
#[serde(default)]
auth: Option<AuthConfig>,
},
Playbook {
path: String,
#[serde(default)]
args: HashMap<String, String>,
#[serde(default)]
input: HashMap<String, serde_yaml::Value>,
},
#[serde(rename = "duckdb")]
DuckDb {
#[serde(default = "default_duckdb_path")]
db: String,
query: Option<String>,
#[serde(default)]
params: Vec<String>,
},
Auth {
provider: String,
#[serde(default)]
scopes: Vec<String>,
#[serde(default)]
project: Option<String>,
},
Sink {
target: SinkTarget,
#[serde(default)]
format: SinkFormat,
},
Rhai {
code: String,
#[serde(default)]
args: HashMap<String, String>,
},
#[serde(other)]
Unsupported,
}
pub fn default_method() -> String {
"GET".to_string()
}
pub fn default_duckdb_path() -> String {
".noetl/state.duckdb".to_string()
}
#[derive(Debug, Deserialize, Clone)]
pub struct AuthConfig {
#[serde(alias = "source")]
pub provider: String,
#[serde(default)]
pub scopes: Vec<String>,
}
#[derive(Debug, Deserialize)]
#[serde(tag = "type", rename_all = "lowercase")]
pub enum SinkTarget {
File {
path: String,
},
#[serde(rename = "duckdb")]
DuckDb {
db: String,
table: String,
},
Gcs {
bucket: String,
path: String,
},
}
#[derive(Debug, Deserialize, Default)]
#[serde(rename_all = "lowercase")]
pub enum SinkFormat {
#[default]
Json,
Yaml,
Csv,
}
#[derive(Debug, Deserialize)]
#[serde(untagged)]
pub enum CmdsList {
Single(String),
Multiple(Vec<String>),
}
impl Default for CmdsList {
fn default() -> Self {
CmdsList::Multiple(vec![])
}
}
#[derive(Debug, Deserialize)]
#[serde(untagged)]
pub enum NextStep {
Canonical {
step: String,
#[serde(rename = "when")]
when_condition: Option<String>,
#[serde(default)]
args: Option<HashMap<String, serde_yaml::Value>>,
},
Conditional { when: Option<String>, then: Vec<NextStep> },
NextAction { next: Vec<NextStep> },
}
#[derive(Debug, Deserialize)]
#[allow(dead_code)]
pub struct LoopConfig {
#[serde(rename = "in")]
pub in_collection: String,
pub iterator: String,
pub mode: Option<String>,
}