Skip to main content

json_eval_rs/jsoneval/
layout.rs

1use super::JSONEval;
2use crate::jsoneval::path_utils;
3use crate::jsoneval::types::{LayoutOverlayEntry, ResolvedLayoutResult};
4use crate::time_block;
5
6use indexmap::IndexMap;
7use serde_json::Value;
8
9use std::sync::Arc;
10
11#[derive(Default, Clone)]
12pub(crate) struct LayoutResolutionState {
13    pub(crate) resolved: bool,
14    pub(crate) cache: Option<Arc<Vec<LayoutOverlayEntry>>>,
15    pub(crate) layout_hidden_refs: indexmap::IndexSet<String>,
16    pub(crate) layout_visible_refs: indexmap::IndexSet<String>,
17    pub(crate) layout_condition_hidden_refs: indexmap::IndexSet<String>,
18    pub(crate) layout_disabled_refs: indexmap::IndexSet<String>,
19}
20
21impl JSONEval {
22    /// Ensure layout references and visibility state are resolved and cached.
23    pub(crate) fn ensure_layout_resolved(&self) {
24        if self.layout_paths.is_empty() {
25            return;
26        }
27
28        if let Ok(state) = self.layout_state.read() {
29            if state.resolved {
30                return;
31            }
32        }
33
34        let mut state = match self.layout_state.write() {
35            Ok(s) => s,
36            Err(poisoned) => poisoned.into_inner(),
37        };
38
39        if state.resolved {
40            return;
41        }
42
43        let entries = self.compute_layout_resolution(&mut state);
44        state.cache = Some(Arc::new(entries));
45        state.resolved = true;
46    }
47
48    /// Invalidate the layout resolution cache and state.
49    pub(crate) fn invalidate_layout_cache(&self) {
50        let mut state = match self.layout_state.write() {
51            Ok(s) => s,
52            Err(poisoned) => poisoned.into_inner(),
53        };
54        state.resolved = false;
55        state.cache = None;
56        state.layout_hidden_refs.clear();
57        state.layout_visible_refs.clear();
58        state.layout_condition_hidden_refs.clear();
59        state.layout_disabled_refs.clear();
60    }
61
62    /// Resolve layout references, return overlay entries.
63    ///
64    /// Unlike old version: does NOT mutate evaluated_schema.
65    /// Returns list of overlay entries describing delta properties per element.
66    ///
67    /// # Arguments
68    ///
69    /// * `evaluate` - If true, runs evaluation before resolving layout.
70    pub fn resolve_layout(&mut self, evaluate: bool) -> Result<ResolvedLayoutResult, String> {
71        if evaluate {
72            let data_str = serde_json::to_string(&self.data)
73                .map_err(|e| format!("Failed to serialize data: {}", e))?;
74            self.evaluate(&data_str, None, None, None)?;
75        }
76
77        self.ensure_layout_resolved();
78        let state = self.layout_state.read().unwrap();
79        Ok(state
80            .cache
81            .as_ref()
82            .map(|c| (**c).clone())
83            .unwrap_or_default())
84    }
85
86    fn compute_layout_resolution(&self, state: &mut LayoutResolutionState) -> ResolvedLayoutResult {
87        time_block!("  resolve_layout_internal()", {
88            let mut all_entries = ResolvedLayoutResult::new();
89
90            state.layout_hidden_refs.clear();
91            state.layout_visible_refs.clear();
92            state.layout_condition_hidden_refs.clear();
93            state.layout_disabled_refs.clear();
94
95            if self.root_layout_paths.is_empty() {
96                return all_entries;
97            }
98
99            let mut ref_cache = std::collections::HashMap::new();
100            time_block!("    resolve_layout_elements", {
101                for layout_path in self.root_layout_paths.iter() {
102                    let normalized_path = path_utils::normalize_to_json_pointer(layout_path);
103                    if let Some(Value::Array(elements)) = self.schema.pointer(&normalized_path) {
104                        self.resolve_and_collect_overlays(
105                            elements,
106                            layout_path,
107                            false,
108                            false,
109                            false,
110                            state,
111                            &mut ref_cache,
112                            &mut all_entries,
113                        );
114                    }
115                }
116            });
117
118            for visible_ref in &state.layout_visible_refs {
119                state.layout_hidden_refs.shift_remove(visible_ref);
120                state.layout_condition_hidden_refs.shift_remove(visible_ref);
121            }
122
123            all_entries
124        })
125    }
126
127    // ── Phase 1 helpers ─────────────────────────────────────────────
128
129    /// Return schema pointer owning `.../$layout/elements`; root layouts have no owner.
130    pub(crate) fn layout_owner_pointer(layout_path: &str) -> String {
131        let owner = layout_path
132            .trim_end_matches("/$layout/elements")
133            .trim_start_matches('#');
134        owner.to_string()
135    }
136
137    /// Compute root layout paths (layout paths not attached to another element).
138    pub(crate) fn compute_root_layout_paths(
139        layout_paths: &[String],
140        schema: &Value,
141    ) -> Vec<String> {
142        let attached_layout_refs = Self::collect_layout_ref_targets(schema);
143        layout_paths
144            .iter()
145            .filter(|path| {
146                let owner = Self::layout_owner_pointer(path);
147                owner.is_empty() || !attached_layout_refs.contains(&owner)
148            })
149            .cloned()
150            .collect()
151    }
152
153    /// Collect schema targets referenced from layout elements only. Formula `$ref`s are
154    /// intentionally ignored: they do not attach a field to a visual layout parent.
155    pub(crate) fn collect_layout_ref_targets(schema: &Value) -> indexmap::IndexSet<String> {
156        fn collect_elements(elements: &Value, refs: &mut indexmap::IndexSet<String>) {
157            let Some(elements) = elements.as_array() else {
158                return;
159            };
160            for element in elements {
161                let Some(map) = element.as_object() else {
162                    continue;
163                };
164                if let Some(reference) = map.get("$ref").and_then(Value::as_str) {
165                    let pointer = path_utils::normalize_to_json_pointer(
166                        &path_utils::dot_notation_to_schema_pointer(reference),
167                    )
168                    .trim_start_matches('#')
169                    .to_string();
170                    refs.insert(pointer);
171                }
172                if let Some(children) = map.get("elements") {
173                    collect_elements(children, refs);
174                }
175            }
176        }
177
178        fn walk(value: &Value, refs: &mut indexmap::IndexSet<String>) {
179            let Some(map) = value.as_object() else {
180                return;
181            };
182            if let Some(elements) = map
183                .get("$layout")
184                .and_then(Value::as_object)
185                .and_then(|layout| layout.get("elements"))
186            {
187                collect_elements(elements, refs);
188            }
189            for child in map.values() {
190                walk(child, refs);
191            }
192        }
193
194        let mut refs = indexmap::IndexSet::new();
195        walk(schema, &mut refs);
196        refs
197    }
198
199    /// Single-pass layout resolution and overlay collection.
200    ///
201    /// Resolves `$ref` references, cascades parent visibility and disabled state,
202    /// emits flat `LayoutOverlayEntry` objects, and records hidden/visible references.
203    fn resolve_and_collect_overlays(
204        &self,
205        elements: &[Value],
206        layout_path: &str,
207        parent_hidden: bool,
208        parent_condition_hidden: bool,
209        parent_disabled: bool,
210        state: &mut LayoutResolutionState,
211        ref_cache: &mut std::collections::HashMap<String, (String, String, String)>,
212        all_entries: &mut Vec<LayoutOverlayEntry>,
213    ) {
214        for (idx, element) in elements.iter().enumerate() {
215            let element_idx = idx;
216            let (resolved, ref_path) = self.resolve_element_ref(element, ref_cache);
217            let mut overlay = IndexMap::new();
218
219            if let Value::Object(map) = resolved {
220                const EXCLUDED: &[&str] = &[
221                    "$ref",
222                    "elements",
223                    "properties",
224                    "items",
225                    "required",
226                    "additionalProperties",
227                ];
228                for (key, value) in &map {
229                    if !EXCLUDED.contains(&key.as_str()) {
230                        overlay.insert(key.clone(), value.clone());
231                    }
232                }
233
234                // Inject $fullpath for ALL elements (ref and non-ref)
235                if !overlay.contains_key("$fullpath") {
236                    if !ref_path.is_empty() {
237                        let last_segment = ref_path.split('.').last().unwrap_or(&ref_path);
238                        overlay.insert("$fullpath".to_string(), Value::String(ref_path.clone()));
239                        overlay
240                            .insert("$path".to_string(), Value::String(last_segment.to_string()));
241                    } else {
242                        let base = Self::layout_path_to_structural_path(layout_path);
243                        let fullpath = if base.is_empty() {
244                            format!("{}", element_idx)
245                        } else {
246                            format!("{}.{}", base, element_idx)
247                        };
248                        let last_segment =
249                            fullpath.split('.').last().unwrap_or(&fullpath).to_string();
250                        overlay.insert("$fullpath".to_string(), Value::String(fullpath));
251                        overlay.insert("$path".to_string(), Value::String(last_segment));
252                    }
253                }
254
255                overlay.insert("$parentHide".to_string(), Value::Bool(parent_hidden));
256
257                // Parent condition cascade
258                let mut element_hidden = parent_hidden;
259                let mut element_condition_hidden = parent_condition_hidden;
260                let mut element_disabled = parent_disabled;
261
262                if let Some(Value::Bool(d)) = overlay.get("disabled") {
263                    element_disabled = element_disabled || *d;
264                }
265                if let Some(Value::Bool(r)) = overlay.get("readonly") {
266                    element_disabled = element_disabled || *r;
267                }
268                if let Some(Value::Bool(r)) = overlay.get("readOnly") {
269                    element_disabled = element_disabled || *r;
270                }
271
272                if let Some(Value::Object(cond)) = overlay.get("condition") {
273                    if let Some(Value::Bool(true)) = cond.get("hidden") {
274                        element_hidden = true;
275                        element_condition_hidden = true;
276                    }
277                    if let Some(Value::Bool(d)) = cond.get("disabled") {
278                        element_disabled = element_disabled || *d;
279                    }
280                    if let Some(Value::Bool(r)) = cond.get("readonly") {
281                        element_disabled = element_disabled || *r;
282                    }
283                    if let Some(Value::Bool(r)) = cond.get("readOnly") {
284                        element_disabled = element_disabled || *r;
285                    }
286                }
287
288                if let Some(Value::Object(hide)) = overlay.get("hideLayout") {
289                    if let Some(Value::Bool(true)) = hide.get("all") {
290                        element_hidden = true;
291                    }
292                }
293
294                if !ref_path.is_empty() {
295                    let pointer = path_utils::normalize_to_json_pointer(
296                        &path_utils::dot_notation_to_schema_pointer(&ref_path),
297                    )
298                    .trim_start_matches('#')
299                    .to_string();
300                    if element_hidden {
301                        state.layout_hidden_refs.insert(pointer.clone());
302                        if element_condition_hidden {
303                            state.layout_condition_hidden_refs.insert(pointer.clone());
304                        }
305                    } else {
306                        state.layout_visible_refs.insert(pointer.clone());
307                    }
308                    if element_disabled {
309                        state.layout_disabled_refs.insert(pointer);
310                    }
311                }
312
313                let show_condition_cascade =
314                    parent_hidden || parent_disabled || element_hidden || element_disabled;
315
316                if show_condition_cascade {
317                    let mut merged_cond = serde_json::Map::new();
318                    if let Some(Value::Object(existing)) = overlay.get("condition") {
319                        for (k, v) in existing.iter() {
320                            merged_cond.insert(k.clone(), v.clone());
321                        }
322                    }
323                    if parent_hidden || element_hidden {
324                        merged_cond.insert("hidden".to_string(), Value::Bool(true));
325                    }
326                    if parent_disabled || element_disabled {
327                        merged_cond.insert("disabled".to_string(), Value::Bool(true));
328                    }
329                    overlay.insert("condition".to_string(), Value::Object(merged_cond));
330
331                    if (parent_hidden || element_hidden)
332                        && (map.get("hideLayout").is_some() || map.get("type").is_some())
333                    {
334                        let mut hide_layout = if let Some(Value::Object(h)) = map.get("hideLayout")
335                        {
336                            h.clone()
337                        } else {
338                            serde_json::Map::new()
339                        };
340                        hide_layout.insert("all".to_string(), Value::Bool(true));
341                        overlay.insert("hideLayout".to_string(), Value::Object(hide_layout));
342                    }
343                }
344
345                // Recurse into nested elements (if any)
346                if let Some(Value::Array(children)) = map.get("elements") {
347                    let child_layout_path = format!(
348                        "{}/{}/elements",
349                        layout_path.trim_end_matches('/'),
350                        element_idx
351                    );
352                    self.resolve_and_collect_overlays(
353                        children,
354                        &child_layout_path,
355                        element_hidden,
356                        element_condition_hidden,
357                        element_disabled,
358                        state,
359                        ref_cache,
360                        all_entries,
361                    );
362                }
363            } else if !ref_path.is_empty() {
364                let pointer = path_utils::normalize_to_json_pointer(
365                    &path_utils::dot_notation_to_schema_pointer(&ref_path),
366                )
367                .trim_start_matches('#')
368                .to_string();
369                if parent_hidden {
370                    state.layout_hidden_refs.insert(pointer.clone());
371                    if parent_condition_hidden {
372                        state.layout_condition_hidden_refs.insert(pointer.clone());
373                    }
374                } else {
375                    state.layout_visible_refs.insert(pointer.clone());
376                }
377                if parent_disabled {
378                    state.layout_disabled_refs.insert(pointer);
379                }
380            }
381
382            all_entries.push(LayoutOverlayEntry {
383                layout_path: layout_path.to_string(),
384                element_idx,
385                schema_ref_path: ref_path,
386                overlay,
387            });
388        }
389    }
390
391    /// Resolve $ref in a single element. Returns (resolved_element, schema_ref_path).
392    /// Does NOT recurse into nested elements.
393    fn resolve_element_ref(
394        &self,
395        element: &Value,
396        ref_cache: &mut std::collections::HashMap<String, (String, String, String)>,
397    ) -> (Value, String) {
398        let Some(map) = element.as_object() else {
399            return (element.clone(), String::new());
400        };
401        let mut map = map.clone();
402        let has_ref = map.get("$ref").is_some();
403        let ref_path = if has_ref {
404            map.get("$ref")
405                .and_then(Value::as_str)
406                .unwrap_or("")
407                .to_string()
408        } else {
409            String::new()
410        };
411
412        if let Some(Value::String(ref_str)) = map.get("$ref").cloned() {
413            let (normalized_path, dotted_path, last_segment) =
414                if let Some(cached) = ref_cache.get(&ref_str) {
415                    cached.clone()
416                } else {
417                    let normalized_path = if ref_str.starts_with('#') || ref_str.starts_with('/') {
418                        path_utils::normalize_to_json_pointer(&ref_str).into_owned()
419                    } else {
420                        let schema_pointer = path_utils::dot_notation_to_schema_pointer(&ref_str);
421                        let schema_path =
422                            path_utils::normalize_to_json_pointer(&schema_pointer).into_owned();
423
424                        if self.evaluated_schema.pointer(&schema_path).is_some() {
425                            schema_path
426                        } else {
427                            format!("/properties/{}", ref_str.replace('.', "/properties/"))
428                        }
429                    };
430
431                    let dotted_path = path_utils::pointer_to_dot_notation(&normalized_path);
432                    let last_segment = dotted_path
433                        .split('.')
434                        .last()
435                        .unwrap_or(&dotted_path)
436                        .to_string();
437                    let entry = (normalized_path, dotted_path, last_segment);
438                    ref_cache.insert(ref_str, entry.clone());
439                    entry
440                };
441
442            map.insert("$fullpath".to_string(), Value::String(dotted_path.clone()));
443            map.insert("$path".to_string(), Value::String(last_segment));
444            map.insert("$parentHide".to_string(), Value::Bool(false));
445
446            if let Some(referenced_value) = self.evaluated_schema.pointer(&normalized_path) {
447                if let Value::Object(ref_map) = referenced_value {
448                    map.remove("$ref");
449
450                    let mut result = if let Some(Value::Object(layout_obj)) = ref_map.get("$layout")
451                    {
452                        layout_obj.clone()
453                    } else {
454                        serde_json::Map::new()
455                    };
456
457                    for (key, value) in ref_map {
458                        if key == "$layout"
459                            || key == "properties"
460                            || key == "items"
461                            || key == "required"
462                            || key == "additionalProperties"
463                        {
464                            continue;
465                        }
466                        if key != "type" || !result.contains_key("type") {
467                            result.insert(key.clone(), value.clone());
468                        }
469                    }
470
471                    for (key, value) in map {
472                        result.insert(key, value);
473                    }
474                    return (Value::Object(result), dotted_path);
475                } else {
476                    return (referenced_value.clone(), dotted_path);
477                }
478            }
479        }
480
481        (Value::Object(map), ref_path)
482    }
483
484    // ── Private helpers ──────────────────────────────────────────────────────
485
486    /// Convert a layout elements pointer to its literal dotted structural path.
487    ///
488    /// ## Examples
489    ///
490    /// ```text
491    /// "#/form/$layout/elements" → "form.$layout.elements"
492    /// "#/properties/form/$layout/elements" → "properties.form.$layout.elements"
493    /// ```
494    fn layout_path_to_structural_path(layout_path: &str) -> String {
495        layout_path
496            .trim_start_matches('#')
497            .trim_start_matches('/')
498            .split('/')
499            .filter(|segment| !segment.is_empty())
500            .collect::<Vec<_>>()
501            .join(".")
502    }
503}