1use serde::{Deserialize, Serialize};
7
8pub const ID_FIELD_NAME: &str = "id";
12
13#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
14pub struct Catalog {
15 pub name: String,
16 #[serde(default, skip_serializing_if = "Option::is_none")]
17 pub description: Option<String>,
18 pub fields: Vec<CatalogField>,
19}
20
21#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
22pub struct CatalogField {
23 pub name: String,
24 #[serde(rename = "type")]
25 pub field_type: CatalogFieldType,
26}
27
28#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
29#[serde(rename_all = "snake_case")]
30pub enum CatalogFieldType {
31 String,
32 Number,
33 Boolean,
34 Time,
35 Object,
36 Array,
37 #[serde(other)]
42 Unknown,
43}
44
45impl CatalogFieldType {
46 pub fn as_str(self) -> &'static str {
52 match self {
53 Self::String => "string",
54 Self::Number => "number",
55 Self::Boolean => "boolean",
56 Self::Time => "time",
57 Self::Object => "object",
58 Self::Array => "array",
59 Self::Unknown => "unknown",
60 }
61 }
62}
63
64impl Catalog {
65 pub fn normalized(&self) -> Self {
71 let mut sorted = self.clone();
72 sorted.fields.sort_by(|a, b| {
73 let a_is_id = a.name == ID_FIELD_NAME;
74 let b_is_id = b.name == ID_FIELD_NAME;
75 b_is_id.cmp(&a_is_id).then_with(|| a.name.cmp(&b.name))
77 });
78 sorted
79 }
80}
81
82#[cfg(test)]
83mod tests {
84 use super::*;
85
86 #[test]
87 fn catalog_yaml_roundtrip() {
88 let cat = Catalog {
89 name: "cardiology".into(),
90 description: Some("Cardiology catalog".into()),
91 fields: vec![
92 CatalogField {
93 name: "condition_id".into(),
94 field_type: CatalogFieldType::String,
95 },
96 CatalogField {
97 name: "display_order".into(),
98 field_type: CatalogFieldType::Number,
99 },
100 ],
101 };
102 let yaml = serde_norway::to_string(&cat).unwrap();
103 let parsed: Catalog = serde_norway::from_str(&yaml).unwrap();
104 assert_eq!(cat, parsed);
105 }
106
107 #[test]
108 fn catalog_field_type_serializes_snake_case() {
109 let yaml = serde_norway::to_string(&CatalogFieldType::Boolean).unwrap();
110 assert_eq!(yaml.trim(), "boolean");
111 }
112
113 #[test]
114 fn unknown_field_type_deserializes_without_failure() {
115 let yaml = "name: future\nfields:\n - name: x\n type: hyperlink\n";
119 let cat: Catalog = serde_norway::from_str(yaml).unwrap();
120 assert_eq!(cat.fields[0].field_type, CatalogFieldType::Unknown);
121 assert_eq!(cat.fields[0].field_type.as_str(), "unknown");
122 }
123
124 #[test]
125 fn unknown_field_type_does_not_break_known_fields() {
126 let yaml = "\
129name: mixed
130fields:
131 - name: id
132 type: string
133 - name: fancy
134 type: quantum_entanglement
135 - name: score
136 type: number
137";
138 let cat: Catalog = serde_norway::from_str(yaml).unwrap();
139 assert_eq!(cat.fields.len(), 3);
140 assert_eq!(cat.fields[0].field_type, CatalogFieldType::String);
141 assert_eq!(cat.fields[1].field_type, CatalogFieldType::Unknown);
142 assert_eq!(cat.fields[2].field_type, CatalogFieldType::Number);
143 }
144
145 #[test]
146 fn description_omitted_when_none() {
147 let cat = Catalog {
148 name: "x".into(),
149 description: None,
150 fields: vec![],
151 };
152 let yaml = serde_norway::to_string(&cat).unwrap();
153 assert!(!yaml.contains("description"));
154 }
155
156 #[test]
157 fn normalized_sorts_fields_by_name() {
158 let cat = Catalog {
159 name: "x".into(),
160 description: None,
161 fields: vec![
162 CatalogField {
163 name: "z".into(),
164 field_type: CatalogFieldType::String,
165 },
166 CatalogField {
167 name: "a".into(),
168 field_type: CatalogFieldType::String,
169 },
170 ],
171 };
172 let n = cat.normalized();
173 assert_eq!(n.fields[0].name, "a");
174 assert_eq!(n.fields[1].name, "z");
175 }
176
177 #[test]
178 fn normalized_hoists_id_field_to_front() {
179 let cat = Catalog {
183 name: "x".into(),
184 description: None,
185 fields: vec![
186 CatalogField {
187 name: "URL".into(),
188 field_type: CatalogFieldType::String,
189 },
190 CatalogField {
191 name: "author".into(),
192 field_type: CatalogFieldType::String,
193 },
194 CatalogField {
195 name: "id".into(),
196 field_type: CatalogFieldType::String,
197 },
198 CatalogField {
199 name: "title".into(),
200 field_type: CatalogFieldType::String,
201 },
202 ],
203 };
204 let n = cat.normalized();
205 let names: Vec<_> = n.fields.iter().map(|f| f.name.as_str()).collect();
206 assert_eq!(names, vec!["id", "URL", "author", "title"]);
207 }
208
209 #[test]
210 fn normalized_without_id_field_is_pure_alphabetical() {
211 let cat = Catalog {
212 name: "x".into(),
213 description: None,
214 fields: vec![
215 CatalogField {
216 name: "z".into(),
217 field_type: CatalogFieldType::String,
218 },
219 CatalogField {
220 name: "a".into(),
221 field_type: CatalogFieldType::String,
222 },
223 ],
224 };
225 let n = cat.normalized();
226 assert_eq!(n.fields[0].name, "a");
227 assert_eq!(n.fields[1].name, "z");
228 }
229}