confql_data_resolver/
lib.rs

1//! Filesystem yaml data resolvers.
2
3#![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/// Data resolution and value manipulation errors
15#[derive(Error, Debug)]
16pub enum DataResolverError {
17    /// Merge attempted into a non-mapping (i.e. primitive or list)
18    #[error("Cannot merge into non-mapping `{0:?}`")]
19    CannotMergeIntoNonMapping(serde_yaml::Value),
20    /// Merge attempted of two types with no obvious general method of doing so
21    #[error("Incompatible merge `{dst:?}` <- `{src:?}`")]
22    IncompatibleYamlMerge {
23        /// Source value which we were attempting to merge into destination
24        src: serde_yaml::Value,
25        /// Destination value into which we were attempting to merge source
26        dst: serde_yaml::Value,
27    },
28    /// [std::io::Error]
29    #[error(transparent)]
30    IOError(#[from] std::io::Error),
31    /// Attempt made to access data at a non-existing key within a mapping
32    #[error("Key `{0}` not found")]
33    KeyNotFound(String),
34    /// [serde_yaml::Error]
35    #[error(transparent)]
36    YamlError(#[from] serde_yaml::Error),
37}
38
39/// Clients interact with this struct for data resolution operations.
40/// In particular, this forms an important part of the `juniper::Context`
41/// generated by the procedural macros.  Essentially this holds a [PathBuf]
42/// pointing at the data root directory, and exposes a [get](DataResolver::get()) method for
43/// trying to resolve a generic type at a specified data address under
44/// that root directory.
45pub struct DataResolver {
46    root: PathBuf,
47}
48
49impl DataResolver {
50    /// Try to retrieve an instance of a type at a specified address under
51    /// the data root directory.
52    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
69/// This trait, when implemented on a type, attaches methods for retrieving a [serde_yaml::Value]
70/// representation of that type.  For primitives, a default impl will do.  For structs, you will
71/// mostly specify the [merge_properties](ResolveValue::merge_properties()) function in a very straightforward way, i.e.
72///
73/// ```
74/// use confql_data_resolver::{DataPath, DataResolverError, Merge, ResolveValue};
75/// use serde_yaml;
76///
77/// struct MyObj {
78///     id: i32,
79///     name: String,
80/// }
81///
82/// impl ResolveValue for MyObj {
83///     fn merge_properties<'a>(
84///         value: &'a mut serde_yaml::Value,
85///         data_path: &DataPath,
86///     ) -> Result<&'a mut serde_yaml::Value, DataResolverError> {
87///         if let Ok(id) = i32::resolve_value(data_path.join("id")) {
88///             value.merge_at("id", id)?;
89///         }
90///         if let Ok(name) = String::resolve_value(data_path.join("name")) {
91///             value.merge_at("name", name)?;
92///         }
93///         Ok(value)
94///     }
95/// }
96/// ```
97///
98/// In fact, that's what a procedural macro in the codebase does for you.
99pub trait ResolveValue {
100    /// Implement this for structs as described in [ResolveValue].
101    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    /// Create a base value from an identifier.  Useful when building an array, where
108    /// some fields are defined with `@confql(arrayIdentifier: true)` in the GraphQL
109    /// schema.  Then you can pre-populate said fields with a file name or mapping
110    /// key.
111    fn init_with_identifier(_identifier: serde_yaml::Value) -> serde_yaml::Value {
112        serde_yaml::Value::Null
113    }
114    /// Resolve data from the given [DataPath].  The default implementation should be sufficient
115    /// in most cases.
116    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    /// Resolve a starting value before data acquisition from actual file
128    /// content.  [Null](serde_yaml::Value::Null) (default impl) is a good starting value in most cases,
129    /// because it accepts any merge.
130    /// Explicitly implement this in cases like
131    /// `impl<T: ResolveValue> ResolveValue for Vec<T>`
132    /// where the initial value might not be null (i.e. in the [Vec<T>] case, some
133    /// fields may be predefined by the file stem of your [DataPath].
134    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        // See above comment about in future chosing not this behaviour
300        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        // we get not guarantee on order with file iterator
318        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}