schema_ado_yaml/
lib.rs

1// Copyright (c) Microsoft Corporation.
2// Licensed under the MIT License.
3
4//! Serde defs for ADO YAML
5
6#![expect(missing_docs)]
7#![forbid(unsafe_code)]
8
9use serde::Deserialize;
10use serde::Serialize;
11use serde::Serializer;
12use std::collections::BTreeMap;
13
14mod none {
15    use serde::Deserialize;
16    use serde::Deserializer;
17    use serde::Serializer;
18
19    pub fn serialize<S>(_: &(), ser: S) -> Result<S::Ok, S::Error>
20    where
21        S: Serializer,
22    {
23        ser.serialize_str("none")
24    }
25
26    pub fn deserialize<'de, D: Deserializer<'de>>(d: D) -> Result<(), D::Error> {
27        let s: &str = Deserialize::deserialize(d)?;
28        if s != "none" {
29            return Err(serde::de::Error::custom("field must be 'none'"));
30        }
31        Ok(())
32    }
33}
34
35/// Valid names may only contain alphanumeric characters and '_' and may not
36/// start with a number.
37fn validate_name<S>(s: &str, ser: S) -> Result<S::Ok, S::Error>
38where
39    S: Serializer,
40{
41    if s.is_empty() {
42        return Err(serde::ser::Error::custom("name cannot be empty"));
43    }
44
45    if s.chars().next().unwrap().is_ascii_digit() {
46        return Err(serde::ser::Error::custom("name cannot start with a number"));
47    }
48
49    if !s.chars().all(|c| c.is_ascii_alphanumeric() || c == '_') {
50        return Err(serde::ser::Error::custom(
51            "name must be ascii alphanumeric + '_'",
52        ));
53    }
54
55    ser.serialize_str(s)
56}
57
58#[derive(Debug, Serialize, Deserialize)]
59#[serde(rename_all = "camelCase")]
60pub struct TriggerBranches {
61    #[serde(skip_serializing_if = "Vec::is_empty")]
62    pub include: Vec<String>,
63    // Wrapping this in an Option is necessary to prevent problems when deserializing and exclude isn't present
64    #[serde(skip_serializing_if = "Option::is_none")]
65    pub exclude: Option<Vec<String>>,
66}
67
68#[derive(Debug, Serialize, Deserialize)]
69#[serde(untagged)]
70#[serde(rename_all = "camelCase")]
71pub enum PrTrigger {
72    None(#[serde(with = "none")] ()),
73    #[serde(rename_all = "camelCase")]
74    Some {
75        auto_cancel: bool,
76        drafts: bool,
77        branches: TriggerBranches,
78    },
79    // serde has a bug with untagged and `with` during deserialization
80    NoneWorkaround(String),
81}
82
83#[derive(Debug, Serialize, Deserialize)]
84#[serde(untagged)]
85#[serde(rename_all = "camelCase")]
86pub enum CiTrigger {
87    None(#[serde(with = "none")] ()),
88    #[serde(rename_all = "camelCase")]
89    Some {
90        batch: bool,
91        branches: TriggerBranches,
92    },
93    // serde has a bug with untagged and `with` during deserialization
94    NoneWorkaround(String),
95}
96
97#[derive(Debug, Serialize, Deserialize)]
98#[serde(rename_all = "camelCase")]
99pub struct Schedule {
100    // FUTURE?: proper cron validation?
101    pub cron: String,
102    pub display_name: String,
103    pub branches: TriggerBranches,
104    #[serde(skip_serializing_if = "std::ops::Not::not")]
105    #[serde(default)]
106    pub batch: bool,
107}
108
109#[derive(Debug, Serialize, Deserialize)]
110#[serde(rename_all = "camelCase")]
111pub struct Variable {
112    pub name: String,
113    pub value: String,
114}
115
116#[derive(Debug, Serialize, Deserialize)]
117#[serde(rename_all = "camelCase")]
118pub struct Pipeline {
119    #[serde(skip_serializing_if = "Option::is_none")]
120    pub name: Option<String>,
121    #[serde(skip_serializing_if = "Option::is_none")]
122    pub trigger: Option<CiTrigger>,
123    #[serde(skip_serializing_if = "Option::is_none")]
124    pub pr: Option<PrTrigger>,
125    #[serde(skip_serializing_if = "Option::is_none")]
126    pub schedules: Option<Vec<Schedule>>,
127    #[serde(skip_serializing_if = "Option::is_none")]
128    pub variables: Option<Vec<Variable>>,
129    #[serde(skip_serializing_if = "Option::is_none")]
130    pub parameters: Option<Vec<Parameter>>,
131    #[serde(skip_serializing_if = "Option::is_none")]
132    pub resources: Option<Resources>,
133    #[serde(skip_serializing_if = "Option::is_none")]
134    pub stages: Option<Vec<Stage>>,
135    #[serde(skip_serializing_if = "Option::is_none")]
136    pub jobs: Option<Vec<Job>>,
137    #[serde(skip_serializing_if = "Option::is_none")]
138    pub extends: Option<Extends>,
139}
140
141#[derive(Debug, Serialize, Deserialize)]
142#[serde(rename_all = "camelCase")]
143pub struct Extends {
144    pub template: String,
145    pub parameters: BTreeMap<String, serde_yaml::Value>,
146}
147
148#[derive(Debug, Default, Serialize, Deserialize)]
149#[serde(rename_all = "camelCase")]
150pub struct Resources {
151    #[serde(skip_serializing_if = "Vec::is_empty")]
152    pub repositories: Vec<ResourcesRepository>,
153}
154
155#[derive(Debug, Serialize, Deserialize)]
156#[serde(rename_all = "camelCase")]
157pub struct ResourcesRepository {
158    // Alias for the specified repository.
159    //
160    // Acceptable values: [-_A-Za-z0-9]*.
161    pub repository: String,
162    /// ID of the service endpoint connecting to this repository
163    #[serde(skip_serializing_if = "Option::is_none")]
164    pub endpoint: Option<String>,
165    /// Repository name. Format depends on 'type'; does not accept variables
166    pub name: String,
167    /// ref name to checkout; defaults to 'refs/heads/main'.
168    #[serde(rename = "ref")]
169    pub r#ref: String,
170    #[serde(rename = "type")]
171    pub r#type: ResourcesRepositoryType,
172}
173
174#[derive(Debug, Serialize, Deserialize)]
175#[serde(rename_all = "lowercase")]
176pub enum ResourcesRepositoryType {
177    Git,
178    GitHub,
179    GitHubEnterprise,
180    Bitbucket,
181}
182
183#[derive(Debug, Serialize, Deserialize)]
184#[serde(rename_all = "camelCase")]
185pub struct Parameter {
186    pub name: String,
187    pub display_name: String,
188    #[serde(flatten)]
189    pub ty: ParameterType,
190}
191
192// ADO also has specialized types for things like steps/jobs/stages, etc... but
193// at this time, it's unclear how they'd be useful in flowey.
194#[derive(Debug, Serialize, Deserialize)]
195#[serde(tag = "type", rename_all = "camelCase")]
196pub enum ParameterType {
197    Boolean {
198        #[serde(skip_serializing_if = "Option::is_none")]
199        default: Option<bool>,
200    },
201    String {
202        #[serde(skip_serializing_if = "Option::is_none")]
203        default: Option<String>,
204        #[serde(skip_serializing_if = "Option::is_none")]
205        values: Option<Vec<String>>,
206    },
207    Number {
208        #[serde(skip_serializing_if = "Option::is_none")]
209        default: Option<i64>,
210        #[serde(skip_serializing_if = "Option::is_none")]
211        values: Option<Vec<i64>>,
212    },
213    Object {
214        #[serde(skip_serializing_if = "Option::is_none")]
215        default: Option<serde_yaml::Value>,
216    },
217}
218
219#[derive(Debug, Serialize, Deserialize)]
220#[serde(rename_all = "camelCase")]
221pub struct Stage {
222    /// Valid names may only contain alphanumeric characters and '_' and may
223    /// not start with a number.
224    #[serde(serialize_with = "validate_name")]
225    pub stage: String,
226    pub display_name: String,
227    pub depends_on: Vec<String>,
228    #[serde(skip_serializing_if = "Option::is_none")]
229    pub condition: Option<String>,
230    pub jobs: Vec<Job>,
231}
232
233#[derive(Debug, Serialize, Deserialize)]
234#[serde(untagged)]
235#[serde(rename_all = "camelCase")]
236pub enum Pool {
237    Pool(String),
238    PoolWithMetadata(BTreeMap<String, serde_yaml::Value>),
239}
240
241#[derive(Debug, Serialize, Deserialize)]
242#[serde(rename_all = "camelCase")]
243pub struct Job {
244    #[serde(serialize_with = "validate_name")]
245    pub job: String,
246    pub display_name: String,
247    pub pool: Pool,
248    pub depends_on: Vec<String>,
249    #[serde(skip_serializing_if = "Option::is_none")]
250    pub condition: Option<String>,
251    #[serde(skip_serializing_if = "Option::is_none")]
252    pub variables: Option<Vec<Variable>>,
253    // individual steps are not type-checked by the serde schema, as there are a
254    // _lot_ of different step kinds nodes might be emitting.
255    //
256    // instead, trust that the user knows what they're doing when emitting yaml
257    // snippets.
258    pub steps: Vec<serde_yaml::Value>,
259}