1#![doc = include_str!("../README.md")]
2
3extern crate alloc;
4
5use alloc::collections::BTreeMap;
6
7use schemars::{JsonSchema, schema_for};
8use serde::{Deserialize, Serialize};
9use serde_json::Value;
10
11#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
19#[schemars(title = "catalog.json")]
20pub struct Catalog {
21 pub version: u32,
23 #[serde(default, skip_serializing_if = "Option::is_none")]
25 pub title: Option<String>,
26 pub schemas: Vec<SchemaEntry>,
28 #[serde(default, skip_serializing_if = "Vec::is_empty")]
32 pub groups: Vec<CatalogGroup>,
33}
34
35#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
40#[schemars(title = "Schema Group")]
41pub struct CatalogGroup {
42 pub name: String,
44 pub description: String,
46 pub schemas: Vec<String>,
48}
49
50#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
54#[schemars(title = "Schema Entry")]
55pub struct SchemaEntry {
56 #[schemars(example = example_schema_name())]
58 pub name: String,
59 pub description: String,
61 #[schemars(example = example_schema_url())]
63 pub url: String,
64 #[serde(default, rename = "sourceUrl", skip_serializing_if = "Option::is_none")]
67 pub source_url: Option<String>,
68 #[serde(default, rename = "fileMatch", skip_serializing_if = "Vec::is_empty")]
73 #[schemars(title = "File Match")]
74 #[schemars(example = example_file_match())]
75 pub file_match: Vec<String>,
76 #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
79 pub versions: BTreeMap<String, String>,
80}
81
82fn example_schema_name() -> String {
83 "My Config".to_owned()
84}
85
86fn example_schema_url() -> String {
87 "https://example.com/schemas/my-config.json".to_owned()
88}
89
90fn example_file_match() -> Vec<String> {
91 vec!["*.config.json".to_owned(), "**/.config.json".to_owned()]
92}
93
94pub fn schema() -> Value {
100 serde_json::to_value(schema_for!(Catalog)).expect("schema serialization cannot fail")
101}
102
103pub fn parse_catalog(json: &str) -> Result<Catalog, serde_json::Error> {
109 serde_json::from_str(json)
110}
111
112pub fn parse_catalog_value(value: serde_json::Value) -> Result<Catalog, serde_json::Error> {
118 serde_json::from_value(value)
119}
120
121#[cfg(test)]
122mod tests {
123 use super::*;
124
125 #[test]
126 fn round_trip_catalog() {
127 let catalog = Catalog {
128 version: 1,
129 title: None,
130 schemas: vec![SchemaEntry {
131 name: "Test Schema".into(),
132 description: "A test schema".into(),
133 url: "https://example.com/test.json".into(),
134 source_url: None,
135 file_match: vec!["*.test.json".into()],
136 versions: BTreeMap::new(),
137 }],
138 groups: vec![],
139 };
140 let json = serde_json::to_string_pretty(&catalog).expect("serialize");
141 let parsed: Catalog = serde_json::from_str(&json).expect("deserialize");
142 assert_eq!(parsed.version, 1);
143 assert_eq!(parsed.schemas.len(), 1);
144 assert_eq!(parsed.schemas[0].name, "Test Schema");
145 assert_eq!(parsed.schemas[0].file_match, vec!["*.test.json"]);
146 }
147
148 #[test]
149 fn parse_catalog_from_json_string() {
150 let json = r#"{"version":1,"schemas":[{"name":"test","description":"desc","url":"https://example.com/s.json","fileMatch":["*.json"]}]}"#;
151 let catalog = parse_catalog(json).expect("parse");
152 assert_eq!(catalog.schemas.len(), 1);
153 assert_eq!(catalog.schemas[0].name, "test");
154 assert_eq!(catalog.schemas[0].file_match, vec!["*.json"]);
155 }
156
157 #[test]
158 fn empty_file_match_omitted_in_serialization() {
159 let entry = SchemaEntry {
160 name: "No Match".into(),
161 description: "desc".into(),
162 url: "https://example.com/no.json".into(),
163 source_url: None,
164 file_match: vec![],
165 versions: BTreeMap::new(),
166 };
167 let json = serde_json::to_string(&entry).expect("serialize");
168 assert!(!json.contains("fileMatch"));
169 assert!(!json.contains("sourceUrl"));
170 assert!(!json.contains("versions"));
171 }
172
173 #[test]
174 fn source_url_serialized_as_camel_case() {
175 let entry = SchemaEntry {
176 name: "Test".into(),
177 description: "desc".into(),
178 url: "https://catalog.example.com/test.json".into(),
179 source_url: Some("https://upstream.example.com/test.json".into()),
180 file_match: vec![],
181 versions: BTreeMap::new(),
182 };
183 let json = serde_json::to_string(&entry).expect("serialize");
184 assert!(json.contains("\"sourceUrl\""));
185 assert!(json.contains("https://upstream.example.com/test.json"));
186
187 let parsed: SchemaEntry = serde_json::from_str(&json).expect("deserialize");
189 assert_eq!(
190 parsed.source_url.as_deref(),
191 Some("https://upstream.example.com/test.json")
192 );
193 }
194
195 #[test]
196 fn deserialize_with_versions() {
197 let json = r#"{
198 "version": 1,
199 "schemas": [{
200 "name": "test",
201 "description": "desc",
202 "url": "https://example.com/s.json",
203 "versions": {"draft-07": "https://example.com/draft07.json"}
204 }]
205 }"#;
206 let catalog = parse_catalog(json).expect("parse");
207 assert_eq!(
208 catalog.schemas[0].versions.get("draft-07"),
209 Some(&"https://example.com/draft07.json".to_string())
210 );
211 }
212
213 #[test]
214 fn schema_has_camel_case_properties() {
215 let schema = schema();
216 let schema_str = serde_json::to_string(&schema).expect("serialize");
217 assert!(
218 schema_str.contains("fileMatch"),
219 "schema should contain fileMatch"
220 );
221 assert!(
222 schema_str.contains("sourceUrl"),
223 "schema should contain sourceUrl"
224 );
225 }
226}