isoloom_spec/syntax/
inputs.rs1use 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#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
17#[serde(deny_unknown_fields)]
18pub struct Input {
19 #[serde(default = "yes")]
22 pub sensitive: bool,
23 #[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#[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 #[must_use]
49 pub fn get(&self, name: &InputName) -> Option<&Input> {
50 self.0.get(name)
51 }
52}
53
54#[derive(JsonSchema)]
57#[serde(untagged)]
58enum InputsInput {
59 Names(Vec<InputName>),
61 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 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
89fn 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}