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(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 #[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 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 context.push_resource_stack(resource.clone())?;
94
95 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 #[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() )),
255 run: Some(RunPatch::default()),
256 ..Default::default()
257 }),
258 ..Default::default()
259 },
260 ..Default::default()
261 }
262 );
263 }
264 }
265 }
266}