dofigen_lib/
extend.rs

1#[cfg(feature = "permissive")]
2use crate::OneOrMany;
3use crate::{dofigen_struct::*, DofigenContext, Error, Result};
4use relative_path::RelativePath;
5#[cfg(feature = "json_schema")]
6use schemars::JsonSchema;
7use serde::{de::DeserializeOwned, Deserialize};
8use std::iter;
9use struct_patch::Merge;
10
11#[cfg(feature = "permissive")]
12type VecType<T> = OneOrMany<T>;
13
14#[cfg(not(feature = "permissive"))]
15type VecType<T> = Vec<T>;
16
17#[derive(Debug, Clone, PartialEq, Default, Deserialize)]
18// #[serde(deny_unknown_fields)]
19#[serde(default)]
20#[cfg_attr(
21    feature = "json_schema",
22    derive(JsonSchema),
23    schemars(rename = "Extend<{T}>", default)
24)]
25pub struct Extend<T: Default + Merge> {
26    #[serde(alias = "extends")]
27    pub extend: VecType<Resource>,
28
29    // Can't use #[serde(flatten)] because of nested flattening is not managed by serde
30    #[serde(flatten)]
31    pub value: T,
32}
33
34impl<P> Extend<P>
35where
36    P: Default + DeserializeOwned + Clone + Merge,
37{
38    pub fn merge(&self, context: &mut DofigenContext) -> Result<P> {
39        if self.extend.is_empty() {
40            return Ok(self.value.clone().into());
41        }
42
43        // load extends files
44        let merged: Option<P> = self
45            .extend
46            .iter()
47            .map(|extend| {
48                let ret = extend.load::<Self>(context)?.merge(context)?;
49                context.pop_resource_stack();
50                Ok(ret)
51            })
52            .collect::<Result<Vec<_>>>()?
53            .into_iter()
54            .chain(iter::once(self.value.clone()))
55            .reduce(|a, b| a.merge(b));
56
57        Ok(merged.expect("Since we have at least one value, we should have a merged value"))
58    }
59}
60
61impl Resource {
62    fn load_resource_content(&self, context: &mut DofigenContext) -> Result<String> {
63        let resource = match self {
64            Resource::File(path) => {
65                if path.is_absolute() {
66                    Resource::File(path.clone())
67                } else {
68                    if let Some(current_resource) = context.current_resource() {
69                        match current_resource {
70                            Resource::File(file) => {
71                                let current_file_relative_path =
72                                    RelativePath::from_path(file).map_err(Error::display)?;
73                                let relative_path =
74                                    RelativePath::from_path(path).map_err(Error::display)?;
75                                let relative_path = current_file_relative_path
76                                    .join("..")
77                                    .join_normalized(relative_path);
78                                Resource::File(relative_path.to_path(""))
79                            }
80                            Resource::Url(url) => {
81                                Resource::Url(url.join(path.to_str().unwrap()).unwrap())
82                            }
83                        }
84                    } else {
85                        Resource::File(path.clone())
86                    }
87                }
88            }
89            Resource::Url(url) => Resource::Url(url.clone()),
90        };
91
92        // push the resource to the stack
93        context.push_resource_stack(resource.clone())?;
94
95        // load the resource content
96        context.get_resource_content(resource)
97    }
98
99    pub fn load<T>(&self, context: &mut DofigenContext) -> Result<T>
100    where
101        T: DeserializeOwned,
102    {
103        Ok(
104            serde_yaml::from_str(self.load_resource_content(context)?.as_str()).map_err(|err| {
105                Error::Custom(format!(
106                    "Could not deserialize resource {:?}: {}",
107                    self, err
108                ))
109            })?,
110        )
111    }
112}
113
114#[cfg(test)]
115mod test {
116    use super::*;
117    use pretty_assertions_sorted::assert_eq_sorted;
118
119    mod deserialize {
120        use super::*;
121        use struct_patch::Patch;
122
123        mod extend {
124
125            use super::*;
126
127            #[derive(Deserialize, Patch)]
128            #[patch(
129                attribute(derive(Deserialize, Debug, Clone, PartialEq, Default)),
130                attribute(serde(default))
131            )]
132            struct TestStruct {
133                pub name: Option<String>,
134                #[serde(flatten)]
135                #[patch(name = "TestSubStructPatch", attribute(serde(flatten)))]
136                pub sub: TestSubStruct,
137            }
138
139            #[derive(Deserialize, Debug, Clone, PartialEq, Default, Patch)]
140            #[patch(
141                attribute(derive(Deserialize, Debug, Clone, PartialEq, Default)),
142                attribute(serde(deny_unknown_fields, default))
143            )]
144            struct TestSubStruct {
145                pub level: u16,
146            }
147
148            #[test]
149            fn empty() {
150                let data = r#"{}"#;
151
152                let extend_image: Extend<TestStructPatch> = serde_yaml::from_str(data).unwrap();
153
154                assert_eq_sorted!(
155                    extend_image,
156                    Extend {
157                        value: TestStructPatch {
158                            sub: Some(TestSubStructPatch::default()),
159                            ..Default::default()
160                        },
161                        ..Default::default()
162                    }
163                );
164            }
165
166            #[test]
167            fn only_name() {
168                let data = "name: ok";
169
170                let extend: Extend<TestStructPatch> = serde_yaml::from_str(data).unwrap();
171
172                assert_eq_sorted!(
173                    extend,
174                    Extend {
175                        value: TestStructPatch {
176                            name: Some(Some("ok".into())),
177                            sub: Some(TestSubStructPatch::default()),
178                            ..Default::default()
179                        },
180                        ..Default::default()
181                    }
182                );
183            }
184
185            #[test]
186            fn only_sub() {
187                let data = "level: 1";
188
189                let extend: Extend<TestStructPatch> = serde_yaml::from_str(data).unwrap();
190
191                assert_eq_sorted!(
192                    extend,
193                    Extend {
194                        value: TestStructPatch {
195                            sub: Some(TestSubStructPatch {
196                                level: Some(1),
197                                ..Default::default()
198                            }),
199                            ..Default::default()
200                        },
201                        ..Default::default()
202                    }
203                );
204            }
205        }
206
207        mod extend_dofigen {
208            use super::*;
209            use crate::{DofigenPatch, ImageNamePatch, RunPatch, StagePatch};
210
211            // #[ignore = "Not managed yet by serde because of multilevel flatten: https://serde.rs/field-attrs.html#flatten"]
212            #[test]
213            fn empty() {
214                let data = r#"{}"#;
215
216                let extend_image: Extend<DofigenPatch> = serde_yaml::from_str(data).unwrap();
217
218                assert_eq_sorted!(
219                    extend_image,
220                    Extend {
221                        value: DofigenPatch {
222                            stage: Some(StagePatch {
223                                run: Some(RunPatch::default()),
224                                ..Default::default()
225                            }),
226                            ..Default::default()
227                        },
228                        ..Default::default()
229                    }
230                );
231            }
232
233            #[test]
234            fn only_from() {
235                let data = r#"
236fromImage:
237  path: ubuntu
238"#;
239
240                let extend_image: Extend<DofigenPatch> = serde_yaml::from_str(data).unwrap();
241
242                assert_eq_sorted!(
243                    extend_image,
244                    Extend {
245                        value: DofigenPatch {
246                            stage: Some(StagePatch {
247                                from: Some(FromContextPatch::FromImage(
248                                    ImageNamePatch {
249                                        path: Some("ubuntu".into()),
250                                        version: Some(None),
251                                        ..Default::default()
252                                    }
253                                    .into() // To manage permissive
254                                )),
255                                run: Some(RunPatch::default()),
256                                ..Default::default()
257                            }),
258                            ..Default::default()
259                        },
260                        ..Default::default()
261                    }
262                );
263            }
264        }
265    }
266}