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