1use std::collections::BTreeMap;
2
3use serde::{Deserialize, Serialize};
4use serde_json::Value;
5
6use super::{WorkflowDagPlan, WorkflowDslCompatibility, WorkflowDslError};
7
8pub const WORKFLOW_DSL_MAX_BYTES: usize = 10 * 1024 * 1024;
10pub const TESTED_WORKFLOW_DSL_VERSION: &str = "0.7.0";
12
13#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
15pub struct WorkflowDsl {
16 version: String,
17 kind: String,
18 app: WorkflowDslApp,
19 #[serde(default)]
20 dependencies: Vec<Value>,
21 workflow: WorkflowDslBody,
22 #[serde(flatten)]
23 extensions: BTreeMap<String, Value>,
24}
25
26impl WorkflowDsl {
27 pub fn from_yaml(source: &str) -> Result<Self, WorkflowDslError> {
29 check_size(source)?;
30 let document: Self =
31 serde_yaml_ng::from_str(source).map_err(|error| WorkflowDslError::InvalidYaml {
32 message: error.to_string(),
33 })?;
34 document.validate_document()?;
35 Ok(document)
36 }
37
38 pub fn from_json(source: &str) -> Result<Self, WorkflowDslError> {
40 check_size(source)?;
41 let document: Self =
42 serde_json::from_str(source).map_err(|error| WorkflowDslError::InvalidJson {
43 message: error.to_string(),
44 })?;
45 document.validate_document()?;
46 Ok(document)
47 }
48
49 pub fn to_yaml(&self) -> Result<String, WorkflowDslError> {
51 serde_yaml_ng::to_string(self).map_err(|error| WorkflowDslError::Serialization {
52 message: error.to_string(),
53 })
54 }
55
56 pub fn to_json(&self) -> Result<String, WorkflowDslError> {
58 serde_json::to_string(self).map_err(|error| WorkflowDslError::Serialization {
59 message: error.to_string(),
60 })
61 }
62
63 pub fn version(&self) -> &str {
65 &self.version
66 }
67
68 pub fn kind(&self) -> &str {
70 &self.kind
71 }
72
73 pub fn app(&self) -> &WorkflowDslApp {
75 &self.app
76 }
77
78 pub fn dependencies(&self) -> &[Value] {
80 &self.dependencies
81 }
82
83 pub fn workflow(&self) -> &WorkflowDslBody {
85 &self.workflow
86 }
87
88 pub fn graph(&self) -> &WorkflowDag {
90 &self.workflow.graph
91 }
92
93 pub fn extensions(&self) -> &BTreeMap<String, Value> {
95 &self.extensions
96 }
97
98 pub fn execution_digest(&self) -> Result<String, WorkflowDslError> {
100 super::digest::document_execution_digest(self)
101 }
102
103 pub fn compatibility(&self) -> Result<WorkflowDslCompatibility, WorkflowDslError> {
107 super::version::classify_dsl_version(&self.version)
108 }
109
110 fn validate_document(&self) -> Result<(), WorkflowDslError> {
111 if self.version.trim().is_empty() {
112 return Err(invalid_document("version is empty"));
113 }
114 if self.kind != "app" {
115 return Err(invalid_document(format!(
116 "kind {:?} is not a workflow app",
117 self.kind
118 )));
119 }
120 if self.app.name.trim().is_empty() {
121 return Err(invalid_document("app.name is empty"));
122 }
123 if self.app.mode != "workflow" {
124 return Err(invalid_document(format!(
125 "app.mode {:?} must be workflow",
126 self.app.mode
127 )));
128 }
129 self.compatibility()?;
130 Ok(())
131 }
132}
133
134#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
136pub struct WorkflowDslApp {
137 name: String,
138 mode: String,
139 #[serde(flatten)]
140 extensions: BTreeMap<String, Value>,
141}
142
143impl WorkflowDslApp {
144 pub fn name(&self) -> &str {
146 &self.name
147 }
148
149 pub fn mode(&self) -> &str {
151 &self.mode
152 }
153
154 pub fn extensions(&self) -> &BTreeMap<String, Value> {
156 &self.extensions
157 }
158}
159
160#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
162pub struct WorkflowDslBody {
163 graph: WorkflowDag,
164 #[serde(flatten)]
165 extensions: BTreeMap<String, Value>,
166}
167
168impl WorkflowDslBody {
169 pub fn graph(&self) -> &WorkflowDag {
171 &self.graph
172 }
173
174 pub fn extensions(&self) -> &BTreeMap<String, Value> {
176 &self.extensions
177 }
178}
179
180#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
182pub struct WorkflowDag {
183 #[serde(default)]
184 nodes: Vec<WorkflowDagNode>,
185 #[serde(default)]
186 edges: Vec<WorkflowDagEdge>,
187 #[serde(default, skip_serializing_if = "Option::is_none")]
188 viewport: Option<Value>,
189 #[serde(flatten)]
190 extensions: BTreeMap<String, Value>,
191}
192
193impl WorkflowDag {
194 pub fn new(nodes: Vec<WorkflowDagNode>, edges: Vec<WorkflowDagEdge>) -> Self {
198 Self {
199 nodes,
200 edges,
201 viewport: None,
202 extensions: BTreeMap::new(),
203 }
204 }
205
206 pub fn from_json(source: &str) -> Result<Self, WorkflowDslError> {
208 check_size(source)?;
209 serde_json::from_str(source).map_err(|error| WorkflowDslError::InvalidJson {
210 message: error.to_string(),
211 })
212 }
213
214 pub fn from_yaml(source: &str) -> Result<Self, WorkflowDslError> {
216 check_size(source)?;
217 serde_yaml_ng::from_str(source).map_err(|error| WorkflowDslError::InvalidYaml {
218 message: error.to_string(),
219 })
220 }
221
222 pub fn to_json(&self) -> Result<String, WorkflowDslError> {
224 serde_json::to_string(self).map_err(|error| WorkflowDslError::Serialization {
225 message: error.to_string(),
226 })
227 }
228
229 pub fn nodes(&self) -> &[WorkflowDagNode] {
231 &self.nodes
232 }
233
234 pub fn edges(&self) -> &[WorkflowDagEdge] {
236 &self.edges
237 }
238
239 pub fn viewport(&self) -> Option<&Value> {
241 self.viewport.as_ref()
242 }
243
244 pub fn extensions(&self) -> &BTreeMap<String, Value> {
246 &self.extensions
247 }
248
249 pub fn node(&self, id: &str) -> Option<&WorkflowDagNode> {
251 self.nodes.iter().find(|node| node.id == id)
252 }
253
254 pub fn execution_plan(&self) -> Result<WorkflowDagPlan, WorkflowDslError> {
256 super::plan::build_execution_plan(self)
257 }
258
259 pub fn execution_digest(&self) -> Result<String, WorkflowDslError> {
262 super::digest::graph_execution_digest(self)
263 }
264}
265
266#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
268pub struct WorkflowDagNode {
269 id: String,
270 data: Value,
271 #[serde(rename = "parentId", default, skip_serializing_if = "Option::is_none")]
272 parent_id: Option<String>,
273 #[serde(flatten)]
274 presentation: BTreeMap<String, Value>,
275}
276
277impl WorkflowDagNode {
278 pub fn new(id: impl Into<String>, node_type: impl Into<String>) -> Self {
280 Self::from_data(
281 id,
282 Value::Object(serde_json::Map::from_iter([(
283 "type".to_owned(),
284 Value::String(node_type.into()),
285 )])),
286 )
287 }
288
289 pub fn from_data(id: impl Into<String>, data: Value) -> Self {
291 Self {
292 id: id.into(),
293 data,
294 parent_id: None,
295 presentation: BTreeMap::new(),
296 }
297 }
298
299 pub fn with_parent_id(mut self, parent_id: impl Into<String>) -> Self {
301 self.parent_id = Some(parent_id.into());
302 self
303 }
304
305 pub fn id(&self) -> &str {
307 &self.id
308 }
309
310 pub fn node_type(&self) -> &str {
312 self.data.get("type").and_then(Value::as_str).unwrap_or("")
313 }
314
315 pub fn data(&self) -> &Value {
317 &self.data
318 }
319
320 pub fn parent_id(&self) -> Option<&str> {
322 self.parent_id.as_deref()
323 }
324
325 pub fn presentation(&self) -> &BTreeMap<String, Value> {
327 &self.presentation
328 }
329}
330
331#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
333pub struct WorkflowDagEdge {
334 id: String,
335 source: String,
336 target: String,
337 #[serde(
338 rename = "sourceHandle",
339 default,
340 skip_serializing_if = "Option::is_none"
341 )]
342 source_handle: Option<String>,
343 #[serde(
344 rename = "targetHandle",
345 default,
346 skip_serializing_if = "Option::is_none"
347 )]
348 target_handle: Option<String>,
349 #[serde(default)]
350 data: Value,
351 #[serde(flatten)]
352 presentation: BTreeMap<String, Value>,
353}
354
355impl WorkflowDagEdge {
356 pub fn new(
358 id: impl Into<String>,
359 source: impl Into<String>,
360 target: impl Into<String>,
361 ) -> Self {
362 Self {
363 id: id.into(),
364 source: source.into(),
365 target: target.into(),
366 source_handle: None,
367 target_handle: None,
368 data: Value::Null,
369 presentation: BTreeMap::new(),
370 }
371 }
372
373 pub fn with_source_handle(mut self, source_handle: impl Into<String>) -> Self {
375 self.source_handle = Some(source_handle.into());
376 self
377 }
378
379 pub fn with_target_handle(mut self, target_handle: impl Into<String>) -> Self {
381 self.target_handle = Some(target_handle.into());
382 self
383 }
384
385 pub fn with_data(mut self, data: Value) -> Self {
387 self.data = data;
388 self
389 }
390
391 pub fn id(&self) -> &str {
393 &self.id
394 }
395
396 pub fn source(&self) -> &str {
398 &self.source
399 }
400
401 pub fn target(&self) -> &str {
403 &self.target
404 }
405
406 pub fn source_handle(&self) -> Option<&str> {
408 self.source_handle.as_deref()
409 }
410
411 pub fn target_handle(&self) -> Option<&str> {
413 self.target_handle.as_deref()
414 }
415
416 pub fn data(&self) -> &Value {
418 &self.data
419 }
420
421 pub fn presentation(&self) -> &BTreeMap<String, Value> {
423 &self.presentation
424 }
425}
426
427fn check_size(source: &str) -> Result<(), WorkflowDslError> {
428 let actual_bytes = source.len();
429 if actual_bytes > WORKFLOW_DSL_MAX_BYTES {
430 return Err(WorkflowDslError::DocumentTooLarge {
431 actual_bytes,
432 maximum_bytes: WORKFLOW_DSL_MAX_BYTES,
433 });
434 }
435 Ok(())
436}
437
438fn invalid_document(message: impl Into<String>) -> WorkflowDslError {
439 WorkflowDslError::InvalidDocument {
440 message: message.into(),
441 }
442}