Skip to main content

isoloom_spec/syntax/
inputs.rs

1//! The spec's `inputs`: values the launcher, CI or the command line provides at each start.
2//! `inputs: [FLAG]` is short for `inputs: { FLAG: {} }`. An input is sensitive unless it says
3//! otherwise, and a sensitive one can only reach a machine as a file (or a catalogue setting).
4
5use std::borrow::Cow;
6use std::fmt;
7
8use indexmap::IndexMap;
9use schemars::{JsonSchema, Schema, SchemaGenerator};
10use serde::de::{self, MapAccess, SeqAccess, Visitor};
11use serde::{Deserialize, Deserializer, Serialize};
12
13use super::names::InputName;
14
15/// One input.
16#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
17#[serde(deny_unknown_fields)]
18pub struct Input {
19    /// Whether the value must stay hidden (default `true`): a sensitive input may be a file's
20    /// `input` or a `with:` value, never an `environment` value, which `docker inspect` shows.
21    #[serde(default = "yes")]
22    pub sensitive: bool,
23    /// What the value is, shown when a runner asks for it.
24    #[serde(default, skip_serializing_if = "Option::is_none")]
25    pub description: Option<String>,
26}
27
28impl Default for Input {
29    fn default() -> Input {
30        Input {
31            sensitive: true,
32            description: None,
33        }
34    }
35}
36
37fn yes() -> bool {
38    true
39}
40
41/// The spec's inputs, by name, in the order written.
42#[derive(Debug, Clone, PartialEq, Eq, Default, Serialize, Deserialize)]
43#[serde(transparent)]
44pub struct Inputs(#[serde(deserialize_with = "inputs")] pub IndexMap<InputName, Input>);
45
46impl Inputs {
47    /// The input named `name`, when declared.
48    #[must_use]
49    pub fn get(&self, name: &InputName) -> Option<&Input> {
50        self.0.get(name)
51    }
52}
53
54// What the schema shows and the reader returns, before the names are checked once each.
55/// `inputs:` as written, for the schema.
56#[derive(JsonSchema)]
57#[serde(untagged)]
58enum InputsInput {
59    /// Input names, each sensitive.
60    Names(Vec<InputName>),
61    /// Inputs by name, each with whether it is sensitive and what it is.
62    Full(IndexMap<InputName, Input>),
63}
64
65impl<'de> Deserialize<'de> for InputsInput {
66    fn deserialize<D: Deserializer<'de>>(d: D) -> Result<InputsInput, D::Error> {
67        struct V;
68        impl<'de> Visitor<'de> for V {
69            type Value = InputsInput;
70            fn expecting(&self, f: &mut fmt::Formatter) -> fmt::Result {
71                f.write_str("a list of input names, or a map of name to `{ sensitive, description }`")
72            }
73            fn visit_seq<A: SeqAccess<'de>>(self, seq: A) -> Result<InputsInput, A::Error> {
74                Vec::<InputName>::deserialize(de::value::SeqAccessDeserializer::new(seq)).map(InputsInput::Names)
75            }
76            fn visit_map<A: MapAccess<'de>>(self, map: A) -> Result<InputsInput, A::Error> {
77                // `FLAG:` alone (null) is an input with every default.
78                let raw = IndexMap::<InputName, Option<Input>>::deserialize(de::value::MapAccessDeserializer::new(map))?;
79                Ok(InputsInput::Full(raw.into_iter().map(|(k, v)| (k, v.unwrap_or_default())).collect()))
80            }
81            fn visit_unit<E: de::Error>(self) -> Result<InputsInput, E> {
82                Ok(InputsInput::Names(Vec::new()))
83            }
84        }
85        d.deserialize_any(V)
86    }
87}
88
89/// The inputs `inputs:` declares, each name once.
90fn inputs<'de, D: Deserializer<'de>>(d: D) -> Result<IndexMap<InputName, Input>, D::Error> {
91    match InputsInput::deserialize(d)? {
92        InputsInput::Full(inputs) => Ok(inputs),
93        InputsInput::Names(names) => {
94            let mut out = IndexMap::new();
95            for name in names {
96                if out.contains_key(&name) {
97                    return Err(de::Error::custom(format!("`{name}` is listed twice")));
98                }
99                out.insert(name, Input::default());
100            }
101            Ok(out)
102        }
103    }
104}
105
106impl JsonSchema for Inputs {
107    fn schema_name() -> Cow<'static, str> {
108        "Inputs".into()
109    }
110
111    fn json_schema(g: &mut SchemaGenerator) -> Schema {
112        InputsInput::json_schema(g)
113    }
114}
115
116#[cfg(test)]
117mod tests {
118    use super::*;
119
120    #[test]
121    fn an_input_is_sensitive_unless_it_says_otherwise() {
122        let map: Inputs = serde_yaml_ng::from_str("{ FLAG: { description: the flag } }").unwrap();
123        assert!(map.0.values().all(|i| i.sensitive));
124        let none: Inputs = serde_yaml_ng::from_str("~").unwrap();
125        assert_eq!(none, Inputs::default());
126        let wrong = serde_yaml_ng::from_str::<Inputs>("5").unwrap_err().to_string();
127        assert!(
128            wrong.contains("a list of input names, or a map of name to `{ sensitive, description }`"),
129            "{wrong}"
130        );
131    }
132
133    #[test]
134    fn inputs_are_a_list_or_a_map() {
135        let list: Inputs = serde_yaml_ng::from_str("[FLAG, API_URL]").unwrap();
136        assert!(list.0.values().all(|i| i.sensitive));
137        let map: Inputs = serde_yaml_ng::from_str("{ FLAG: , API_URL: { sensitive: false } }").unwrap();
138        let flags: Vec<bool> = map.0.values().map(|i| i.sensitive).collect();
139        assert_eq!(flags, [true, false]);
140        assert!(serde_yaml_ng::from_str::<Inputs>("[FLAG, FLAG]").unwrap_err().to_string().contains("twice"));
141        assert!(serde_yaml_ng::from_str::<Inputs>("[1FLAG]").is_err());
142    }
143}