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.insert("$path".to_string(), Value::String(last_segment.to_string()));
240                    } else {
241                        let base = Self::layout_path_to_structural_path(layout_path);
242                        let fullpath = if base.is_empty() {
243                            format!("{}", element_idx)
244                        } else {
245                            format!("{}.{}", base, element_idx)
246                        };
247                        let last_segment = fullpath.split('.').last().unwrap_or(&fullpath).to_string();
248                        overlay.insert("$fullpath".to_string(), Value::String(fullpath));
249                        overlay.insert("$path".to_string(), Value::String(last_segment));
250                    }
251                }
252
253                overlay.insert("$parentHide".to_string(), Value::Bool(parent_hidden));
254
255                // Parent condition cascade
256                let mut element_hidden = parent_hidden;
257                let mut element_condition_hidden = parent_condition_hidden;
258                let mut element_disabled = parent_disabled;
259
260                if let Some(Value::Bool(d)) = overlay.get("disabled") {
261                    element_disabled = element_disabled || *d;
262                }
263                if let Some(Value::Bool(r)) = overlay.get("readonly") {
264                    element_disabled = element_disabled || *r;
265                }
266                if let Some(Value::Bool(r)) = overlay.get("readOnly") {
267                    element_disabled = element_disabled || *r;
268                }
269
270                if let Some(Value::Object(cond)) = overlay.get("condition") {
271                    if let Some(Value::Bool(true)) = cond.get("hidden") {
272                        element_hidden = true;
273                        element_condition_hidden = true;
274                    }
275                    if let Some(Value::Bool(d)) = cond.get("disabled") {
276                        element_disabled = element_disabled || *d;
277                    }
278                    if let Some(Value::Bool(r)) = cond.get("readonly") {
279                        element_disabled = element_disabled || *r;
280                    }
281                    if let Some(Value::Bool(r)) = cond.get("readOnly") {
282                        element_disabled = element_disabled || *r;
283                    }
284                }
285
286                if let Some(Value::Object(hide)) = overlay.get("hideLayout") {
287                    if let Some(Value::Bool(true)) = hide.get("all") {
288                        element_hidden = true;
289                    }
290                }
291
292                if !ref_path.is_empty() {
293                    let pointer = path_utils::normalize_to_json_pointer(
294                        &path_utils::dot_notation_to_schema_pointer(&ref_path),
295                    )
296                    .trim_start_matches('#')
297                    .to_string();
298                    if element_hidden {
299                        state.layout_hidden_refs.insert(pointer.clone());
300                        if element_condition_hidden {
301                            state.layout_condition_hidden_refs.insert(pointer.clone());
302                        }
303                    } else {
304                        state.layout_visible_refs.insert(pointer.clone());
305                    }
306                    if element_disabled {
307                        state.layout_disabled_refs.insert(pointer);
308                    }
309                }
310
311                let show_condition_cascade =
312                    parent_hidden || parent_disabled || element_hidden || element_disabled;
313
314                if show_condition_cascade {
315                    let mut merged_cond = serde_json::Map::new();
316                    if let Some(Value::Object(existing)) = overlay.get("condition") {
317                        for (k, v) in existing.iter() {
318                            merged_cond.insert(k.clone(), v.clone());
319                        }
320                    }
321                    if parent_hidden || element_hidden {
322                        merged_cond.insert("hidden".to_string(), Value::Bool(true));
323                    }
324                    if parent_disabled || element_disabled {
325                        merged_cond.insert("disabled".to_string(), Value::Bool(true));
326                    }
327                    overlay.insert("condition".to_string(), Value::Object(merged_cond));
328
329                    if (parent_hidden || element_hidden)
330                        && (map.get("hideLayout").is_some() || map.get("type").is_some())
331                    {
332                        let mut hide_layout = if let Some(Value::Object(h)) = map.get("hideLayout") {
333                            h.clone()
334                        } else {
335                            serde_json::Map::new()
336                        };
337                        hide_layout.insert("all".to_string(), Value::Bool(true));
338                        overlay.insert("hideLayout".to_string(), Value::Object(hide_layout));
339                    }
340                }
341
342                // Recurse into nested elements (if any)
343                if let Some(Value::Array(children)) = map.get("elements") {
344                    let child_layout_path = format!(
345                        "{}/{}/elements",
346                        layout_path.trim_end_matches('/'),
347                        element_idx
348                    );
349                    self.resolve_and_collect_overlays(
350                        children,
351                        &child_layout_path,
352                        element_hidden,
353                        element_condition_hidden,
354                        element_disabled,
355                        state,
356                        ref_cache,
357                        all_entries,
358                    );
359                }
360            } else if !ref_path.is_empty() {
361                let pointer = path_utils::normalize_to_json_pointer(
362                    &path_utils::dot_notation_to_schema_pointer(&ref_path),
363                )
364                .trim_start_matches('#')
365                .to_string();
366                if parent_hidden {
367                    state.layout_hidden_refs.insert(pointer.clone());
368                    if parent_condition_hidden {
369                        state.layout_condition_hidden_refs.insert(pointer.clone());
370                    }
371                } else {
372                    state.layout_visible_refs.insert(pointer.clone());
373                }
374                if parent_disabled {
375                    state.layout_disabled_refs.insert(pointer);
376                }
377            }
378
379            all_entries.push(LayoutOverlayEntry {
380                layout_path: layout_path.to_string(),
381                element_idx,
382                schema_ref_path: ref_path,
383                overlay,
384            });
385        }
386    }
387
388    /// Resolve $ref in a single element. Returns (resolved_element, schema_ref_path).
389    /// Does NOT recurse into nested elements.
390    fn resolve_element_ref(
391        &self,
392        element: &Value,
393        ref_cache: &mut std::collections::HashMap<String, (String, String, String)>,
394    ) -> (Value, String) {
395        let Some(map) = element.as_object() else {
396            return (element.clone(), String::new());
397        };
398        let mut map = map.clone();
399        let has_ref = map.get("$ref").is_some();
400        let ref_path = if has_ref {
401            map.get("$ref")
402                .and_then(Value::as_str)
403                .unwrap_or("")
404                .to_string()
405        } else {
406            String::new()
407        };
408
409        if let Some(Value::String(ref_str)) = map.get("$ref").cloned() {
410            let (normalized_path, dotted_path, last_segment) =
411                if let Some(cached) = ref_cache.get(&ref_str) {
412                    cached.clone()
413                } else {
414                    let normalized_path = if ref_str.starts_with('#') || ref_str.starts_with('/') {
415                        path_utils::normalize_to_json_pointer(&ref_str).into_owned()
416                    } else {
417                        let schema_pointer = path_utils::dot_notation_to_schema_pointer(&ref_str);
418                        let schema_path =
419                            path_utils::normalize_to_json_pointer(&schema_pointer).into_owned();
420
421                        if self.evaluated_schema.pointer(&schema_path).is_some() {
422                            schema_path
423                        } else {
424                            format!("/properties/{}", ref_str.replace('.', "/properties/"))
425                        }
426                    };
427
428                    let dotted_path = path_utils::pointer_to_dot_notation(&normalized_path);
429                    let last_segment =
430                        dotted_path.split('.').last().unwrap_or(&dotted_path).to_string();
431                    let entry = (normalized_path, dotted_path, last_segment);
432                    ref_cache.insert(ref_str, entry.clone());
433                    entry
434                };
435
436            map.insert("$fullpath".to_string(), Value::String(dotted_path.clone()));
437            map.insert("$path".to_string(), Value::String(last_segment));
438            map.insert("$parentHide".to_string(), Value::Bool(false));
439
440            if let Some(referenced_value) = self.evaluated_schema.pointer(&normalized_path) {
441                if let Value::Object(ref_map) = referenced_value {
442                    map.remove("$ref");
443
444                    let mut result =
445                        if let Some(Value::Object(layout_obj)) = ref_map.get("$layout") {
446                            layout_obj.clone()
447                        } else {
448                            serde_json::Map::new()
449                        };
450
451                    for (key, value) in ref_map {
452                        if key == "$layout"
453                            || key == "properties"
454                            || key == "items"
455                            || key == "required"
456                            || key == "additionalProperties"
457                        {
458                            continue;
459                        }
460                        if key != "type" || !result.contains_key("type") {
461                            result.insert(key.clone(), value.clone());
462                        }
463                    }
464
465                    for (key, value) in map {
466                        result.insert(key, value);
467                    }
468                    return (Value::Object(result), dotted_path);
469                } else {
470                    return (referenced_value.clone(), dotted_path);
471                }
472            }
473        }
474
475        (Value::Object(map), ref_path)
476    }
477
478    // ── Private helpers ──────────────────────────────────────────────────────
479
480    /// Convert a layout elements pointer to its literal dotted structural path.
481    ///
482    /// ## Examples
483    ///
484    /// ```text
485    /// "#/illustration/$layout/elements" → "illustration.$layout.elements"
486    /// "#/properties/form/$layout/elements" → "properties.form.$layout.elements"
487    /// ```
488    fn layout_path_to_structural_path(layout_path: &str) -> String {
489        layout_path
490            .trim_start_matches('#')
491            .trim_start_matches('/')
492            .split('/')
493            .filter(|segment| !segment.is_empty())
494            .collect::<Vec<_>>()
495            .join(".")
496    }
497}