use crate::dsl::*;
use crate::events::{
RunnerHeartbeatEvent, RunnerOfflineEvent, RunnerRegisterEvent, StepCompletedEvent,
StepFailedEvent, StepQueryEvent, StepQueryResponseEvent, StepRunningEvent, StepScheduledEvent,
WorkflowAbortedEvent, WorkflowCompletedEvent, WorkflowFailedEvent, WorkflowQueuedEvent,
WorkflowRunningEvent, WorkflowStartPendingEvent,
};
use schemars::schema::{ObjectValidation, RootSchema, Schema, SchemaObject, SubschemaValidation};
use schemars::{schema_for, Map};
pub fn generate_event_schemas() -> HashMap<String, RootSchema> {
let mut schemas = HashMap::new();
schemas.insert(
"WorkflowQueuedEvent".to_string(),
schema_for!(WorkflowQueuedEvent),
);
schemas.insert(
"WorkflowStartPendingEvent".to_string(),
schema_for!(WorkflowStartPendingEvent),
);
schemas.insert(
"WorkflowRunningEvent".to_string(),
schema_for!(WorkflowRunningEvent),
);
schemas.insert(
"WorkflowCompletedEvent".to_string(),
schema_for!(WorkflowCompletedEvent),
);
schemas.insert(
"WorkflowFailedEvent".to_string(),
schema_for!(WorkflowFailedEvent),
);
schemas.insert(
"WorkflowAbortedEvent".to_string(),
schema_for!(WorkflowAbortedEvent),
);
schemas.insert(
"StepScheduledEvent".to_string(),
schema_for!(StepScheduledEvent),
);
schemas.insert(
"StepRunningEvent".to_string(),
schema_for!(StepRunningEvent),
);
schemas.insert(
"StepCompletedEvent".to_string(),
schema_for!(StepCompletedEvent),
);
schemas.insert("StepFailedEvent".to_string(), schema_for!(StepFailedEvent));
schemas.insert("StepQueryEvent".to_string(), schema_for!(StepQueryEvent));
schemas.insert(
"StepQueryResponseEvent".to_string(),
schema_for!(StepQueryResponseEvent),
);
schemas.insert(
"RunnerRegisterEvent".to_string(),
schema_for!(RunnerRegisterEvent),
);
schemas.insert(
"RunnerHeartbeatEvent".to_string(),
schema_for!(RunnerHeartbeatEvent),
);
schemas.insert(
"RunnerOfflineEvent".to_string(),
schema_for!(RunnerOfflineEvent),
);
schemas
}
use serde_json::Value;
use std::collections::HashMap;
pub fn apply_step_extensibility(
root_schema: &mut RootSchema,
spec_schemas: &HashMap<String, Schema>,
) {
if let Some(Schema::Object(step_obj)) = root_schema.definitions.get_mut("Step") {
let mut all_of = Vec::new();
if let Some(subschemas) = &mut step_obj.subschemas {
if let Some(existing_all_of) = &subschemas.all_of {
all_of.extend(existing_all_of.clone());
}
}
for (type_name, spec_schema) in spec_schemas {
let mut if_schema = SchemaObject::default();
let mut properties = Map::new();
let type_schema = SchemaObject {
const_value: Some(Value::String(type_name.clone())),
..Default::default()
};
properties.insert("type".to_string(), Schema::Object(type_schema));
if_schema.object = Some(Box::new(ObjectValidation {
properties,
required: std::collections::BTreeSet::from(["type".to_string()]),
..Default::default()
}));
let mut then_schema = SchemaObject::default();
let mut then_props = Map::new();
then_props.insert("spec".to_string(), spec_schema.clone());
then_schema.object = Some(Box::new(ObjectValidation {
properties: then_props,
required: std::collections::BTreeSet::from(["spec".to_string()]),
..Default::default()
}));
let condition = SchemaObject {
subschemas: Some(Box::new(SubschemaValidation {
if_schema: Some(Box::new(Schema::Object(if_schema))),
then_schema: Some(Box::new(Schema::Object(then_schema))),
..Default::default()
})),
..Default::default()
};
all_of.push(Schema::Object(condition));
}
if let Some(subschemas) = &mut step_obj.subschemas {
subschemas.all_of = Some(all_of);
} else {
step_obj.subschemas = Some(Box::new(SubschemaValidation {
all_of: Some(all_of),
..Default::default()
}));
}
if let Some(obj) = step_obj.object.as_mut() {
obj.additional_properties = Some(Box::new(Schema::Bool(true)));
}
}
}
pub fn generate_dsl_schema() -> RootSchema {
let mut generator = schemars::gen::SchemaSettings::draft07()
.with(|s| s.option_nullable = true)
.into_generator();
let mut spec_schemas = HashMap::new();
spec_schemas.insert(
"RunContainer".to_string(),
generator.subschema_for::<CommonContainerSpec>(),
);
spec_schemas.insert(
"RunK8sJob".to_string(),
generator.subschema_for::<K8sJobSpec>(),
);
spec_schemas.insert(
"Approval".to_string(),
generator.subschema_for::<ApprovalSpec>(),
);
spec_schemas.insert(
"WaitEvent".to_string(),
generator.subschema_for::<WaitEventSpec>(),
);
spec_schemas.insert(
"LambdaInvoke".to_string(),
generator.subschema_for::<LambdaInvokeSpec>(),
);
spec_schemas.insert(
"GitCheckout".to_string(),
generator.subschema_for::<GitCheckoutSpec>(),
);
spec_schemas.insert(
"Wasm".to_string(),
generator.subschema_for::<WasmStepSpec>(),
);
spec_schemas.insert(
"SlackMessage".to_string(),
generator.subschema_for::<SlackMessageSpec>(),
);
spec_schemas.insert(
"TeamsMessage".to_string(),
generator.subschema_for::<TeamsMessageSpec>(),
);
spec_schemas.insert(
"WebhookInvoke".to_string(),
generator.subschema_for::<WebhookInvokeSpec>(),
);
spec_schemas.insert(
"RestApi".to_string(),
generator.subschema_for::<RestApiSpec>(),
);
spec_schemas.insert(
"SqlExecute".to_string(),
generator.subschema_for::<SqlExecuteSpec>(),
);
spec_schemas.insert("Email".to_string(), generator.subschema_for::<EmailSpec>());
spec_schemas.insert(
"JinjaRender".to_string(),
generator.subschema_for::<JinjaRenderSpec>(),
);
spec_schemas.insert(
"TestReportEmail".to_string(),
generator.subschema_for::<TestReportEmailSpec>(),
);
spec_schemas.insert("Jq".to_string(), generator.subschema_for::<JqSpec>());
let mut root_schema = generator.root_schema_for::<Workflow>();
apply_step_extensibility(&mut root_schema, &spec_schemas);
root_schema.meta_schema = None;
root_schema
}
pub fn flatten_schema_for_ui(schema: &Value, inputs: &Value) -> Value {
let mut flat = schema.clone();
fn recursive_flatten(node: &mut Value, current_inputs: &Value) {
if let Some(obj) = node.as_object_mut() {
if let Some(props) = obj.get_mut("properties").and_then(|v| v.as_object_mut()) {
for v in props.values_mut() {
recursive_flatten(v, current_inputs);
}
}
if let Some(all_of) = obj.get("allOf").and_then(|v| v.as_array()) {
let mut splices: Vec<(Option<String>, serde_json::Map<String, Value>)> = Vec::new();
let mut merged_required = Vec::new();
for condition in all_of {
let mut matches = true;
let mut last_trigger_field = None;
if let Some(if_props) = condition
.get("if")
.and_then(|i| i.get("properties"))
.and_then(|p| p.as_object())
{
for (k, v) in if_props {
last_trigger_field = Some(k.clone());
if let Some(const_val) = v.get("const") {
if current_inputs.get(k) != Some(const_val) {
matches = false;
break;
}
} else {
matches = false;
break;
}
}
} else {
matches = false; }
if matches {
if let Some(then_props) = condition
.get("then")
.and_then(|t| t.get("properties"))
.and_then(|p| p.as_object())
{
splices.push((last_trigger_field, then_props.clone()));
}
if let Some(then_req) = condition
.get("then")
.and_then(|t| t.get("required"))
.and_then(|r| r.as_array())
{
for r in then_req {
if let Some(rs) = r.as_str() {
merged_required.push(Value::String(rs.to_string()));
}
}
}
}
}
if !splices.is_empty() {
if let Some(props) = obj.get_mut("properties").and_then(|v| v.as_object_mut()) {
let mut new_props = serde_json::Map::new();
let mut processed_splices = vec![false; splices.len()];
for (k, v) in props.iter() {
new_props.insert(k.clone(), v.clone());
for (i, (trigger, then_props)) in splices.iter().enumerate() {
if !processed_splices[i] && trigger.as_ref() == Some(k) {
for (tk, tv) in then_props {
new_props.insert(tk.clone(), tv.clone());
}
processed_splices[i] = true;
}
}
}
for (i, (_, then_props)) in splices.iter().enumerate() {
if !processed_splices[i] {
for (tk, tv) in then_props {
new_props.insert(tk.clone(), tv.clone());
}
}
}
*props = new_props;
} else {
let mut merged_props = serde_json::Map::new();
for (_, then_props) in splices {
for (tk, tv) in then_props {
merged_props.insert(tk, tv);
}
}
obj.insert("properties".to_string(), Value::Object(merged_props));
}
}
if !merged_required.is_empty() {
if let Some(reqs) = obj.get_mut("required").and_then(|v| v.as_array_mut()) {
for r in merged_required {
if !reqs.contains(&r) {
reqs.push(r);
}
}
} else {
obj.insert("required".to_string(), Value::Array(merged_required));
}
}
}
}
}
recursive_flatten(&mut flat, inputs);
flat
}
#[cfg(test)]
mod tests {
use super::generate_dsl_schema;
#[test]
fn test_generate_dsl_schema_registers_rest_api_spec() {
let schema = generate_dsl_schema();
let rest_api_schema = schema
.definitions
.get("RestApiSpec")
.expect("generated DSL schema should include the RestApiSpec definition");
let rest_api_schema_json = serde_json::to_value(rest_api_schema)
.expect("RestApiSpec schema should serialize to JSON value");
let properties = rest_api_schema_json
.get("properties")
.and_then(serde_json::Value::as_object)
.expect("RestApiSpec schema should expose object properties");
assert!(
properties.contains_key("url"),
"RestApiSpec schema should include the url property"
);
assert!(
properties.contains_key("extractors"),
"RestApiSpec schema should include the extractors property"
);
}
#[test]
fn test_generate_dsl_schema_registers_sql_execute_spec() {
let schema = generate_dsl_schema();
let sql_execute_schema = schema
.definitions
.get("SqlExecuteSpec")
.expect("generated DSL schema should include the SqlExecuteSpec definition");
let sql_execute_schema_json = serde_json::to_value(sql_execute_schema)
.expect("SqlExecuteSpec schema should serialize to JSON value");
let properties = sql_execute_schema_json
.get("properties")
.and_then(serde_json::Value::as_object)
.expect("SqlExecuteSpec schema should expose object properties");
assert!(
properties.contains_key("connection"),
"SqlExecuteSpec schema should include the connection property"
);
assert!(
properties.contains_key("query"),
"SqlExecuteSpec schema should include the query property"
);
}
#[test]
fn test_flatten_schema_for_ui() {
use super::flatten_schema_for_ui;
use serde_json::json;
let schema = json!({
"properties": {
"type": { "type": "string" }
},
"allOf": [
{
"if": { "properties": { "type": { "const": "RunK8sJob" } } },
"then": { "properties": { "spec": { "type": "object" } }, "required": ["spec"] }
}
]
});
let inputs = json!({
"type": "RunK8sJob"
});
let flat = flatten_schema_for_ui(&schema, &inputs);
let properties = flat.get("properties").unwrap().as_object().unwrap();
assert!(properties.contains_key("spec"));
let required = flat.get("required").unwrap().as_array().unwrap();
assert!(required.contains(&json!("spec")));
let inputs_no_match = json!({
"type": "Other"
});
let flat_no_match = flatten_schema_for_ui(&schema, &inputs_no_match);
let properties_no_match = flat_no_match
.get("properties")
.unwrap()
.as_object()
.unwrap();
assert!(!properties_no_match.contains_key("spec"));
}
#[test]
fn test_flatten_schema_for_ui_ordering() {
use super::flatten_schema_for_ui;
use serde_json::json;
let schema = json!({
"properties": {
"first_field": { "type": "string" },
"trigger_field": { "type": "string" },
"last_field": { "type": "string" }
},
"allOf": [
{
"if": { "properties": { "trigger_field": { "const": "show_more" } } },
"then": { "properties": { "cond_field1": { "type": "string" } } }
}
]
});
let inputs = json!({
"trigger_field": "show_more"
});
let flat = flatten_schema_for_ui(&schema, &inputs);
let props = flat.get("properties").unwrap().as_object().unwrap();
let keys: Vec<_> = props.keys().map(|s| s.as_str()).collect();
assert_eq!(
keys,
vec!["first_field", "trigger_field", "cond_field1", "last_field"]
);
}
}