anodizer_core/config/publishers/
schemastore.rs1use schemars::JsonSchema;
6use serde::{Deserialize, Serialize};
7
8use super::super::{StringOrBool, deserialize_string_or_bool_opt};
9use super::{CommitAuthorConfig, RepositoryConfig};
10
11#[derive(Debug, Clone, Default, Serialize, Deserialize, JsonSchema)]
14#[serde(default, deny_unknown_fields)]
15pub struct SchemastoreConfig {
16 pub repository: Option<RepositoryConfig>,
18 pub commit_author: Option<CommitAuthorConfig>,
20 pub versioned: Option<bool>,
22 #[serde(
24 deserialize_with = "deserialize_string_or_bool_opt",
25 default,
26 alias = "disable"
27 )]
28 pub skip: Option<StringOrBool>,
29 #[serde(rename = "if")]
31 pub if_condition: Option<String>,
32 pub schemas: Vec<SchemaEntry>,
34 pub retain_on_rollback: Option<bool>,
37}
38
39#[derive(Debug, Clone, Default, Serialize, Deserialize, JsonSchema)]
41#[serde(default, deny_unknown_fields)]
42pub struct SchemaEntry {
43 pub name: String,
45 pub slug: Option<String>,
47 pub file_match: Vec<String>,
49 pub url: Option<String>,
51 pub schema_file: Option<String>,
53 #[serde(rename = "crate")]
55 pub crate_: Option<String>,
56 pub description: Option<String>,
58 pub versioned: Option<bool>,
60 #[serde(default, skip_serializing_if = "Option::is_none")]
62 pub required: Option<bool>,
63 #[serde(
65 deserialize_with = "deserialize_string_or_bool_opt",
66 default,
67 alias = "disable"
68 )]
69 pub skip: Option<StringOrBool>,
70 #[serde(rename = "if")]
72 pub if_condition: Option<String>,
73}
74
75#[derive(Debug, Clone, Copy, PartialEq, Eq)]
77pub enum SchemaMode {
78 External,
79 Vendor,
80}
81
82impl SchemaEntry {
83 pub fn mode(&self) -> anyhow::Result<SchemaMode> {
85 match (self.url.is_some(), self.schema_file.is_some()) {
86 (true, false) => Ok(SchemaMode::External),
87 (false, true) => Ok(SchemaMode::Vendor),
88 (false, false) => anyhow::bail!(
89 "schemastore schema `{}`: set `url` or `schema_file`",
90 self.name
91 ),
92 (true, true) => anyhow::bail!(
93 "schemastore schema `{}`: set `url` or `schema_file`, not both",
94 self.name
95 ),
96 }
97 }
98
99 pub fn validate(&self) -> anyhow::Result<()> {
102 self.mode()?;
103 if self.file_match.is_empty() {
104 anyhow::bail!(
105 "schemastore schema `{}`: `file_match` must list at least one filename",
106 self.name
107 );
108 }
109 Ok(())
110 }
111}
112
113impl SchemastoreConfig {
114 pub fn resolved_repository(&self, _entry: &SchemaEntry) -> Option<&RepositoryConfig> {
116 self.repository.as_ref()
117 }
118
119 pub fn resolved_commit_author(&self, _entry: &SchemaEntry) -> Option<&CommitAuthorConfig> {
121 self.commit_author.as_ref()
122 }
123
124 pub fn resolved_versioned(&self, entry: &SchemaEntry) -> bool {
126 entry.versioned.or(self.versioned).unwrap_or(false)
127 }
128
129 pub fn resolved_skip(&self, entry: &SchemaEntry) -> bool {
131 let block = self
132 .skip
133 .as_ref()
134 .map(StringOrBool::as_bool)
135 .unwrap_or(false);
136 let per = entry
137 .skip
138 .as_ref()
139 .map(StringOrBool::as_bool)
140 .unwrap_or(false);
141 block || per
142 }
143
144 pub fn resolved_if<'a>(&'a self, entry: &'a SchemaEntry) -> Option<&'a str> {
146 entry
147 .if_condition
148 .as_deref()
149 .or(self.if_condition.as_deref())
150 }
151}
152
153#[cfg(test)]
154mod tests {
155 use super::*;
156
157 #[test]
158 fn deserializes_external_and_vendor_entries() {
159 let yaml = r#"
160repository: { owner: tj-smith47, name: schemastore }
161versioned: false
162schemas:
163 - name: Anodizer
164 file_match: [".anodizer.yaml", ".anodizer.yml"]
165 url: "https://tj-smith47.github.io/anodizer/schema.json"
166 description: "Anodizer Rust release-automation configuration file"
167 - name: cfgd-config
168 file_match: ["cfgd.yaml"]
169 schema_file: "schemas/cfgd-config.schema.json"
170 crate: cfgd
171"#;
172 let cfg: SchemastoreConfig = serde_yaml_ng::from_str(yaml).unwrap();
173 assert_eq!(cfg.schemas.len(), 2);
174 assert_eq!(cfg.schemas[0].name, "Anodizer");
175 assert_eq!(
176 cfg.schemas[0].url.as_deref(),
177 Some("https://tj-smith47.github.io/anodizer/schema.json")
178 );
179 assert_eq!(
180 cfg.schemas[1].schema_file.as_deref(),
181 Some("schemas/cfgd-config.schema.json")
182 );
183 assert_eq!(cfg.schemas[1].crate_.as_deref(), Some("cfgd"));
184 }
185
186 #[test]
187 fn rejects_unknown_field() {
188 let yaml = "schemas: []\nbogus: 1\n";
189 assert!(serde_yaml_ng::from_str::<SchemastoreConfig>(yaml).is_err());
190 }
191
192 #[test]
193 fn per_entry_versioned_overrides_block_default() {
194 let cfg = SchemastoreConfig {
195 versioned: Some(false),
196 schemas: vec![
197 SchemaEntry {
198 name: "a".into(),
199 versioned: None,
200 ..Default::default()
201 },
202 SchemaEntry {
203 name: "b".into(),
204 versioned: Some(true),
205 ..Default::default()
206 },
207 ],
208 ..Default::default()
209 };
210 assert!(!cfg.resolved_versioned(&cfg.schemas[0])); assert!(cfg.resolved_versioned(&cfg.schemas[1])); }
213
214 #[test]
215 fn repository_and_author_fall_through_to_block() {
216 let repo = RepositoryConfig {
217 owner: Some("tj-smith47".into()),
218 name: Some("schemastore".into()),
219 ..Default::default()
220 };
221 let cfg = SchemastoreConfig {
222 repository: Some(repo),
223 schemas: vec![SchemaEntry {
224 name: "a".into(),
225 ..Default::default()
226 }],
227 ..Default::default()
228 };
229 assert_eq!(
230 cfg.resolved_repository(&cfg.schemas[0])
231 .unwrap()
232 .owner
233 .as_deref(),
234 Some("tj-smith47")
235 );
236 }
237
238 #[test]
239 fn mode_inferred_from_field_presence() {
240 let ext = SchemaEntry {
241 name: "a".into(),
242 url: Some("https://x/s.json".into()),
243 file_match: vec!["a.yaml".into()],
244 ..Default::default()
245 };
246 let ven = SchemaEntry {
247 name: "b".into(),
248 schema_file: Some("s.json".into()),
249 file_match: vec!["b.yaml".into()],
250 ..Default::default()
251 };
252 assert_eq!(ext.mode().unwrap(), SchemaMode::External);
253 assert_eq!(ven.mode().unwrap(), SchemaMode::Vendor);
254 }
255
256 #[test]
257 fn validate_rejects_neither_both_and_empty_filematch() {
258 let neither = SchemaEntry {
259 name: "a".into(),
260 file_match: vec!["a.yaml".into()],
261 ..Default::default()
262 };
263 assert!(
264 neither
265 .validate()
266 .unwrap_err()
267 .to_string()
268 .contains("url` or `schema_file")
269 );
270 let both = SchemaEntry {
271 name: "a".into(),
272 url: Some("u".into()),
273 schema_file: Some("s".into()),
274 file_match: vec!["a.yaml".into()],
275 ..Default::default()
276 };
277 assert!(
278 both.validate()
279 .unwrap_err()
280 .to_string()
281 .contains("not both")
282 );
283 let no_fm = SchemaEntry {
284 name: "a".into(),
285 url: Some("u".into()),
286 file_match: vec![],
287 ..Default::default()
288 };
289 assert!(
290 no_fm
291 .validate()
292 .unwrap_err()
293 .to_string()
294 .contains("file_match")
295 );
296 }
297
298 #[test]
299 fn disable_alias_is_accepted() {
300 let yaml = "schemas: []\nskip: true\n";
301 let cfg: SchemastoreConfig = serde_yaml_ng::from_str(yaml).unwrap();
302 assert!(cfg.skip.is_some());
303
304 let via_alias = "schemas: []\ndisable: true\n";
305 let cfg2: SchemastoreConfig = serde_yaml_ng::from_str(via_alias).unwrap();
306 assert!(cfg2.skip.is_some());
307 }
308}