1use serde::{Deserialize, Serialize};
50use std::collections::BTreeMap;
51
52#[derive(Debug, Clone, Serialize, Deserialize)]
56pub struct SdlDocument {
57 pub schema: SdlSchemaBody,
58}
59
60#[derive(Debug, Clone, Serialize, Deserialize)]
62pub struct SdlSchemaBody {
63 pub version: String,
64
65 #[serde(default)]
67 pub required: Vec<String>,
68
69 pub properties: BTreeMap<String, PropertyDef>,
72}
73
74#[derive(Debug, Clone, Serialize, Deserialize)]
78pub struct PropertyDef {
79 #[serde(rename = "type")]
81 pub type_: PropertyType,
82
83 pub merge: MergeStrategyDef,
85}
86
87#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
91#[serde(rename_all = "lowercase")]
92pub enum PropertyType {
93 String,
94 Number,
95 Boolean,
96 Object,
97 Array,
98}
99
100#[derive(Debug, Clone, Serialize, Deserialize)]
104pub struct MergeStrategyDef {
105 #[serde(rename = "type")]
107 pub strategy_type: MergeStrategyType,
108
109 #[serde(default, skip_serializing_if = "Option::is_none")]
111 pub identity: Option<IdentityRule>,
112
113 #[serde(default, skip_serializing_if = "Option::is_none")]
115 pub source_key: Option<String>,
116
117 #[serde(default, skip_serializing_if = "Option::is_none")]
119 pub target_key: Option<String>,
120}
121
122#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
124#[serde(rename_all = "snake_case")]
125pub enum MergeStrategyType {
126 Replace,
128 DeepMerge,
130 Atomic,
132 OrderedUnique,
134 SetUnion,
136 EdgeList,
138}
139
140#[derive(Debug, Clone, Serialize, Deserialize)]
144#[serde(tag = "mode", rename_all = "snake_case")]
145pub enum IdentityRule {
146 PrimitiveValue,
148 Key { key: String },
150}
151
152impl SdlDocument {
155 pub fn from_yaml(yaml: &str) -> Result<Self, SdlError> {
157 serde_yaml::from_str(yaml).map_err(|e| SdlError::ParseError {
158 message: e.to_string(),
159 })
160 }
161
162 pub fn to_yaml(&self) -> Result<String, SdlError> {
164 serde_yaml::to_string(self).map_err(|e| SdlError::SerializeError {
165 message: e.to_string(),
166 })
167 }
168
169 pub fn property_def(&self, path: &str) -> Option<&PropertyDef> {
175 self.schema.properties.get(path)
176 }
177
178 pub fn property_paths(&self) -> impl Iterator<Item = &String> {
180 self.schema.properties.keys()
181 }
182}
183
184#[derive(Debug, thiserror::Error)]
188pub enum SdlError {
189 #[error("SDL parse error: {message}")]
190 ParseError { message: String },
191
192 #[error("SDL serialize error: {message}")]
193 SerializeError { message: String },
194}
195
196#[cfg(test)]
199mod tests {
200 use super::*;
201
202 fn valid_sdl_yaml() -> &'static str {
203 r#"
204schema:
205 version: "1.0"
206 required:
207 - id
208 properties:
209 name:
210 type: string
211 merge:
212 type: replace
213 tags:
214 type: array
215 merge:
216 type: ordered_unique
217 identity:
218 mode: primitive_value
219 characters:
220 type: array
221 merge:
222 type: ordered_unique
223 identity:
224 mode: key
225 key: id
226 appears_in:
227 type: array
228 merge:
229 type: edge_list
230 source_key: character_id
231 target_key: scene_id
232 identity:
233 mode: key
234 key: id
235 version:
236 type: number
237 merge:
238 type: atomic
239 metadata:
240 type: object
241 merge:
242 type: deep_merge
243 tags_set:
244 type: array
245 merge:
246 type: set_union
247 identity:
248 mode: primitive_value
249"#
250 }
251
252 #[test]
253 fn test_parse_valid_sdl() {
254 let doc = SdlDocument::from_yaml(valid_sdl_yaml()).unwrap();
255 assert_eq!(doc.schema.version, "1.0");
256 assert_eq!(doc.schema.required, vec!["id"]);
257 assert_eq!(doc.schema.properties.len(), 7);
258 }
259
260 #[test]
261 fn test_parse_replace_strategy() {
262 let doc = SdlDocument::from_yaml(valid_sdl_yaml()).unwrap();
263 let def = doc.property_def("name").unwrap();
264 assert!(matches!(def.type_, PropertyType::String));
265 assert!(matches!(
266 def.merge.strategy_type,
267 MergeStrategyType::Replace
268 ));
269 }
270
271 #[test]
272 fn test_parse_ordered_unique_primitive() {
273 let doc = SdlDocument::from_yaml(valid_sdl_yaml()).unwrap();
274 let def = doc.property_def("tags").unwrap();
275 assert!(matches!(def.type_, PropertyType::Array));
276 assert!(matches!(
277 def.merge.strategy_type,
278 MergeStrategyType::OrderedUnique
279 ));
280 let identity = def.merge.identity.as_ref().unwrap();
281 assert!(matches!(identity, IdentityRule::PrimitiveValue));
282 }
283
284 #[test]
285 fn test_parse_ordered_unique_key() {
286 let doc = SdlDocument::from_yaml(valid_sdl_yaml()).unwrap();
287 let def = doc.property_def("characters").unwrap();
288 assert!(matches!(def.type_, PropertyType::Array));
289 let identity = def.merge.identity.as_ref().unwrap();
290 match identity {
291 IdentityRule::Key { key } => assert_eq!(key, "id"),
292 _ => panic!("expected Key identity"),
293 }
294 }
295
296 #[test]
297 fn test_parse_edge_list() {
298 let doc = SdlDocument::from_yaml(valid_sdl_yaml()).unwrap();
299 let def = doc.property_def("appears_in").unwrap();
300 assert!(matches!(
301 def.merge.strategy_type,
302 MergeStrategyType::EdgeList
303 ));
304 assert_eq!(def.merge.source_key.as_deref(), Some("character_id"));
305 assert_eq!(def.merge.target_key.as_deref(), Some("scene_id"));
306 }
307
308 #[test]
309 fn test_parse_atomic() {
310 let doc = SdlDocument::from_yaml(valid_sdl_yaml()).unwrap();
311 let def = doc.property_def("version").unwrap();
312 assert!(matches!(def.type_, PropertyType::Number));
313 assert!(matches!(def.merge.strategy_type, MergeStrategyType::Atomic));
314 }
315
316 #[test]
317 fn test_parse_deep_merge() {
318 let doc = SdlDocument::from_yaml(valid_sdl_yaml()).unwrap();
319 let def = doc.property_def("metadata").unwrap();
320 assert!(matches!(def.type_, PropertyType::Object));
321 assert!(matches!(
322 def.merge.strategy_type,
323 MergeStrategyType::DeepMerge
324 ));
325 }
326
327 #[test]
328 fn test_parse_set_union() {
329 let doc = SdlDocument::from_yaml(valid_sdl_yaml()).unwrap();
330 let def = doc.property_def("tags_set").unwrap();
331 assert!(matches!(
332 def.merge.strategy_type,
333 MergeStrategyType::SetUnion
334 ));
335 }
336
337 #[test]
338 fn test_undefined_property_returns_none() {
339 let doc = SdlDocument::from_yaml(valid_sdl_yaml()).unwrap();
340 assert!(doc.property_def("nonexistent").is_none());
341 }
342
343 #[test]
344 fn test_invalid_yaml_returns_error() {
345 let result = SdlDocument::from_yaml("not: valid: yaml: [[[");
346 assert!(result.is_err());
347 }
348
349 #[test]
350 fn test_sdl_yaml_roundtrip() {
351 let doc = SdlDocument::from_yaml(valid_sdl_yaml()).unwrap();
352 let yaml = doc.to_yaml().unwrap();
353 let parsed = SdlDocument::from_yaml(&yaml).unwrap();
354 assert_eq!(parsed.schema.properties.len(), doc.schema.properties.len());
355 }
356
357 #[test]
358 fn test_missing_merge_strategy_is_parse_error() {
359 let yaml = r#"
363schema:
364 version: "1.0"
365 required: []
366 properties:
367 bad_field:
368 type: string
369"#;
370 let result = SdlDocument::from_yaml(yaml);
371 assert!(
372 result.is_err(),
373 "expected parse error for missing merge strategy"
374 );
375 }
376}