1use std::collections::BTreeMap;
2use std::num::NonZeroU32;
3
4use daggy::Dag;
5
6use schemars::JsonSchema as DeriveJsonSchema;
7use serde::{Deserialize, Serialize};
8
9#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, DeriveJsonSchema)]
15pub struct CommandStep {
16 pub key: String,
18 #[serde(default)]
20 pub label: Option<String>,
21 pub cmd: String,
23 #[serde(default)]
27 pub image: Option<String>,
28 #[serde(default)]
30 pub env: Option<BTreeMap<String, String>>,
31 #[serde(default)]
35 pub timeout_seconds: Option<NonZeroU32>,
36 #[serde(default)]
38 pub cache: Option<Cache>,
39 #[serde(default, skip_serializing_if = "Option::is_none")]
42 pub runner: Option<String>,
43 #[serde(default, skip_serializing_if = "Option::is_none")]
45 pub runner_args: Option<serde_json::Value>,
46}
47
48#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, DeriveJsonSchema)]
50pub struct Cache {
51 pub policy: String,
53 #[serde(default)]
55 pub key: Option<String>,
56}
57
58#[derive(Debug, Clone, Serialize, Deserialize)]
63pub struct Transition {
64 pub step: CommandStep,
65 pub env: BTreeMap<String, String>,
66}
67
68#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
70#[serde(rename_all = "snake_case")]
71pub enum EdgeKind {
72 BuildsIn,
75 DependsOn,
79}
80
81#[derive(Debug, Clone, Serialize, Deserialize)]
87pub struct PipelineGraph {
88 #[serde(default = "default_version")]
89 version: String,
90 #[serde(default, skip_serializing_if = "Option::is_none")]
91 default_image: Option<String>,
92 #[serde(default, skip_serializing_if = "Option::is_none")]
98 timeout_seconds: Option<NonZeroU32>,
99 #[serde(rename = "graph")]
100 inner: Dag<Transition, EdgeKind>,
101}
102
103fn default_version() -> String {
104 "0".to_string()
105}
106
107impl PipelineGraph {
108 #[must_use]
110 pub fn node_count(&self) -> usize {
111 self.inner.node_count()
112 }
113
114 #[must_use]
116 pub fn default_image(&self) -> Option<&str> {
117 self.default_image.as_deref()
118 }
119
120 #[must_use]
125 pub const fn timeout_seconds(&self) -> Option<NonZeroU32> {
126 self.timeout_seconds
127 }
128
129 #[must_use]
131 pub const fn dag(&self) -> &Dag<Transition, EdgeKind> {
132 &self.inner
133 }
134}
135
136#[cfg(test)]
137mod timeout_tests {
138 #![allow(clippy::unwrap_used, clippy::expect_used)]
139
140 use std::num::NonZeroU32;
141
142 use super::PipelineGraph;
143
144 #[test]
145 fn deserializes_pipeline_timeout_seconds() {
146 let json = r#"{
147 "version": "0",
148 "timeout_seconds": 1800,
149 "graph": {"nodes": [], "node_holes": [], "edge_property": "directed", "edges": []}
150 }"#;
151 let g: PipelineGraph = serde_json::from_str(json).unwrap();
152 assert_eq!(g.timeout_seconds(), NonZeroU32::new(1800));
153 }
154
155 #[test]
156 fn rejects_zero_pipeline_timeout_seconds() {
157 let json = r#"{
158 "version": "0",
159 "timeout_seconds": 0,
160 "graph": {"nodes": [], "node_holes": [], "edge_property": "directed", "edges": []}
161 }"#;
162 assert!(serde_json::from_str::<PipelineGraph>(json).is_err());
163 }
164
165 #[test]
166 fn pipeline_timeout_defaults_to_none() {
167 let json = r#"{
168 "version": "0",
169 "graph": {"nodes": [], "node_holes": [], "edge_property": "directed", "edges": []}
170 }"#;
171 let g: PipelineGraph = serde_json::from_str(json).unwrap();
172 assert_eq!(g.timeout_seconds(), None);
173 }
174}