isoloom_spec/syntax/
shared.rs1use std::borrow::Cow;
8use std::fmt;
9
10use indexmap::IndexMap;
11use schemars::{JsonSchema, Schema, SchemaGenerator};
12use serde::de::{self, MapAccess, Visitor};
13use serde::{Deserialize, Deserializer};
14
15use super::machine::MachineInput;
16
17pub(crate) const OWN_FIELDS: &[&str] = &["networks", "ports", "access", "count", "aliases", "seal"];
19
20pub(crate) const SHARED_FIELDS: &[&str] = &[
22 "os",
23 "service",
24 "with",
25 "app",
26 "command",
27 "setup",
28 "healthcheck",
29 "depends_on",
30 "environment",
31 "files",
32 "volumes",
33 "disks",
34 "tmpfs",
35 "resources",
36 "hardware",
37 "dns",
38 "privileged",
39 "read_only",
40 "shm_size",
41 "sysctls",
42 "ulimits",
43 "rate_limit",
44];
45
46#[derive(Debug, Clone, PartialEq, Default)]
48pub struct Shared(pub IndexMap<String, serde_json::Value>);
49
50#[derive(Debug, Clone, PartialEq, Default)]
52pub struct Group {
53 pub members: Vec<String>,
55 pub shared: Shared,
57}
58
59struct Fields {
61 group: bool,
62}
63
64impl<'de> Visitor<'de> for Fields {
65 type Value = Group;
66
67 fn expecting(&self, f: &mut fmt::Formatter) -> fmt::Result {
68 f.write_str("a map of machine fields")
69 }
70
71 fn visit_map<A: MapAccess<'de>>(self, mut map: A) -> Result<Group, A::Error> {
72 let mut out = Group::default();
73 while let Some(key) = map.next_key::<String>()? {
74 if self.group && key == "members" {
75 out.members = map.next_value()?;
76 } else if OWN_FIELDS.contains(&key.as_str()) {
77 return Err(de::Error::custom(format!("`{key}` is each machine's own: write it on the machines")));
78 } else if SHARED_FIELDS.contains(&key.as_str()) {
79 out.shared.0.insert(key, map.next_value()?);
80 } else {
81 return Err(de::Error::unknown_field(&key, SHARED_FIELDS));
82 }
83 }
84 if self.group && out.members.is_empty() {
85 return Err(de::Error::custom("a group names its `members`"));
86 }
87 Ok(out)
88 }
89
90 fn visit_unit<E: de::Error>(self) -> Result<Group, E> {
91 Ok(Group::default())
92 }
93}
94
95impl<'de> Deserialize<'de> for Shared {
96 fn deserialize<D: Deserializer<'de>>(d: D) -> Result<Shared, D::Error> {
97 d.deserialize_any(Fields { group: false }).map(|g| g.shared)
98 }
99}
100
101impl<'de> Deserialize<'de> for Group {
102 fn deserialize<D: Deserializer<'de>>(d: D) -> Result<Group, D::Error> {
103 d.deserialize_map(Fields { group: true })
104 }
105}
106
107fn shared_schema(g: &mut SchemaGenerator) -> Schema {
109 let mut s = MachineInput::json_schema(g);
110 if let Some(props) = s.get_mut("properties").and_then(serde_json::Value::as_object_mut) {
111 props.retain(|k, _| !OWN_FIELDS.contains(&k.as_str()));
112 }
113 s.remove("required");
114 s
115}
116
117impl JsonSchema for Shared {
118 fn schema_name() -> Cow<'static, str> {
119 "Shared".into()
120 }
121
122 fn json_schema(g: &mut SchemaGenerator) -> Schema {
123 let mut s = shared_schema(g);
124 s.insert("description".into(), "Machine fields every machine shares (a machine's own value wins).".into());
125 s
126 }
127}
128
129impl JsonSchema for Group {
130 fn schema_name() -> Cow<'static, str> {
131 "Group".into()
132 }
133
134 fn json_schema(g: &mut SchemaGenerator) -> Schema {
135 let mut s = shared_schema(g);
136 let members = g.subschema_for::<Vec<String>>();
137 if let Some(props) = s.get_mut("properties").and_then(serde_json::Value::as_object_mut) {
138 let mut m = members.to_value();
139 if let Some(o) = m.as_object_mut() {
140 o.insert("description".into(), "Machine names, globs (`ws*`), or other groups.".into());
141 }
142 props.insert("members".into(), m);
143 }
144 s.insert("required".into(), serde_json::json!(["members"]));
145 s
146 }
147}
148
149#[cfg(test)]
150mod tests {
151 use super::*;
152
153 #[test]
154 fn shared_fields_left_empty_share_nothing() {
155 assert_eq!(serde_yaml_ng::from_str::<Shared>("~").unwrap(), Shared::default());
156 let wrong = serde_yaml_ng::from_str::<Group>("[a]").unwrap_err().to_string();
157 assert!(wrong.contains("a map of machine fields"), "{wrong}");
158 }
159
160 #[test]
161 fn the_shared_fields_are_a_machines_but_its_own() {
162 let schema = MachineInput::json_schema(&mut SchemaGenerator::default());
163 let mut fields: Vec<&str> = schema.get("properties").unwrap().as_object().unwrap().keys().map(String::as_str).collect();
164 fields.retain(|f| !OWN_FIELDS.contains(f));
165 let mut shared = SHARED_FIELDS.to_vec();
166 fields.sort_unstable();
167 shared.sort_unstable();
168 assert_eq!(fields, shared);
169 }
170
171 #[test]
172 fn groups_share_fields_but_not_a_machines_own() {
173 let g: Group = serde_yaml_ng::from_str("{ members: [dc01, ws*], resources: { cpus: 2 }, os: windows-server@2019 }").unwrap();
174 assert_eq!((g.members.len(), g.shared.0.len()), (2, 2));
175 let e = serde_yaml_ng::from_str::<Group>("{ members: [a], networks: { lab: 10 } }")
176 .unwrap_err()
177 .to_string();
178 assert!(e.contains("each machine's own"), "{e}");
179 assert!(
180 serde_yaml_ng::from_str::<Shared>("{ docker: { image: x } }")
181 .unwrap_err()
182 .to_string()
183 .contains("unknown field `docker`")
184 );
185 assert!(serde_yaml_ng::from_str::<Group>("{ resources: { cpus: 2 } }").is_err());
186 }
187}