1#![doc = include_str!("../README.md")]
2#![no_std]
3
4extern crate alloc;
5
6use alloc::borrow::ToOwned;
7use alloc::collections::BTreeMap;
8use alloc::string::String;
9use alloc::vec;
10use alloc::vec::Vec;
11
12use schemars::{JsonSchema, schema_for};
13use serde::{Deserialize, Serialize};
14use serde_json::Value;
15
16#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
24#[schemars(title = "catalog.json")]
25pub struct Catalog {
26 pub version: u32,
28 #[serde(default, skip_serializing_if = "Option::is_none")]
30 pub title: Option<String>,
31 pub schemas: Vec<SchemaEntry>,
33 #[serde(default, skip_serializing_if = "Vec::is_empty")]
37 pub groups: Vec<CatalogGroup>,
38}
39
40#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
45#[schemars(title = "Schema Group")]
46pub struct CatalogGroup {
47 pub name: String,
49 pub description: String,
51 pub schemas: Vec<String>,
53}
54
55#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
59#[schemars(title = "Schema Entry")]
60pub struct SchemaEntry {
61 #[schemars(example = example_schema_name())]
63 pub name: String,
64 pub description: String,
66 #[schemars(example = example_schema_url())]
68 pub url: String,
69 #[serde(default, rename = "sourceUrl", skip_serializing_if = "Option::is_none")]
72 pub source_url: Option<String>,
73 #[serde(default, rename = "fileMatch", skip_serializing_if = "Vec::is_empty")]
78 #[schemars(title = "File Match")]
79 #[schemars(example = example_file_match())]
80 pub file_match: Vec<String>,
81 #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
84 pub versions: BTreeMap<String, String>,
85}
86
87fn example_schema_name() -> String {
88 "My Config".to_owned()
89}
90
91fn example_schema_url() -> String {
92 "https://example.com/schemas/my-config.json".to_owned()
93}
94
95fn example_file_match() -> Vec<String> {
96 vec!["*.config.json".to_owned(), "**/.config.json".to_owned()]
97}
98
99pub fn schema() -> Value {
105 serde_json::to_value(schema_for!(Catalog)).expect("schema serialization cannot fail")
106}
107
108pub fn parse_catalog(json: &str) -> Result<Catalog, serde_json::Error> {
114 serde_json::from_str(json)
115}
116
117pub fn parse_catalog_value(value: serde_json::Value) -> Result<Catalog, serde_json::Error> {
123 serde_json::from_value(value)
124}
125
126#[cfg(test)]
127mod tests {
128 use alloc::string::ToString;
129
130 use super::*;
131
132 #[test]
133 fn round_trip_catalog() {
134 let catalog = Catalog {
135 version: 1,
136 title: None,
137 schemas: vec![SchemaEntry {
138 name: "Test Schema".into(),
139 description: "A test schema".into(),
140 url: "https://example.com/test.json".into(),
141 source_url: None,
142 file_match: vec!["*.test.json".into()],
143 versions: BTreeMap::new(),
144 }],
145 groups: vec![],
146 };
147 let json = serde_json::to_string_pretty(&catalog).expect("serialize");
148 let parsed: Catalog = serde_json::from_str(&json).expect("deserialize");
149 assert_eq!(parsed.version, 1);
150 assert_eq!(parsed.schemas.len(), 1);
151 assert_eq!(parsed.schemas[0].name, "Test Schema");
152 assert_eq!(parsed.schemas[0].file_match, vec!["*.test.json"]);
153 }
154
155 #[test]
156 fn parse_catalog_from_json_string() {
157 let json = r#"{"version":1,"schemas":[{"name":"test","description":"desc","url":"https://example.com/s.json","fileMatch":["*.json"]}]}"#;
158 let catalog = parse_catalog(json).expect("parse");
159 assert_eq!(catalog.schemas.len(), 1);
160 assert_eq!(catalog.schemas[0].name, "test");
161 assert_eq!(catalog.schemas[0].file_match, vec!["*.json"]);
162 }
163
164 #[test]
165 fn empty_file_match_omitted_in_serialization() {
166 let entry = SchemaEntry {
167 name: "No Match".into(),
168 description: "desc".into(),
169 url: "https://example.com/no.json".into(),
170 source_url: None,
171 file_match: vec![],
172 versions: BTreeMap::new(),
173 };
174 let json = serde_json::to_string(&entry).expect("serialize");
175 assert!(!json.contains("fileMatch"));
176 assert!(!json.contains("sourceUrl"));
177 assert!(!json.contains("versions"));
178 }
179
180 #[test]
181 fn source_url_serialized_as_camel_case() {
182 let entry = SchemaEntry {
183 name: "Test".into(),
184 description: "desc".into(),
185 url: "https://catalog.example.com/test.json".into(),
186 source_url: Some("https://upstream.example.com/test.json".into()),
187 file_match: vec![],
188 versions: BTreeMap::new(),
189 };
190 let json = serde_json::to_string(&entry).expect("serialize");
191 assert!(json.contains("\"sourceUrl\""));
192 assert!(json.contains("https://upstream.example.com/test.json"));
193
194 let parsed: SchemaEntry = serde_json::from_str(&json).expect("deserialize");
196 assert_eq!(
197 parsed.source_url.as_deref(),
198 Some("https://upstream.example.com/test.json")
199 );
200 }
201
202 #[test]
203 fn deserialize_with_versions() {
204 let json = r#"{
205 "version": 1,
206 "schemas": [{
207 "name": "test",
208 "description": "desc",
209 "url": "https://example.com/s.json",
210 "versions": {"draft-07": "https://example.com/draft07.json"}
211 }]
212 }"#;
213 let catalog = parse_catalog(json).expect("parse");
214 assert_eq!(
215 catalog.schemas[0].versions.get("draft-07"),
216 Some(&"https://example.com/draft07.json".to_string())
217 );
218 }
219
220 #[test]
221 fn schema_has_camel_case_properties() {
222 let schema = schema();
223 let schema_str = serde_json::to_string(&schema).expect("serialize");
224 assert!(
225 schema_str.contains("fileMatch"),
226 "schema should contain fileMatch"
227 );
228 assert!(
229 schema_str.contains("sourceUrl"),
230 "schema should contain sourceUrl"
231 );
232 }
233}