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