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 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 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 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 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 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 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 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 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 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 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 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 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}