1#![deny(missing_docs)]
4use juniper::ID;
5use serde::Deserialize;
6use std::path::PathBuf;
7use thiserror::Error;
8
9mod data_path;
10pub use data_path::DataPath;
11mod values;
12pub use values::Merge;
13
14#[derive(Error, Debug)]
16pub enum DataResolverError {
17 #[error("Cannot merge into non-mapping `{0:?}`")]
19 CannotMergeIntoNonMapping(serde_yaml::Value),
20 #[error("Incompatible merge `{dst:?}` <- `{src:?}`")]
22 IncompatibleYamlMerge {
23 src: serde_yaml::Value,
25 dst: serde_yaml::Value,
27 },
28 #[error(transparent)]
30 IOError(#[from] std::io::Error),
31 #[error("Key `{0}` not found")]
33 KeyNotFound(String),
34 #[error(transparent)]
36 YamlError(#[from] serde_yaml::Error),
37}
38
39pub struct DataResolver {
46 root: PathBuf,
47}
48
49impl DataResolver {
50 pub fn get<T>(&self, address: &[&str]) -> Result<T, DataResolverError>
53 where
54 T: for<'de> Deserialize<'de>,
55 T: ResolveValue,
56 {
57 let data_path = DataPath::new(&self.root, address);
58 let value = T::resolve_value(data_path)?;
59 Ok(serde_yaml::from_value(value)?)
60 }
61}
62
63impl From<PathBuf> for DataResolver {
64 fn from(root: PathBuf) -> Self {
65 Self { root }
66 }
67}
68
69pub trait ResolveValue {
100 fn merge_properties<'a>(
102 value: &'a mut serde_yaml::Value,
103 _data_path: &DataPath,
104 ) -> Result<&'a mut serde_yaml::Value, DataResolverError> {
105 Ok(value)
106 }
107 fn init_with_identifier(_identifier: serde_yaml::Value) -> serde_yaml::Value {
112 serde_yaml::Value::Null
113 }
114 fn resolve_value(data_path: DataPath) -> Result<serde_yaml::Value, DataResolverError> {
117 let mut value = data_path.value().unwrap_or(serde_yaml::Value::Null);
118 if data_path.done() {
119 Self::merge_properties(&mut value, &data_path)?;
120 } else if let Some(data_path) = data_path.descend() {
121 if let Ok(mergee) = Self::resolve_value(data_path) {
122 value.merge(mergee)?;
123 }
124 }
125 Ok(value)
126 }
127 fn resolve_vec_base(_data_path: &DataPath) -> serde_yaml::Value {
135 serde_yaml::Value::Null
136 }
137}
138
139impl ResolveValue for bool {}
140impl ResolveValue for f64 {}
141impl ResolveValue for ID {}
142impl ResolveValue for String {}
143impl ResolveValue for i32 {}
144impl<T: ResolveValue> ResolveValue for Option<T> {
145 fn resolve_value(data_path: DataPath) -> Result<serde_yaml::Value, DataResolverError> {
146 T::resolve_value(data_path).or(Ok(serde_yaml::Value::Null))
147 }
148}
149impl<T: ResolveValue> ResolveValue for Vec<T> {
150 fn merge_properties<'a>(
151 value: &'a mut serde_yaml::Value,
152 data_path: &DataPath,
153 ) -> Result<&'a mut serde_yaml::Value, DataResolverError> {
154 use serde_yaml::Value::{Mapping, Sequence};
155 match value {
156 Mapping(map) => {
157 *value = Sequence(
158 map.into_iter()
159 .filter_map(|(k, v)| {
160 let mut value = T::init_with_identifier(k.clone());
161 value.merge(v.take()).ok().map(|merged| merged.take())
162 })
163 .collect(),
164 );
165 Ok(value)
166 }
167 _ => value.merge(
168 data_path
169 .sub_paths()
170 .into_iter()
171 .filter_map(|dp| {
172 let mut base_value = T::resolve_vec_base(&dp);
173 T::resolve_value(dp)
174 .ok()
175 .map(|v| match base_value.merge(v) {
176 Ok(_) => Some(base_value),
177 _ => None,
178 })
179 })
180 .map(|v| v.unwrap())
181 .collect(),
182 ),
183 }
184 }
185}
186
187#[cfg(test)]
188mod tests {
189 use super::values::Merge;
190 use super::*;
191 use color_eyre::Result;
192 use indoc::indoc;
193 use test_files::TestFiles;
194
195 #[derive(Debug, Deserialize, PartialEq)]
196 struct MyObj {
197 id: i32,
198 name: String,
199 }
200
201 impl ResolveValue for MyObj {
202 fn merge_properties<'a>(
203 value: &'a mut serde_yaml::Value,
204 data_path: &DataPath,
205 ) -> Result<&'a mut serde_yaml::Value, DataResolverError> {
206 if let Ok(id) = i32::resolve_value(data_path.join("id")) {
207 value.merge_at("id", id)?;
208 }
209 if let Ok(name) = String::resolve_value(data_path.join("name")) {
210 value.merge_at("name", name)?;
211 }
212 Ok(value)
213 }
214 }
215
216 #[derive(Debug, Deserialize, Eq, Ord, PartialEq, PartialOrd)]
217 struct MyOtherObj {
218 id: i32,
219 alias: String,
220 }
221
222 impl ResolveValue for MyOtherObj {
223 fn init_with_identifier(identifier: serde_yaml::Value) -> serde_yaml::Value {
224 use serde_yaml::{Mapping, Value};
225 let mut mapping = Mapping::new();
226 mapping.insert(Value::from("alias"), identifier);
227 Value::Mapping(mapping)
228 }
229 fn merge_properties<'a>(
230 value: &'a mut serde_yaml::Value,
231 data_path: &DataPath,
232 ) -> Result<&'a mut serde_yaml::Value, DataResolverError> {
233 if let Ok(id) = i32::resolve_value(data_path.join("id")) {
234 value.merge_at("id", id)?;
235 }
236 if let Ok(alias) = String::resolve_value(data_path.join("alias")) {
237 value.merge_at("alias", alias)?;
238 }
239 Ok(value)
240 }
241 }
242
243 #[derive(Debug, Deserialize, PartialEq)]
244 struct Query {
245 my_obj: MyObj,
246 my_list: Vec<MyOtherObj>,
247 }
248
249 impl ResolveValue for Query {
250 fn merge_properties<'a>(
251 value: &'a mut serde_yaml::Value,
252 data_path: &DataPath,
253 ) -> Result<&'a mut serde_yaml::Value, DataResolverError> {
254 if let Ok(my_obj) = MyObj::resolve_value(data_path.join("my_obj")) {
255 value.merge_at("my_obj", my_obj)?;
256 }
257 if let Ok(my_list) = Vec::<MyOtherObj>::resolve_value(data_path.join("my_list")) {
258 value.merge_at("my_list", my_list)?;
259 }
260 Ok(value)
261 }
262 }
263
264 trait GetResolver<'a> {
265 fn data_path(&self, address: &'a [&'a str]) -> DataPath<'a>;
266 fn resolver(&self) -> DataResolver;
267 }
268
269 impl<'a> GetResolver<'a> for TestFiles {
270 fn data_path(&self, address: &'a [&'a str]) -> DataPath<'a> {
271 DataPath::new(self.path().to_path_buf(), address)
272 }
273 fn resolver(&self) -> DataResolver {
274 DataResolver {
275 root: self.path().to_path_buf(),
276 }
277 }
278 }
279
280 #[test]
281 fn resolves_num() -> Result<()> {
282 color_eyre::install()?;
283 let mocks = TestFiles::new();
284 mocks.file(
285 "index.yml",
286 indoc! {"
287 ---
288 1
289 "},
290 );
291 let v: i32 = mocks.resolver().get(&[])?;
292 assert_eq!(v, 1);
293 Ok(())
294 }
295
296 #[test]
297 fn resolves_list_num_accross_files() -> Result<()> {
298 let mocks = TestFiles::new();
299 mocks
301 .file(
302 "a.yml",
303 indoc! {"
304 ---
305 1
306 "},
307 )
308 .file(
309 "b.yml",
310 indoc! {"
311 ---
312 2
313 "},
314 );
315
316 let mut v: Vec<i32> = mocks.resolver().get(&[])?;
317 v.sort();
319 assert_eq!(v, vec![1, 2]);
320 Ok(())
321 }
322
323 #[test]
324 fn resolves_object_from_index() -> Result<()> {
325 let mocks = TestFiles::new();
326 mocks.file(
327 "index.yml",
328 indoc! {"
329 ---
330 id: 1
331 name: Objy
332 "},
333 );
334 let v: MyObj = mocks.resolver().get(&[])?;
335 assert_eq!(
336 v,
337 MyObj {
338 id: 1,
339 name: "Objy".to_owned()
340 }
341 );
342 Ok(())
343 }
344
345 #[test]
346 fn resolves_object_from_broken_files() -> Result<()> {
347 let mocks = TestFiles::new();
348 mocks
349 .file(
350 "id.yml",
351 indoc! {"
352 ---
353 1
354 "},
355 )
356 .file(
357 "name.yml",
358 indoc! {"
359 ---
360 Objy
361 "},
362 );
363 let v: MyObj = mocks.resolver().get(&[])?;
364 assert_eq!(
365 v,
366 MyObj {
367 id: 1,
368 name: "Objy".to_owned()
369 }
370 );
371 Ok(())
372 }
373
374 #[test]
375 fn resolves_deep_object_from_index() -> Result<()> {
376 let mocks = TestFiles::new();
377 mocks.file(
378 "index.yml",
379 indoc! {"
380 ---
381 my_obj:
382 id: 1
383 name: Objy
384 my_list:
385 - id: 1
386 alias: Obbo
387 - id: 2
388 alias: Ali
389 "},
390 );
391 let v: Query = mocks.resolver().get(&[])?;
392 assert_eq!(
393 v,
394 Query {
395 my_obj: MyObj {
396 id: 1,
397 name: "Objy".to_owned()
398 },
399 my_list: vec![
400 MyOtherObj {
401 id: 1,
402 alias: "Obbo".to_owned(),
403 },
404 MyOtherObj {
405 id: 2,
406 alias: "Ali".to_owned(),
407 },
408 ]
409 }
410 );
411 Ok(())
412 }
413
414 #[test]
415 fn resolves_list_from_map() -> Result<()> {
416 let mocks = TestFiles::new();
417 mocks.file(
418 "index.yml",
419 indoc! {"
420 ---
421 Obbo:
422 id: 1
423 Ali:
424 id: 2
425 "},
426 );
427 let v: Vec<MyOtherObj> = mocks.resolver().get(&[])?;
428 assert_eq!(
429 v,
430 vec![
431 MyOtherObj {
432 id: 1,
433 alias: "Obbo".to_owned(),
434 },
435 MyOtherObj {
436 id: 2,
437 alias: "Ali".to_owned(),
438 },
439 ]
440 );
441 Ok(())
442 }
443
444 #[test]
445 fn resolves_nested_list_from_files() -> Result<()> {
446 let mocks = TestFiles::new();
447 mocks
448 .file(
449 "my_obj/index.yml",
450 indoc! {"
451 ---
452 id: 1
453 name: Objy
454 "},
455 )
456 .file(
457 "my_list/x.yml",
458 indoc! {"
459 ---
460 id: 1
461 alias: Obbo
462 "},
463 )
464 .file(
465 "my_list/y.yml",
466 indoc! {"
467 ---
468 id: 2
469 alias: Ali
470 "},
471 );
472 let mut v: Query = mocks.resolver().get(&[])?;
473 v.my_list.sort();
474 assert_eq!(
475 v,
476 Query {
477 my_obj: MyObj {
478 id: 1,
479 name: "Objy".to_owned()
480 },
481 my_list: vec![
482 MyOtherObj {
483 id: 1,
484 alias: "Obbo".to_owned(),
485 },
486 MyOtherObj {
487 id: 2,
488 alias: "Ali".to_owned(),
489 },
490 ]
491 }
492 );
493 Ok(())
494 }
495
496 #[test]
497 fn resolves_broken_nested_list_from_dir_index_files() -> Result<()> {
498 let mocks = TestFiles::new();
499 mocks
500 .file(
501 "my_obj/index.yml",
502 indoc! {"
503 ---
504 id: 1
505 name: Objy
506 "},
507 )
508 .file(
509 "my_list/x/index.yml",
510 indoc! {"
511 ---
512 id: 1
513 alias: Obbo
514 "},
515 )
516 .file(
517 "my_list/y/index.yml",
518 indoc! {"
519 ---
520 id: 2
521 alias: Ali
522 "},
523 );
524 let mut v: Query = mocks.resolver().get(&[])?;
525 v.my_list.sort();
526 assert_eq!(
527 v,
528 Query {
529 my_obj: MyObj {
530 id: 1,
531 name: "Objy".to_owned()
532 },
533 my_list: vec![
534 MyOtherObj {
535 id: 1,
536 alias: "Obbo".to_owned(),
537 },
538 MyOtherObj {
539 id: 2,
540 alias: "Ali".to_owned(),
541 },
542 ]
543 }
544 );
545 Ok(())
546 }
547
548 #[test]
549 fn resolves_broken_nested_list_from_dir_tree() -> Result<()> {
550 let mocks = TestFiles::new();
551 mocks
552 .file(
553 "my_obj/index.yml",
554 indoc! {"
555 ---
556 id: 1
557 name: Objy
558 "},
559 )
560 .file(
561 "my_list/x/index.yml",
562 indoc! {"
563 ---
564 id: 1
565 "},
566 )
567 .file(
568 "my_list/x/alias.yml",
569 indoc! {"
570 ---
571 Obbo
572 "},
573 )
574 .file(
575 "my_list/y/alias.yml",
576 indoc! {"
577 ---
578 Ali
579 "},
580 )
581 .file(
582 "my_list/y/id.yml",
583 indoc! {"
584 ---
585 2
586 "},
587 );
588 let mut v: Query = mocks.resolver().get(&[])?;
589 v.my_list.sort();
590 assert_eq!(
591 v,
592 Query {
593 my_obj: MyObj {
594 id: 1,
595 name: "Objy".to_owned()
596 },
597 my_list: vec![
598 MyOtherObj {
599 id: 1,
600 alias: "Obbo".to_owned(),
601 },
602 MyOtherObj {
603 id: 2,
604 alias: "Ali".to_owned(),
605 },
606 ]
607 }
608 );
609 Ok(())
610 }
611}