1use crate::imports::*;
2
3#[derive(Debug, Clone)]
33pub enum Param {
34 Literal(Value),
36 Ref(String),
40 Template(Vec<Param>),
45 Array(Vec<Param>),
48 Map(HashMap<String, Param>),
51}
52
53impl Param {
54 pub fn literal(value: impl Into<Value>) -> Self {
57 Param::Literal(value.into())
58 }
59 pub fn reference(path: impl Into<String>) -> Self {
61 Param::Ref(path.into())
62 }
63 pub fn template(parts: Vec<Param>) -> Self {
65 Param::Template(parts)
66 }
67 pub fn array(items: Vec<Param>) -> Self {
69 Param::Array(items)
70 }
71 pub fn map(entries: HashMap<String, Param>) -> Self {
73 Param::Map(entries)
74 }
75
76 pub(crate) fn resolve(&self, store: &Store<StoreEntry>) -> Result<StoreEntry, OperationError> {
78 match self {
79 Param::Literal(v) => {
80 let ty = v.get_type();
81 Ok(StoreEntry::Var {
82 value: v.clone(),
83 ty,
84 })
85 }
86
87 Param::Ref(path) => {
88 store
89 .get(path)
90 .cloned()
91 .map_err(|_| OperationError::ReferenceNotFound {
92 reference: path.clone(),
93 })
94 }
95
96 Param::Template(parts) => {
97 let mut result = String::new();
98 for part in parts {
99 let resolved = part.resolve(store)?;
100 match resolved {
101 StoreEntry::Var { value, .. } => {
102 result.push_str(&value.to_string());
103 }
104 _ => {
105 return Err(OperationError::InvalidTemplatePart);
106 }
107 }
108 }
109 Ok(StoreEntry::Var {
110 ty: Type::Text,
111 value: Value::Text(result),
112 })
113 }
114
115 Param::Array(items) => {
116 let resolved: Result<Vec<StoreEntry>, _> =
117 items.iter().map(|item| item.resolve(store)).collect();
118 Ok(StoreEntry::Array(resolved?))
119 }
120
121 Param::Map(entries) => {
122 let resolved: Result<HashMap<String, StoreEntry>, _> = entries
123 .iter()
124 .map(|(k, v)| v.resolve(store).map(|resolved| (k.clone(), resolved)))
125 .collect();
126 Ok(StoreEntry::Map(resolved?))
127 }
128 }
129 }
130
131 pub fn get_references(&self) -> Vec<String> {
135 match self {
136 Param::Literal(_) => vec![],
137 Param::Ref(path) => vec![path.clone()],
138 Param::Template(parts) => parts.iter().flat_map(|p| p.get_references()).collect(),
139 Param::Array(items) => items.iter().flat_map(|i| i.get_references()).collect(),
140 Param::Map(entries) => entries.values().flat_map(|v| v.get_references()).collect(),
141 }
142 }
143}
144
145impl<V: Into<Value>> From<V> for Param {
146 fn from(v: V) -> Self {
147 Param::Literal(v.into())
149 }
150}
151
152#[derive(Debug, Clone)]
160pub struct Parameters(HashMap<String, Param>);
161
162impl Parameters {
163 pub fn new(map: HashMap<String, Param>) -> Self {
166 Parameters(map)
167 }
168
169 pub fn get(&self, key: &str) -> Option<&Param> {
172 self.0.get(key)
173 }
174
175 pub fn keys(&self) -> impl Iterator<Item = &String> {
177 self.0.keys()
178 }
179
180 pub fn values(&self) -> impl Iterator<Item = &Param> {
182 self.0.values()
183 }
184
185 pub fn iter(&self) -> impl Iterator<Item = (&String, &Param)> {
187 self.0.iter()
188 }
189
190 fn resolve_all(
191 &self,
192 store: &Store<StoreEntry>,
193 ) -> Result<HashMap<String, StoreEntry>, OperationError> {
194 self.0
195 .iter()
196 .map(|(k, v)| v.resolve(store).map(|resolved| (k.clone(), resolved)))
197 .collect()
198 }
199
200 pub fn resolve_in_store(
204 &self,
205 prefix_part: impl Into<String>,
206 store: &mut Store<StoreEntry>,
207 ) -> Result<String, OperationError> {
208 let resolved = self.resolve_all(store)?;
209 let prefix: String = prefix_part.into();
210
211 for (key, value) in resolved {
212 store
213 .insert(format!("{}.{}", prefix, key), value)
214 .map_err(|e| OperationError::ParameterResolutionFailed {
215 parameter: key.clone(),
216 reason: e.to_string(),
217 })?;
218 }
219 Ok(prefix)
220 }
221
222 pub fn resolve_to_store(
228 &self,
229 prefix_part: impl Into<String>,
230 src: &Store<StoreEntry>,
231 dst: &mut Store<StoreEntry>,
232 ) -> Result<(), OperationError> {
233 let resolved = self.resolve_all(src)?;
234 let prefix: String = prefix_part.into();
235
236 for (key, value) in resolved {
237 dst.insert(format!("{}.{}", prefix, key), value)
238 .map_err(|e| OperationError::ParameterResolutionFailed {
239 parameter: key.clone(),
240 reason: e.to_string(),
241 })?;
242 }
243 Ok(())
244 }
245}
246
247#[macro_export]
269macro_rules! params {
270 () => {
271 $crate::extend::Parameters::new(
272 ::std::collections::HashMap::<String, $crate::prelude::Param>::new(),
273 )
274 };
275 ($($key:expr => $value:expr),+ $(,)?) => {{
276 let mut map = ::std::collections::HashMap::<String, $crate::prelude::Param>::new();
277 $(
278 map.insert($key.into(), $value.into());
279 )+
280 $crate::extend::Parameters::new(map)
281 }};
282}
283
284#[cfg(test)]
285mod tests {
286 use super::*;
287
288 #[test]
289 fn scalar_literals() {
290 let mut store = Store::<StoreEntry>::new();
291 store.define_var("unused", "placeholder").unwrap();
292
293 let p = params!(
294 "name" => "number",
295 "value" => 42i64,
296 "flag" => true,
297 );
298
299 let resolved = p.get("name").unwrap().resolve(&store).unwrap();
300 assert_eq!(
301 resolved,
302 StoreEntry::Var {
303 value: Value::Text("number".into()),
304 ty: Type::Text,
305 }
306 );
307
308 let resolved = p.get("value").unwrap().resolve(&store).unwrap();
309 assert_eq!(
310 resolved,
311 StoreEntry::Var {
312 value: Value::Integer(42),
313 ty: Type::Integer,
314 }
315 );
316 }
317
318 #[test]
319 fn reference_resolution() {
320 let mut store = Store::<StoreEntry>::new();
321 store.define_var("workspace_id", "ws-abc-123").unwrap();
322
323 let p = params!(
324 "workspace" => Param::reference("workspace_id"),
325 );
326
327 let resolved = p.get("workspace").unwrap().resolve(&store).unwrap();
328 assert_eq!(
329 resolved,
330 StoreEntry::Var {
331 value: Value::Text("ws-abc-123".into()),
332 ty: Type::Text,
333 }
334 );
335 }
336
337 #[test]
338 fn reference_missing() {
339 let store = Store::<StoreEntry>::new();
340
341 let p = Param::reference("nonexistent");
342 assert!(p.resolve(&store).is_err());
343 }
344
345 #[test]
346 fn template_with_refs() {
347 let mut store = Store::<StoreEntry>::new();
348 store.define_var("days", 7i64).unwrap();
349
350 let p = Param::template(vec![
351 Param::literal("SigninLogs | ago("),
352 Param::reference("days"),
353 Param::literal("d)"),
354 ]);
355
356 let resolved = p.resolve(&store).unwrap();
357 assert_eq!(
358 resolved,
359 StoreEntry::Var {
360 value: Value::Text("SigninLogs | ago(7d)".into()),
361 ty: Type::Text,
362 }
363 );
364 }
365
366 #[test]
367 fn array_of_literals_and_refs() {
368 let mut store = Store::<StoreEntry>::new();
369 store.define_var("auto_label", "auto-triaged").unwrap();
370
371 let p = Param::array(vec![
372 Param::literal("high-priority"),
373 Param::reference("auto_label"),
374 ]);
375
376 let resolved = p.resolve(&store).unwrap();
377 match resolved {
378 StoreEntry::Array(items) => {
379 assert_eq!(items.len(), 2);
380 assert_eq!(
381 items[0].get_value().unwrap(),
382 &Value::Text("high-priority".into())
383 );
384 assert_eq!(
385 items[1].get_value().unwrap(),
386 &Value::Text("auto-triaged".into())
387 );
388 }
389 _ => panic!("Expected Array"),
390 }
391 }
392
393 #[test]
394 fn inline_map() {
395 let mut store = Store::<StoreEntry>::new();
396 store.define_var("ws_id", "ws-001").unwrap();
397
398 let p = params!(
399 "severity" => "High",
400 "config" => Param::map(HashMap::from([
401 ("workspace".into(), Param::reference("ws_id")),
402 ("timeout".into(), Param::literal(30i64)),
403 ])),
404 );
405
406 let resolved = p.get("config").unwrap().resolve(&store).unwrap();
407 match resolved {
408 StoreEntry::Map(entries) => {
409 assert_eq!(
410 entries.get("workspace").unwrap().get_value().unwrap(),
411 &Value::Text("ws-001".into())
412 );
413 assert_eq!(
414 entries.get("timeout").unwrap().get_value().unwrap(),
415 &Value::Integer(30)
416 );
417 }
418 _ => panic!("Expected Map"),
419 }
420 }
421
422 #[test]
423 fn nested_map_with_array_and_template() {
424 let mut store = Store::<StoreEntry>::new();
425 store.define_var("tenant", "contoso.com").unwrap();
426 store.define_var("days", 30i64).unwrap();
427
428 let p = Param::map(HashMap::from([
429 ("workspace".into(), Param::literal("ws-sentinel-01")),
430 (
431 "scopes".into(),
432 Param::array(vec![
433 Param::literal("https://graph.microsoft.com/.default"),
434 Param::literal("https://management.azure.com/.default"),
435 ]),
436 ),
437 (
438 "query".into(),
439 Param::template(vec![
440 Param::literal("SigninLogs | where TenantId == '"),
441 Param::reference("tenant"),
442 Param::literal("' | ago("),
443 Param::reference("days"),
444 Param::literal("d)"),
445 ]),
446 ),
447 ]));
448
449 let resolved = p.resolve(&store).unwrap();
450 match resolved {
451 StoreEntry::Map(entries) => {
452 assert_eq!(
453 entries.get("workspace").unwrap().get_value().unwrap(),
454 &Value::Text("ws-sentinel-01".into())
455 );
456
457 match entries.get("scopes").unwrap() {
458 StoreEntry::Array(items) => assert_eq!(items.len(), 2),
459 _ => panic!("Expected Array for scopes"),
460 }
461
462 assert_eq!(
463 entries.get("query").unwrap().get_value().unwrap(),
464 &Value::Text("SigninLogs | where TenantId == 'contoso.com' | ago(30d)".into())
465 );
466 }
467 _ => panic!("Expected Map"),
468 }
469 }
470
471 #[test]
472 fn get_references_extracts_all_refs() {
473 let p = Param::map(HashMap::from([
474 ("literal".into(), Param::literal("ignored")),
475 ("ref".into(), Param::reference("ws_id")),
476 (
477 "nested".into(),
478 Param::array(vec![
479 Param::reference("tenant"),
480 Param::template(vec![Param::literal("prefix_"), Param::reference("suffix")]),
481 ]),
482 ),
483 ]));
484
485 let mut refs = p.get_references();
486 refs.sort();
487 assert_eq!(refs, vec!["suffix", "tenant", "ws_id"]);
488 }
489}