Skip to main content

json_eval_rs/jsoneval/
layout.rs

1use super::JSONEval;
2use crate::jsoneval::path_utils;
3use crate::time_block;
4
5use serde_json::Value;
6use std::mem;
7
8impl JSONEval {
9    /// Resolve layout references with optional evaluation
10    ///
11    /// # Arguments
12    ///
13    /// * `evaluate` - If true, runs evaluation before resolving layout. If false, only resolves layout.
14    ///
15    /// # Returns
16    ///
17    /// A Result indicating success or an error message.
18    pub fn resolve_layout(&mut self, evaluate: bool) -> Result<(), String> {
19        if evaluate {
20            // Use existing data
21            let data_str = serde_json::to_string(&self.data)
22                .map_err(|e| format!("Failed to serialize data: {}", e))?;
23            self.evaluate(&data_str, None, None, None)?;
24        }
25
26        self.resolve_layout_internal();
27        Ok(())
28    }
29
30    fn resolve_layout_internal(&mut self) {
31        time_block!("  resolve_layout_internal()", {
32            // Use cached layout paths (collected at parse time)
33            let layout_paths = self.layout_paths.clone();
34
35            time_block!("    resolve_layout_elements", {
36                for layout_path in layout_paths.iter() {
37                    self.resolve_layout_elements(layout_path);
38                }
39            });
40
41            // After resolving all references, propagate parent hidden/disabled to children
42            time_block!("    propagate_parent_conditions", {
43                for layout_path in layout_paths.iter() {
44                    self.propagate_parent_conditions(layout_path);
45                }
46            });
47
48            // Sync layout hidden state to schema definitions
49            // This ensures validation (which checks schema) respects layout hiding
50            time_block!("    sync_layout_hidden_to_schema", {
51                self.sync_layout_hidden_to_schema(&layout_paths);
52            });
53        });
54    }
55
56    /// Resolve $ref references in layout elements (recursively)
57    fn resolve_layout_elements(&mut self, layout_elements_path: &str) {
58        // Normalize path from schema format (#/) to JSON pointer format (/)
59        let normalized_path = path_utils::normalize_to_json_pointer(layout_elements_path);
60
61        // Always read elements from original schema (not evaluated_schema)
62        // This ensures we get fresh $ref entries on re-evaluation
63        let elements = if let Some(Value::Array(arr)) = self.schema.pointer(&normalized_path) {
64            arr.clone()
65        } else {
66            return;
67        };
68
69        // Extract the parent path from normalized_path
70        let parent_path = normalized_path
71            .trim_start_matches('/')
72            .replace("/elements", "")
73            .replace('/', ".");
74
75        // Process elements
76        let mut resolved_elements = Vec::with_capacity(elements.len());
77        for (index, element) in elements.iter().enumerate() {
78            let element_path = if parent_path.is_empty() {
79                format!("elements.{}", index)
80            } else {
81                format!("{}.elements.{}", parent_path, index)
82            };
83            let element_pointer = format!("{}/{}", normalized_path, index);
84            let resolved = self.resolve_element_ref_recursive(element.clone(), &element_path, &element_pointer);
85            resolved_elements.push(resolved);
86        }
87
88        // Write back the resolved elements
89        if let Some(target) = self.evaluated_schema.pointer_mut(&normalized_path) {
90            *target = Value::Array(resolved_elements);
91        }
92    }
93
94    fn resolve_element_ref_recursive(&self, element: Value, path_context: &str, original_pointer_path: &str) -> Value {
95        // First resolve the current element's $ref
96        let resolved = self.resolve_element_ref(element, original_pointer_path);
97
98        // Then recursively resolve any nested elements arrays
99        if let Value::Object(mut map) = resolved {
100            // Ensure all layout elements have metadata fields
101            if !map.contains_key("$parentHide") {
102                map.insert("$parentHide".to_string(), Value::Bool(false));
103            }
104
105            // Set path metadata for direct layout elements (without $ref)
106            if !map.contains_key("$fullpath") {
107                map.insert(
108                    "$fullpath".to_string(),
109                    Value::String(path_context.to_string()),
110                );
111            }
112
113            if !map.contains_key("$path") {
114                let last_segment = path_context.split('.').last().unwrap_or(path_context);
115                map.insert("$path".to_string(), Value::String(last_segment.to_string()));
116            }
117
118            // Check if this object has an "elements" array
119            if let Some(Value::Array(elements)) = map.get("elements") {
120                let mut resolved_nested = Vec::with_capacity(elements.len());
121                for (index, nested_element) in elements.iter().enumerate() {
122                    let nested_path = format!("{}.elements.{}", path_context, index);
123                    let nested_pointer = format!("{}/elements/{}", original_pointer_path, index);
124                    resolved_nested.push(
125                        self.resolve_element_ref_recursive(nested_element.clone(), &nested_path, &nested_pointer),
126                    );
127                }
128                map.insert("elements".to_string(), Value::Array(resolved_nested));
129            }
130
131            return Value::Object(map);
132        }
133
134        resolved
135    }
136
137    /// Resolve $ref in a single element
138    fn resolve_element_ref(&self, element: Value, original_pointer_path: &str) -> Value {
139        match element {
140            Value::Object(mut map) => {
141                // Check if element has $ref
142                if let Some(Value::String(ref_path)) = map.get("$ref").cloned() {
143                    // Convert ref_path to dotted notation for metadata storage
144                    let dotted_path = path_utils::pointer_to_dot_notation(&ref_path);
145
146                    // Extract last segment for $path
147                    let last_segment = dotted_path.split('.').last().unwrap_or(&dotted_path);
148
149                    // Inject metadata fields with dotted notation
150                    map.insert("$fullpath".to_string(), Value::String(dotted_path.clone()));
151                    map.insert("$path".to_string(), Value::String(last_segment.to_string()));
152                    map.insert("$parentHide".to_string(), Value::Bool(false));
153
154                    // Normalize to JSON pointer for actual lookup
155                    let normalized_path = if ref_path.starts_with('#') || ref_path.starts_with('/')
156                    {
157                        path_utils::normalize_to_json_pointer(&ref_path).into_owned()
158                    } else {
159                        let schema_pointer = path_utils::dot_notation_to_schema_pointer(&ref_path);
160                        let schema_path =
161                            path_utils::normalize_to_json_pointer(&schema_pointer).into_owned();
162
163                        if self.evaluated_schema.pointer(&schema_path).is_some() {
164                            schema_path
165                        } else {
166                            format!("/properties/{}", ref_path.replace('.', "/properties/"))
167                        }
168                    };
169
170                    // Get the referenced value
171                    if let Some(referenced_value) = self.evaluated_schema.pointer(&normalized_path)
172                    {
173                        let resolved = referenced_value.clone();
174
175                        if let Value::Object(mut resolved_map) = resolved {
176                            map.remove("$ref");
177
178                            // Special case: if resolved has $layout, flatten it
179                            if let Some(Value::Object(layout_obj)) = resolved_map.remove("$layout")
180                            {
181                                let mut result = layout_obj.clone();
182
183                                // properties are now preserved and will be merged below
184
185                                // Merge remaining resolved_map properties
186                                for (key, value) in resolved_map {
187                                    if key != "type" || !result.contains_key("type") {
188                                        result.insert(key, value);
189                                    }
190                                }
191
192                                // Finally, merge element override properties
193                                for (key, value) in map {
194                                    result.insert(key, value);
195                                }
196
197                                return Value::Object(result);
198                            } else {
199                                // Normal merge: element properties override referenced properties
200                                for (key, value) in map {
201                                    resolved_map.insert(key, value);
202                                }
203
204                                return Value::Object(resolved_map);
205                            }
206                        } else {
207                            return resolved;
208                        }
209                    }
210                }
211
212                // If it's a direct element (no $ref), we must preserve its evaluated state 
213                // from evaluated_schema, because evaluate_others may have updated hideLayout or condition.
214                if let Some(Value::Object(evaluated_map)) = self.evaluated_schema.pointer(original_pointer_path) {
215                    if let Some(hide_layout) = evaluated_map.get("hideLayout") {
216                        map.insert("hideLayout".to_string(), hide_layout.clone());
217                    }
218                    if let Some(condition) = evaluated_map.get("condition") {
219                        map.insert("condition".to_string(), condition.clone());
220                    }
221                }
222
223                Value::Object(map)
224            }
225            _ => element,
226        }
227    }
228
229    /// Propagate parent hidden/disabled conditions to children recursively
230    fn propagate_parent_conditions(&mut self, layout_elements_path: &str) {
231        let normalized_path = path_utils::normalize_to_json_pointer(layout_elements_path);
232
233        // Extract elements array to avoid borrow checker issues
234        let elements =
235            if let Some(Value::Array(arr)) = self.evaluated_schema.pointer_mut(&normalized_path) {
236                mem::take(arr)
237            } else {
238                return;
239            };
240
241        // Process elements
242        let mut updated_elements = Vec::with_capacity(elements.len());
243        for element in elements {
244            updated_elements.push(self.apply_parent_conditions(element, false, false));
245        }
246
247        // Write back  the updated elements
248        if let Some(target) = self.evaluated_schema.pointer_mut(&normalized_path) {
249            *target = Value::Array(updated_elements);
250        }
251    }
252
253    /// Recursively apply parent hidden/disabled conditions to an element and its children
254    fn apply_parent_conditions(
255        &self,
256        element: Value,
257        parent_hidden: bool,
258        parent_disabled: bool,
259    ) -> Value {
260        if let Value::Object(mut map) = element {
261            // Get current element's condition
262            let mut element_hidden = parent_hidden;
263            let mut element_disabled = parent_disabled;
264
265            // Check condition field (from $ref elements)
266            if let Some(Value::Object(condition)) = map.get("condition") {
267                if let Some(Value::Bool(hidden)) = condition.get("hidden") {
268                    element_hidden = element_hidden || *hidden;
269                }
270                if let Some(Value::Bool(disabled)) = condition.get("disabled") {
271                    element_disabled = element_disabled || *disabled;
272                }
273            }
274
275            // Check hideLayout field (from direct layout elements)
276            if let Some(Value::Object(hide_layout)) = map.get("hideLayout") {
277                if let Some(Value::Bool(all)) = hide_layout.get("all") {
278                    if *all {
279                        element_hidden = true;
280                    }
281                }
282            }
283
284            // Update condition to include parent state (for field elements)
285            if parent_hidden || parent_disabled {
286                // Update condition field if it exists or if this is a field element
287                if map.contains_key("condition")
288                    || map.contains_key("$ref")
289                    || map.contains_key("$fullpath")
290                {
291                    let mut condition = if let Some(Value::Object(c)) = map.get("condition") {
292                        c.clone()
293                    } else {
294                        serde_json::Map::new()
295                    };
296
297                    if parent_hidden {
298                        condition.insert("hidden".to_string(), Value::Bool(true));
299                        element_hidden = true;
300                    }
301                    if parent_disabled {
302                        condition.insert("disabled".to_string(), Value::Bool(true));
303                        element_disabled = true;
304                    }
305
306                    map.insert("condition".to_string(), Value::Object(condition));
307                }
308
309                // Update hideLayout for direct layout elements
310                if parent_hidden && (map.contains_key("hideLayout") || map.contains_key("type")) {
311                    let mut hide_layout = if let Some(Value::Object(h)) = map.get("hideLayout") {
312                        h.clone()
313                    } else {
314                        serde_json::Map::new()
315                    };
316
317                    // Set hideLayout.all to true when parent is hidden
318                    hide_layout.insert("all".to_string(), Value::Bool(true));
319                    map.insert("hideLayout".to_string(), Value::Object(hide_layout));
320                }
321            }
322
323            // Update $parentHide flag if element has it
324            if map.contains_key("$parentHide") {
325                map.insert("$parentHide".to_string(), Value::Bool(parent_hidden));
326            }
327
328            // Recursively process children if elements array exists
329            if let Some(Value::Array(elements)) = map.get("elements") {
330                let mut updated_children = Vec::with_capacity(elements.len());
331                for child in elements {
332                    updated_children.push(self.apply_parent_conditions(
333                        child.clone(),
334                        element_hidden,
335                        element_disabled,
336                    ));
337                }
338                map.insert("elements".to_string(), Value::Array(updated_children));
339            }
340
341            return Value::Object(map);
342        }
343
344        element
345    }
346
347    /// Sync hidden state from layout elements to their schema definitions
348    fn sync_layout_hidden_to_schema(&mut self, layout_paths: &[String]) {
349        let mut hidden_paths = Vec::new();
350
351        // Collect all schema paths that are effectively hidden in the layout
352        for layout_path in layout_paths {
353            let normalized_path = path_utils::normalize_to_json_pointer(layout_path);
354            if let Some(Value::Array(elements)) = self.evaluated_schema.pointer(&normalized_path) {
355                for element in elements {
356                    self.collect_hidden_paths_recursive(element, &mut hidden_paths);
357                }
358            }
359        }
360
361        // Apply hidden state to schema definitions
362        for schema_path_dot in hidden_paths {
363            let schema_pointer = path_utils::dot_notation_to_schema_pointer(&schema_path_dot);
364            let normalized_pointer = path_utils::normalize_to_json_pointer(&schema_pointer);
365
366            if let Some(schema_node) = self.evaluated_schema.pointer_mut(&normalized_pointer) {
367                if let Value::Object(map) = schema_node {
368                    // Update condition.hidden = true
369                    let mut condition = if let Some(Value::Object(c)) = map.get("condition") {
370                        c.clone()
371                    } else {
372                        serde_json::Map::new()
373                    };
374
375                    condition.insert("hidden".to_string(), Value::Bool(true));
376                    map.insert("condition".to_string(), Value::Object(condition));
377                }
378            } else {
379                // Try with /properties prefix if direct lookup fails
380                let alt_pointer = format!("/properties{}", normalized_pointer);
381                if let Some(schema_node) = self.evaluated_schema.pointer_mut(&alt_pointer) {
382                    if let Value::Object(map) = schema_node {
383                        let mut condition = if let Some(Value::Object(c)) = map.get("condition") {
384                            c.clone()
385                        } else {
386                            serde_json::Map::new()
387                        };
388
389                        condition.insert("hidden".to_string(), Value::Bool(true));
390                        map.insert("condition".to_string(), Value::Object(condition));
391                    }
392                }
393            }
394        }
395    }
396
397    fn collect_hidden_paths_recursive(&self, element: &Value, hidden_paths: &mut Vec<String>) {
398        if let Value::Object(map) = element {
399            // Check if this element is effectively hidden
400            let is_hidden = if let Some(Value::Object(condition)) = map.get("condition") {
401                condition
402                    .get("hidden")
403                    .and_then(|v| v.as_bool())
404                    .unwrap_or(false)
405            } else {
406                false
407            };
408
409            if is_hidden {
410                // If hidden and has $fullpath, verify it points to a schema definition
411                if let Some(Value::String(fullpath)) = map.get("fullpath") {
412                    hidden_paths.push(fullpath.clone());
413                } else if let Some(Value::String(fullpath)) = map.get("$fullpath") {
414                    hidden_paths.push(fullpath.clone());
415                }
416            }
417
418            // Recurse children
419            if let Some(Value::Array(elements)) = map.get("elements") {
420                for child in elements {
421                    self.collect_hidden_paths_recursive(child, hidden_paths);
422                }
423            }
424        }
425    }
426}