Skip to main content

schema_catalog/
lib.rs

1#![doc = include_str!("../README.md")]
2
3extern crate alloc;
4
5use alloc::collections::BTreeMap;
6
7use serde::{Deserialize, Serialize};
8
9/// A JSON Schema catalog following the `SchemaStore` catalog format.
10/// See: <https://json.schemastore.org/schema-catalog.json>
11#[derive(Debug, Clone, Serialize, Deserialize)]
12pub struct Catalog {
13    pub version: u32,
14    #[serde(default, skip_serializing_if = "Option::is_none")]
15    pub title: Option<String>,
16    pub schemas: Vec<SchemaEntry>,
17    #[serde(default, skip_serializing_if = "Vec::is_empty")]
18    pub groups: Vec<CatalogGroup>,
19}
20
21/// A group of related schemas in the catalog.
22///
23/// Groups provide richer metadata for catalog consumers that support them.
24/// Consumers that don't understand `groups` simply ignore the field.
25#[derive(Debug, Clone, Serialize, Deserialize)]
26pub struct CatalogGroup {
27    pub name: String,
28    pub description: String,
29    /// Schema names that belong to this group.
30    pub schemas: Vec<String>,
31}
32
33/// A single schema entry in the catalog.
34#[derive(Debug, Clone, Serialize, Deserialize)]
35pub struct SchemaEntry {
36    pub name: String,
37    pub description: String,
38    pub url: String,
39    #[serde(default, rename = "fileMatch", skip_serializing_if = "Vec::is_empty")]
40    pub file_match: Vec<String>,
41    #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
42    pub versions: BTreeMap<String, String>,
43}
44
45/// Parse a catalog from a JSON string.
46///
47/// # Errors
48///
49/// Returns an error if the string is not valid JSON or does not match the catalog schema.
50pub fn parse_catalog(json: &str) -> Result<Catalog, serde_json::Error> {
51    serde_json::from_str(json)
52}
53
54/// Parse a catalog from a `serde_json::Value`.
55///
56/// # Errors
57///
58/// Returns an error if the value does not match the expected catalog schema.
59pub fn parse_catalog_value(value: serde_json::Value) -> Result<Catalog, serde_json::Error> {
60    serde_json::from_value(value)
61}
62
63#[cfg(test)]
64mod tests {
65    use super::*;
66
67    #[test]
68    fn round_trip_catalog() {
69        let catalog = Catalog {
70            version: 1,
71            title: None,
72            schemas: vec![SchemaEntry {
73                name: "Test Schema".into(),
74                description: "A test schema".into(),
75                url: "https://example.com/test.json".into(),
76                file_match: vec!["*.test.json".into()],
77                versions: BTreeMap::new(),
78            }],
79            groups: vec![],
80        };
81        let json = serde_json::to_string_pretty(&catalog).expect("serialize");
82        let parsed: Catalog = serde_json::from_str(&json).expect("deserialize");
83        assert_eq!(parsed.version, 1);
84        assert_eq!(parsed.schemas.len(), 1);
85        assert_eq!(parsed.schemas[0].name, "Test Schema");
86        assert_eq!(parsed.schemas[0].file_match, vec!["*.test.json"]);
87    }
88
89    #[test]
90    fn parse_catalog_from_json_string() {
91        let json = r#"{"version":1,"schemas":[{"name":"test","description":"desc","url":"https://example.com/s.json","fileMatch":["*.json"]}]}"#;
92        let catalog = parse_catalog(json).expect("parse");
93        assert_eq!(catalog.schemas.len(), 1);
94        assert_eq!(catalog.schemas[0].name, "test");
95        assert_eq!(catalog.schemas[0].file_match, vec!["*.json"]);
96    }
97
98    #[test]
99    fn empty_file_match_omitted_in_serialization() {
100        let entry = SchemaEntry {
101            name: "No Match".into(),
102            description: "desc".into(),
103            url: "https://example.com/no.json".into(),
104            file_match: vec![],
105            versions: BTreeMap::new(),
106        };
107        let json = serde_json::to_string(&entry).expect("serialize");
108        assert!(!json.contains("fileMatch"));
109        assert!(!json.contains("versions"));
110    }
111
112    #[test]
113    fn deserialize_with_versions() {
114        let json = r#"{
115            "version": 1,
116            "schemas": [{
117                "name": "test",
118                "description": "desc",
119                "url": "https://example.com/s.json",
120                "versions": {"draft-07": "https://example.com/draft07.json"}
121            }]
122        }"#;
123        let catalog = parse_catalog(json).expect("parse");
124        assert_eq!(
125            catalog.schemas[0].versions.get("draft-07"),
126            Some(&"https://example.com/draft07.json".to_string())
127        );
128    }
129}