1use std::collections::HashMap;
8use crate::spec::{ParamsSpec, ParamDef};
9
10pub type ParamValues = HashMap<String, serde_json::Value>;
12
13pub fn collect_param_defaults(params_specs: &[&ParamsSpec]) -> ParamValues {
17 let mut values = ParamValues::new();
18
19 for spec in params_specs {
20 for param in &spec.params {
21 let key = if let Some(ref name) = spec.name {
22 format!("{}.{}", name, param.id)
23 } else {
24 param.id.clone()
25 };
26
27 if let Some(ref default) = param.default {
28 values.insert(key, default.clone());
29 }
30 }
31 }
32
33 values
34}
35
36pub fn resolve_param_references(yaml: &str, param_values: &ParamValues) -> String {
45 let mut result = yaml.to_string();
46
47 let mut keys: Vec<&String> = param_values.keys().collect();
50 keys.sort_by_key(|b| std::cmp::Reverse(b.len()));
51
52 for key in keys {
53 let value = ¶m_values[key];
54 let pattern = format!("\"${}\"", key);
55
56 if result.contains(&pattern) {
57 let replacement = value_to_yaml(value);
58 result = result.replace(&pattern, &replacement);
59 }
60
61 let _unquoted_pattern = format!("${}", key);
63 }
66
67 result
68}
69
70fn value_to_yaml(value: &serde_json::Value) -> String {
72 match value {
73 serde_json::Value::String(s) => format!("\"{}\"", s),
74 serde_json::Value::Number(n) => n.to_string(),
75 serde_json::Value::Bool(b) => b.to_string(),
76 serde_json::Value::Null => "null".to_string(),
77 serde_json::Value::Array(arr) => {
78 let items: Vec<String> = arr.iter().map(value_to_yaml).collect();
79 format!("[{}]", items.join(", "))
80 }
81 serde_json::Value::Object(_obj) => {
82 serde_json::to_string(value).unwrap_or_else(|_| "{}".to_string())
84 }
85 }
86}
87
88pub fn extract_inline_param_defaults(yaml: &str) -> ParamValues {
94 let mut values = ParamValues::new();
95
96 if let Ok(serde_yaml::Value::Mapping(map)) = serde_yaml::from_str::<serde_yaml::Value>(yaml) {
99 if let Some(params_val) = map.get(serde_yaml::Value::String("params".into())) {
100 if let Ok(params) = serde_yaml::from_value::<Vec<ParamDef>>(params_val.clone()) {
101 for param in ¶ms {
102 if let Some(ref default) = param.default {
103 values.insert(param.id.clone(), default.clone());
104 }
105 }
106 }
107 }
108 }
109
110 values
111}
112
113pub fn extract_param_references(yaml: &str) -> Vec<String> {
116 let mut refs = Vec::new();
117 let mut i = 0;
119 let bytes = yaml.as_bytes();
120 while i < bytes.len() {
121 if bytes[i] == b'$' && i > 0 && (bytes[i - 1] == b'"' || bytes[i - 1] == b' ' || bytes[i - 1] == b':') {
122 let start = i + 1;
123 let mut end = start;
124 while end < bytes.len()
125 && (bytes[end].is_ascii_alphanumeric() || bytes[end] == b'_' || bytes[end] == b'.')
126 {
127 end += 1;
128 }
129 if end > start {
130 refs.push(yaml[start..end].to_string());
131 }
132 i = end;
133 } else {
134 i += 1;
135 }
136 }
137 refs.sort();
138 refs.dedup();
139 refs
140}
141
142#[cfg(test)]
143mod tests {
144 #![allow(clippy::unwrap_used)]
145 use super::*;
146
147 #[test]
148 fn collect_defaults_named_params() {
149 let spec = ParamsSpec {
150 version: 1,
151 name: Some("dashboard_filters".to_string()),
152 params: vec![
153 ParamDef {
154 id: "region".to_string(),
155 param_type: "multiselect".to_string(),
156 label: "Region".to_string(),
157 options: Some(vec!["US".into(), "EU".into()]),
158 default: Some(serde_json::json!(["US", "EU"])),
159 placeholder: None,
160 layout: None,
161 },
162 ParamDef {
163 id: "min_revenue".to_string(),
164 param_type: "number".to_string(),
165 label: "Min Revenue".to_string(),
166 options: None,
167 default: Some(serde_json::json!(50000)),
168 placeholder: None,
169 layout: None,
170 },
171 ],
172 };
173
174 let defaults = collect_param_defaults(&[&spec]);
175 assert_eq!(defaults.get("dashboard_filters.region"), Some(&serde_json::json!(["US", "EU"])));
176 assert_eq!(defaults.get("dashboard_filters.min_revenue"), Some(&serde_json::json!(50000)));
177 }
178
179 #[test]
180 fn resolve_string_param() {
181 let mut params = ParamValues::new();
182 params.insert("top_n".to_string(), serde_json::json!(10));
183
184 let yaml = r#"limit: "$top_n""#;
185 let resolved = resolve_param_references(yaml, ¶ms);
186 assert_eq!(resolved, "limit: 10");
187 }
188
189 #[test]
190 fn resolve_array_param() {
191 let mut params = ParamValues::new();
192 params.insert("dashboard_filters.region".to_string(), serde_json::json!(["US", "EU"]));
193
194 let yaml = r#"value: "$dashboard_filters.region""#;
195 let resolved = resolve_param_references(yaml, ¶ms);
196 assert_eq!(resolved, r#"value: ["US", "EU"]"#);
197 }
198
199 #[test]
200 fn unresolved_param_stays() {
201 let params = ParamValues::new();
202 let yaml = r#"value: "$unknown.param""#;
203 let resolved = resolve_param_references(yaml, ¶ms);
204 assert_eq!(resolved, r#"value: "$unknown.param""#);
205 }
206
207 #[test]
208 fn extract_refs() {
209 let yaml = r#"
210 value: "$dashboard_filters.region"
211 limit: "$top_n"
212 "#;
213 let refs = extract_param_references(yaml);
214 assert!(refs.contains(&"dashboard_filters.region".to_string()));
215 assert!(refs.contains(&"top_n".to_string()));
216 }
217}