1use crate::escape_html;
2use serde_json::{Map, Value};
3use std::collections::BTreeMap;
4
5pub fn parse_path_segments(path: &str) -> Vec<String> {
12 if path.contains('[') {
13 let mut out = Vec::new();
14 let mut start = 0usize;
15 let bytes = path.as_bytes();
16 let mut index = 0usize;
17 while index < bytes.len() {
18 if bytes[index] == b'[' {
19 if start < index {
20 out.push(path[start..index].to_string());
21 }
22 let close = path[index + 1..]
23 .find(']')
24 .map(|value| value + index + 1)
25 .unwrap_or(bytes.len());
26 if index + 1 < close {
27 out.push(path[index + 1..close].to_string());
28 }
29 index = close + 1;
30 start = index;
31 } else {
32 index += 1;
33 }
34 }
35 if start < bytes.len() {
36 out.push(path[start..].to_string());
37 }
38 return out;
39 }
40
41 path.split('.')
42 .map(str::trim)
43 .filter(|segment| !segment.is_empty())
44 .map(ToString::to_string)
45 .collect()
46}
47
48pub fn dot_path(path: &str) -> String {
50 parse_path_segments(path).join(".")
51}
52
53pub fn value_as_string(value: Option<&Value>) -> String {
55 value
56 .and_then(Value::as_str)
57 .unwrap_or_default()
58 .to_string()
59}
60
61pub struct FormData<'a> {
63 root: &'a Value,
64}
65
66impl<'a> FormData<'a> {
67 pub fn new(root: &'a Value) -> Self {
68 Self { root }
69 }
70
71 pub fn value(&self, path: &str) -> Option<&'a Value> {
72 let segments = parse_path_segments(path);
73 if segments.is_empty() {
74 return Some(self.root);
75 }
76 let mut current = self.root;
77 for segment in segments {
78 match current {
79 Value::Object(map) => {
80 current = map.get(&segment)?;
81 }
82 Value::Array(items) => {
83 let index = segment.parse::<usize>().ok()?;
84 current = items.get(index)?;
85 }
86 _ => return None,
87 }
88 }
89 Some(current)
90 }
91
92 pub fn string(&self, path: &str) -> String {
93 self.value(path)
94 .and_then(Value::as_str)
95 .unwrap_or_default()
96 .to_string()
97 }
98
99 pub fn object(&self, path: &str) -> Option<&'a Map<String, Value>> {
100 self.value(path).and_then(Value::as_object)
101 }
102
103 pub fn array(&self, path: &str) -> Option<&'a Vec<Value>> {
104 self.value(path).and_then(Value::as_array)
105 }
106}
107
108#[derive(Debug, Clone, Default, PartialEq, Eq)]
110pub struct ValidationErrors {
111 inner: BTreeMap<String, String>,
112}
113
114impl ValidationErrors {
115 pub fn is_empty(&self) -> bool {
116 self.inner.is_empty()
117 }
118
119 pub fn get(&self, path: &str) -> Option<&String> {
120 self.inner.get(path)
121 }
122
123 pub fn set(&mut self, path: &str, value: Option<String>) {
124 match value {
125 Some(message) => {
126 self.inner.insert(path.to_string(), message);
127 }
128 None => {
129 self.inner.remove(path);
130 }
131 }
132 }
133
134 pub fn remove(&mut self, path: &str) {
135 self.inner.remove(path);
136 }
137
138 pub fn reindex_array(&mut self, prefix: &str, len: usize) {
143 let mut rebuilt = BTreeMap::new();
144 let base = format!("{prefix}.");
145 for (key, value) in &self.inner {
146 if let Some(rest) = key.strip_prefix(&base) {
147 let mut parts = rest.splitn(2, '.');
148 if let (Some(index), Some(field)) = (parts.next(), parts.next()) {
149 if let Ok(index) = index.parse::<usize>() {
150 if index < len {
151 rebuilt.insert(format!("{prefix}.{index}.{field}"), value.clone());
152 }
153 continue;
154 }
155 }
156 }
157 rebuilt.insert(key.clone(), value.clone());
158 }
159 self.inner = rebuilt;
160 }
161
162 pub fn as_map(&self) -> &BTreeMap<String, String> {
163 &self.inner
164 }
165
166 pub fn html(&self, path: &str) -> String {
167 self.get(path)
168 .map(|message| {
169 format!(
170 r#"<p class="error" role="alert">{}</p>"#,
171 escape_html(message)
172 )
173 })
174 .unwrap_or_default()
175 }
176}
177
178#[cfg(test)]
179mod tests {
180 use super::{dot_path, parse_path_segments, value_as_string, FormData, ValidationErrors};
181 use serde_json::json;
182
183 #[test]
184 fn parse_path_segments_supports_bracket_and_dot_paths() {
185 assert_eq!(
186 parse_path_segments("addresses[2][city]"),
187 vec!["addresses", "2", "city"]
188 );
189 assert_eq!(parse_path_segments("owner.email"), vec!["owner", "email"]);
190 }
191
192 #[test]
193 fn dotted_path_normalization_round_trips() {
194 assert_eq!(dot_path("owner[name]"), "owner.name");
195 assert_eq!(dot_path("addresses[1][street]"), "addresses.1.street");
196 }
197
198 #[test]
199 fn form_data_reads_nested_values() {
200 let payload = json!({
201 "owner": { "name": "Ada", "email": "ada@example.test" },
202 "addresses": [
203 { "street": "42 Logic Rd", "city": "Math City" }
204 ]
205 });
206 let form = FormData::new(&payload);
207 assert_eq!(form.string("owner[name]"), "Ada");
208 assert_eq!(form.string("owner.email"), "ada@example.test");
209 assert_eq!(form.string("addresses[0][city]"), "Math City");
210 }
211
212 #[test]
213 fn value_as_string_returns_empty_for_non_strings() {
214 let payload = json!({
215 "name": "Ada",
216 "age": 19
217 });
218 assert_eq!(value_as_string(payload.get("name")), "Ada");
219 assert_eq!(value_as_string(payload.get("age")), "");
220 assert_eq!(value_as_string(payload.get("missing")), "");
221 }
222
223 #[test]
224 fn validation_errors_set_remove_and_reindex() {
225 let mut errors = ValidationErrors::default();
226 errors.set("addresses.0.street", Some("Street required".to_string()));
227 errors.set("addresses.1.city", Some("City required".to_string()));
228 errors.set("owner.name", Some("Owner required".to_string()));
229 errors.reindex_array("addresses", 1);
230
231 assert!(errors.get("addresses.0.street").is_some());
232 assert!(errors.get("addresses.1.city").is_none());
233 assert!(errors.get("owner.name").is_some());
234 }
235
236 #[test]
237 fn form_data_object_array_and_missing_paths_cover_edge_cases() {
238 let payload = json!({
239 "owner": { "name": "Ada", "email": "ada@example.test" },
240 "addresses": [
241 { "street": "42 Logic Rd", "city": "Math City" },
242 { "street": "73 Compile Ln", "city": "Rustville" }
243 ]
244 });
245 let form = FormData::new(&payload);
246
247 assert_eq!(form.value(""), Some(&payload));
248 assert!(form.object("owner").is_some());
249 assert!(form.array("addresses").is_some());
250 assert_eq!(form.string("owner[missing]"), "");
251 assert!(form.value("addresses[9][city]").is_none());
252 assert!(form.value("owner[0]").is_none());
253 }
254
255 #[test]
256 fn validation_errors_html_and_optional_set_paths_are_supported() {
257 let mut errors = ValidationErrors::default();
258 assert!(errors.is_empty());
259 assert_eq!(errors.html("owner.name"), "");
260
261 errors.set("owner.name", Some("<invalid>".to_string()));
262 assert!(!errors.is_empty());
263 assert!(errors.html("owner.name").contains("<invalid>"));
264 assert!(errors.as_map().contains_key("owner.name"));
265
266 errors.set("owner.name", None);
267 assert!(errors.get("owner.name").is_none());
268 errors.remove("owner.name");
269 assert!(errors.is_empty());
270 }
271
272 #[test]
273 fn validation_reindex_keeps_non_numeric_array_segments() {
274 let mut errors = ValidationErrors::default();
275 errors.set("addresses.foo.city", Some("bad key".to_string()));
276 errors.set("addresses.0.city", Some("required".to_string()));
277 errors.reindex_array("addresses", 1);
278
279 assert_eq!(
280 errors.get("addresses.foo.city").map(String::as_str),
281 Some("bad key")
282 );
283 assert_eq!(
284 errors.get("addresses.0.city").map(String::as_str),
285 Some("required")
286 );
287 }
288}