Skip to main content

json_eval_rs/jsoneval/
getters.rs

1use super::JSONEval;
2use crate::jsoneval::path_utils;
3use crate::jsoneval::types::{ResolvedLayoutResult, ReturnFormat};
4use crate::time_block;
5use crate::utils::clean_float_noise_scalar;
6use serde_json::Value;
7
8impl JSONEval {
9    /// Check if a field is effectively hidden by checking its condition and all parents
10    /// Also checks for $layout.hideLayout.all on parents
11    /// Check if a field is effectively hidden by checking its condition and all parents
12    /// Also checks for $layout.hideLayout.all on parents
13    pub(crate) fn is_effective_hidden(&self, schema_pointer: &str) -> bool {
14        self.ensure_layout_resolved();
15        let schema_pointer = schema_pointer.trim_start_matches('#');
16        if let Ok(state) = self.layout_state.read() {
17            if state.layout_hidden_refs.iter().any(|hidden_ref| {
18                schema_pointer == hidden_ref
19                    || schema_pointer
20                        .strip_prefix(hidden_ref)
21                        .is_some_and(|suffix| {
22                            suffix.starts_with("/properties/") || suffix.starts_with("/items/")
23                        })
24            }) {
25                return true;
26            }
27        }
28
29        self.is_schema_effective_hidden(schema_pointer)
30    }
31
32    /// Check if a field is effectively hidden using pre-acquired layout hidden refs and a local cache.
33    pub(crate) fn is_effective_hidden_with_cache(
34        &self,
35        schema_pointer: &str,
36        layout_hidden_refs: &indexmap::IndexSet<String>,
37        cache: &mut std::collections::HashMap<String, bool>,
38    ) -> bool {
39        let schema_pointer = schema_pointer.trim_start_matches('#');
40        if let Some(&is_hidden) = cache.get(schema_pointer) {
41            return is_hidden;
42        }
43
44        if layout_hidden_refs.iter().any(|hidden_ref| {
45            schema_pointer == hidden_ref
46                || schema_pointer
47                    .strip_prefix(hidden_ref)
48                    .is_some_and(|suffix| {
49                        suffix.starts_with("/properties/") || suffix.starts_with("/items/")
50                    })
51        }) {
52            cache.insert(schema_pointer.to_string(), true);
53            return true;
54        }
55
56        let is_hidden = self.is_schema_effective_hidden_cached(schema_pointer, cache);
57        cache.insert(schema_pointer.to_string(), is_hidden);
58        is_hidden
59    }
60
61    /// Check if a field is effectively hidden in the schema hierarchy with ancestor caching.
62    pub(crate) fn is_schema_effective_hidden_cached(
63        &self,
64        schema_pointer: &str,
65        cache: &mut std::collections::HashMap<String, bool>,
66    ) -> bool {
67        let schema_pointer = schema_pointer.trim_start_matches('#');
68        let mut end = schema_pointer.len();
69
70        loop {
71            let current_path = &schema_pointer[..end];
72
73            if let Some(&ancestor_hidden) = cache.get(current_path) {
74                if ancestor_hidden {
75                    return true;
76                }
77                break;
78            }
79
80            if let Some(schema_node) = self.evaluated_schema.pointer(current_path) {
81                if let Value::Object(map) = schema_node {
82                    if let Some(Value::Object(condition)) = map.get("condition") {
83                        if let Some(Value::Bool(true)) = condition.get("hidden") {
84                            cache.insert(current_path.to_string(), true);
85                            return true;
86                        }
87                    }
88
89                    if let Some(Value::Object(layout)) = map.get("$layout") {
90                        if let Some(Value::Object(hide_layout)) = layout.get("hideLayout") {
91                            if let Some(Value::Bool(true)) = hide_layout.get("all") {
92                                cache.insert(current_path.to_string(), true);
93                                return true;
94                            }
95                        }
96                    }
97                }
98            }
99
100            if end == 0 {
101                break;
102            }
103
104            match schema_pointer[..end].rfind('/') {
105                Some(0) | None => {
106                    end = 0;
107                }
108                Some(last_slash) => {
109                    end = last_slash;
110                    let parent = &schema_pointer[..end];
111                    if parent.ends_with("/properties") {
112                        end -= "/properties".len();
113                    } else if parent.ends_with("/items") {
114                        end -= "/items".len();
115                    }
116                }
117            }
118        }
119
120        false
121    }
122
123    /// Check if a field is effectively hidden in the schema hierarchy (condition.hidden or $layout.hideLayout.all)
124    /// without requiring layout resolution.
125    pub(crate) fn is_schema_effective_hidden(&self, schema_pointer: &str) -> bool {
126        let schema_pointer = schema_pointer.trim_start_matches('#');
127        let mut end = schema_pointer.len();
128
129        loop {
130            let current_path = &schema_pointer[..end];
131
132            if let Some(schema_node) = self.evaluated_schema.pointer(current_path) {
133                if let Value::Object(map) = schema_node {
134                    if let Some(Value::Object(condition)) = map.get("condition") {
135                        if let Some(Value::Bool(true)) = condition.get("hidden") {
136                            return true;
137                        }
138                    }
139
140                    if let Some(Value::Object(layout)) = map.get("$layout") {
141                        if let Some(Value::Object(hide_layout)) = layout.get("hideLayout") {
142                            if let Some(Value::Bool(true)) = hide_layout.get("all") {
143                                return true;
144                            }
145                        }
146                    }
147                }
148            }
149
150            if end == 0 {
151                break;
152            }
153
154            // Move to parent: find last '/' and strip /properties or /items suffixes
155            match schema_pointer[..end].rfind('/') {
156                Some(0) | None => {
157                    end = 0;
158                }
159                Some(last_slash) => {
160                    end = last_slash;
161                    let parent = &schema_pointer[..end];
162                    if parent.ends_with("/properties") {
163                        end -= "/properties".len();
164                    } else if parent.ends_with("/items") {
165                        end -= "/items".len();
166                    }
167                }
168            }
169        }
170
171        false
172    }
173
174    /// Check if a field is effectively readonly/disabled using pre-acquired layout disabled refs and a local cache.
175    pub(crate) fn is_effective_readonly_with_cache(
176        &self,
177        schema_pointer: &str,
178        layout_disabled_refs: &indexmap::IndexSet<String>,
179        cache: &mut std::collections::HashMap<String, bool>,
180    ) -> bool {
181        let schema_pointer = schema_pointer.trim_start_matches('#');
182        if let Some(&is_readonly) = cache.get(schema_pointer) {
183            return is_readonly;
184        }
185
186        if layout_disabled_refs.iter().any(|disabled_ref| {
187            schema_pointer == disabled_ref
188                || schema_pointer
189                    .strip_prefix(disabled_ref)
190                    .is_some_and(|suffix| {
191                        suffix.starts_with("/properties/") || suffix.starts_with("/items/")
192                    })
193        }) {
194            cache.insert(schema_pointer.to_string(), true);
195            return true;
196        }
197
198        let is_readonly = self.is_schema_effective_readonly_cached(schema_pointer, cache);
199        cache.insert(schema_pointer.to_string(), is_readonly);
200        is_readonly
201    }
202
203    /// Check if a field is effectively readonly/disabled in the schema hierarchy with ancestor caching.
204    pub(crate) fn is_schema_effective_readonly_cached(
205        &self,
206        schema_pointer: &str,
207        cache: &mut std::collections::HashMap<String, bool>,
208    ) -> bool {
209        let schema_pointer = schema_pointer.trim_start_matches('#');
210        let mut end = schema_pointer.len();
211
212        loop {
213            let current_path = &schema_pointer[..end];
214
215            if let Some(&ancestor_readonly) = cache.get(current_path) {
216                if ancestor_readonly {
217                    return true;
218                }
219                break;
220            }
221
222            if let Some(schema_node) = self.evaluated_schema.pointer(current_path) {
223                if let Value::Object(map) = schema_node {
224                    if map.get("disabled").and_then(Value::as_bool) == Some(true)
225                        || map.get("readonly").and_then(Value::as_bool) == Some(true)
226                        || map.get("readOnly").and_then(Value::as_bool) == Some(true)
227                        || map.get("$readonly").and_then(Value::as_bool) == Some(true)
228                    {
229                        cache.insert(current_path.to_string(), true);
230                        return true;
231                    }
232
233                    if let Some(Value::Object(condition)) = map.get("condition") {
234                        if condition.get("disabled").and_then(Value::as_bool) == Some(true)
235                            || condition.get("readonly").and_then(Value::as_bool) == Some(true)
236                            || condition.get("readOnly").and_then(Value::as_bool) == Some(true)
237                        {
238                            cache.insert(current_path.to_string(), true);
239                            return true;
240                        }
241                    }
242                }
243            }
244
245            if end == 0 {
246                break;
247            }
248
249            match schema_pointer[..end].rfind('/') {
250                Some(0) | None => {
251                    end = 0;
252                }
253                Some(last_slash) => {
254                    end = last_slash;
255                    let parent = &schema_pointer[..end];
256                    if parent.ends_with("/properties") {
257                        end -= "/properties".len();
258                    } else if parent.ends_with("/items") {
259                        end -= "/items".len();
260                    }
261                }
262            }
263        }
264
265        false
266    }
267
268    /// Return whether a schema field appears in any `$layout` element.
269    /// Field references are collected once while parsing schema, so this is O(1).
270    fn is_mapped_in_any_layout(&self, schema_path: &str) -> bool {
271        self.layout_field_refs
272            .contains(schema_path.trim_start_matches('#'))
273    }
274
275    /// Prune hidden values from data object recursively
276    fn prune_hidden_values(&self, data: &mut Value, current_path: &str) {
277        if let Value::Object(map) = data {
278            // Collect keys to remove to avoid borrow checker issues
279            let mut keys_to_remove = Vec::new();
280
281            for (key, value) in map.iter_mut() {
282                // Skip special keys
283                if key == "$params" || key == "$context" {
284                    continue;
285                }
286
287                // Construct schema path for this key
288                // For root fields: /properties/key (or /key if schema root directly holds keys as in subforms)
289                // For nested fields: current_path/properties/key
290                let schema_path = if current_path.is_empty() {
291                    if self
292                        .evaluated_schema
293                        .pointer(&format!("/properties/{}", key))
294                        .is_some()
295                    {
296                        format!("/properties/{}", key)
297                    } else if self
298                        .evaluated_schema
299                        .pointer(&format!("/{}", key))
300                        .is_some()
301                    {
302                        format!("/{}", key)
303                    } else {
304                        format!("/properties/{}", key)
305                    }
306                } else {
307                    format!("{}/properties/{}", current_path, key)
308                };
309
310                if self.is_effective_hidden(&schema_path) {
311                    keys_to_remove.push(key.clone());
312                } else {
313                    // Recurse if object
314                    if value.is_object() {
315                        self.prune_hidden_values(value, &schema_path);
316                    }
317                }
318            }
319
320            // Remove hidden keys
321            for key in keys_to_remove {
322                map.remove(&key);
323            }
324        }
325    }
326
327    /// Replace any `{"$static_array": "/$table/..."}` and `{"$static_array": "/$params/..."}` markers in `schema_output`
328    /// with the actual evaluated array data from `eval_data`.
329    ///
330    /// By iterating only over tracked `static_arrays`, we replace markers in O(markers) time
331    /// instead of requiring an expensive O(schema_nodes) recursive tree walk.
332    fn resolve_static_markers_in_value(&self, schema_output: &mut Value) {
333        for (static_key, array_arc) in self.static_arrays.iter() {
334            // Determine the schema pointer path where this marker was placed
335            let schema_path = if static_key.starts_with("/$table") {
336                &static_key["/$table".len()..] // e.g. /properties/product_benefit/...
337            } else {
338                static_key.as_str() // e.g. /$params/references/...
339            };
340
341            // Only attempt replacement if the exact path exists in the cloned schema output
342            if let Some(target_val) = schema_output.pointer_mut(schema_path) {
343                // The actual evaluated array is seamlessly stored right in the map's value
344                *target_val = (**array_arc).clone();
345            }
346        }
347    }
348
349    /// Get the evaluated schema (compact — $ref intact, no layout expansion).
350    ///
351    /// # Returns
352    ///
353    /// The evaluated schema as a JSON value, with all `$static_array` markers resolved
354    /// to their actual evaluated data.
355    pub fn get_evaluated_schema(&mut self) -> Value {
356        time_block!("get_evaluated_schema()", {
357            let mut schema = self.evaluated_schema.clone();
358            self.resolve_static_markers_in_value(&mut schema);
359            schema
360        })
361    }
362
363    /// Get layout overlay entries — the delta properties per layout element.
364    /// Consumer merges these into compact schema to get fully resolved layout.
365    pub fn get_resolved_layout(&mut self) -> ResolvedLayoutResult {
366        time_block!("get_resolved_layout()", {
367            self.ensure_layout_resolved();
368            self.layout_state
369                .read()
370                .unwrap()
371                .cache
372                .as_ref()
373                .map(|c| (**c).clone())
374                .unwrap_or_default()
375        })
376    }
377
378    /// Get evaluated schema with layout overlays already applied.
379    /// Convenience: returns compact schema + overlays merged.
380    ///
381    /// Two-pass approach to handle nested elements:
382    /// 1. First pass: resolve $ref and apply overlay for entries whose target
383    ///    path exists in the compact schema (top-level elements).
384    /// 2. Second pass: apply overlay-only for entries whose path appears
385    ///    after parent $ref resolution (nested elements).
386    pub fn get_evaluated_schema_resolved(&mut self) -> Value {
387        time_block!("get_evaluated_schema_resolved()", {
388            let mut schema = self.get_evaluated_schema_without_params();
389            let overlays = self.get_resolved_layout();
390
391            struct ResolveEntry {
392                layout_path: String,
393                element_idx: usize,
394                overlay: indexmap::IndexMap<String, Value>,
395            }
396
397            let mut entries: Vec<ResolveEntry> = overlays
398                .iter()
399                .map(|entry| {
400                    let layout_path =
401                        path_utils::normalize_to_json_pointer(&entry.layout_path).into_owned();
402                    ResolveEntry {
403                        layout_path,
404                        element_idx: entry.element_idx,
405                        overlay: entry.overlay.clone(),
406                    }
407                })
408                .collect();
409            drop(overlays);
410
411            // Sort entries shallow-first so parent elements are expanded before their children.
412            // Child entries (e.g. layout_path = ".../elements/1/elements") depend on the parent
413            // ("…/elements") being resolved first so the nested `elements` array exists in `schema`.
414            entries.sort_by(|a, b| {
415                let depth_a = a.layout_path.matches('/').count();
416                let depth_b = b.layout_path.matches('/').count();
417                depth_a
418                    .cmp(&depth_b)
419                    .then_with(|| a.element_idx.cmp(&b.element_idx))
420            });
421
422            // ── Phase 2 (mutable): resolve $ref + apply overlays (parent-first order) ──
423            // Entries are sorted shallowest layout_path first, so parent elements are
424            // expanded before any child entries that path through them.
425            for entry in entries {
426                // Resolve $ref from the current (already partially mutated) schema so that
427                // parent expansions are visible when we process child entries.
428                let resolved_value: Option<Value> = (|| -> Option<Value> {
429                    let arr = schema.pointer(&entry.layout_path)?.as_array()?;
430                    let element = arr.get(entry.element_idx)?;
431                    let ref_str = element.get("$ref")?.as_str()?;
432
433                    let ref_pointer = if ref_str.starts_with('#') || ref_str.starts_with('/') {
434                        path_utils::normalize_to_json_pointer(ref_str).into_owned()
435                    } else {
436                        let schema_pointer = path_utils::dot_notation_to_schema_pointer(ref_str);
437                        let normalized =
438                            path_utils::normalize_to_json_pointer(&schema_pointer).into_owned();
439                        if schema.pointer(&normalized).is_some() {
440                            normalized
441                        } else {
442                            format!("/properties/{}", ref_str.replace('.', "/properties/"))
443                        }
444                    };
445
446                    let mut resolved = schema.pointer(&ref_pointer)?.clone();
447
448                    // Flatten $layout into top level
449                    if let Value::Object(ref mut resolved_map) = resolved {
450                        if let Some(Value::Object(layout_obj)) = resolved_map.remove("$layout") {
451                            let mut result = layout_obj;
452                            for (key, value) in resolved_map.clone().into_iter() {
453                                if key != "type" || !result.contains_key("type") {
454                                    result.insert(key, value);
455                                }
456                            }
457                            resolved = Value::Object(result);
458                        }
459                    }
460
461                    Some(resolved)
462                })();
463
464                if let Some(Value::Array(arr)) = schema.pointer_mut(&entry.layout_path) {
465                    if entry.element_idx < arr.len() {
466                        let element = &mut arr[entry.element_idx];
467
468                        // Apply $ref resolution
469                        if let Some(resolved) = resolved_value {
470                            if let Value::Object(mut resolved_map) = resolved {
471                                if let Value::Object(mut map) = element.take() {
472                                    map.remove("$ref");
473                                    for (key, value) in map {
474                                        resolved_map.insert(key, value);
475                                    }
476                                }
477                                *element = Value::Object(resolved_map);
478                            } else {
479                                *element = resolved;
480                            }
481                        }
482
483                        // Apply overlay on top
484                        if let Value::Object(ref mut map) = element {
485                            for (k, v) in &entry.overlay {
486                                map.insert(k.clone(), v.clone());
487                            }
488                        }
489                    }
490                }
491            }
492
493            Self::stamp_property_metadata(&mut schema);
494            schema
495        })
496    }
497
498    /// Stamp every schema property with raw pointer-style dotted metadata.
499    fn stamp_property_metadata(schema: &mut Value) {
500        fn walk(value: &mut Value, path: &str, parent_hidden: bool) {
501            let Some(map) = value.as_object_mut() else {
502                return;
503            };
504
505            let hidden = parent_hidden
506                || map
507                    .get("condition")
508                    .and_then(Value::as_object)
509                    .and_then(|condition| condition.get("hidden"))
510                    .is_some_and(|hidden| hidden == &Value::Bool(true));
511
512            if let Some(Value::Object(properties)) = map.get_mut("properties") {
513                for (name, property) in properties {
514                    let property_path = if path.is_empty() {
515                        format!("properties.{}", name)
516                    } else {
517                        format!("{}.properties.{}", path, name)
518                    };
519                    if let Value::Object(property_map) = property {
520                        property_map.insert(
521                            "$fullpath".to_string(),
522                            Value::String(property_path.clone()),
523                        );
524                        property_map.insert("$path".to_string(), Value::String(name.clone()));
525                        property_map.insert("$parentHide".to_string(), Value::Bool(hidden));
526                    }
527                    walk(property, &property_path, hidden);
528                }
529            }
530
531            for (name, child) in map {
532                if name != "properties" && !name.starts_with('$') && child.is_object() {
533                    let child_path = if path.is_empty() {
534                        name.clone()
535                    } else {
536                        format!("{}.{}", path, name)
537                    };
538                    walk(child, &child_path, hidden);
539                }
540            }
541        }
542
543        walk(schema, "", false);
544    }
545
546    /// Resolve `$static_array` markers within the subtree rooted at `schema_prefix`.
547    ///
548    /// Clones only the node at `schema_prefix` from `evaluated_schema`, then iterates
549    /// the tracked `static_arrays` list filtering to entries whose schema path is at or
550    /// under `schema_prefix`. Only those markers are replaced inside the cloned subtree;
551    /// unrelated entries are skipped entirely.
552    ///
553    /// # Examples
554    /// - `schema_prefix = "/$params/references"` → resolves only arrays nested under that key
555    /// - `schema_prefix = "/properties/foo/value"` → resolves a single marker if the field itself is one
556    pub(crate) fn resolve_static_markers_at_path(&self, schema_prefix: &str) -> Option<Value> {
557        // Resolve indexed static-array paths directly.
558        for (static_key, array_arc) in self.static_arrays.iter() {
559            let schema_path: &str = if static_key.starts_with("/$table") {
560                &static_key["/$table".len()..]
561            } else {
562                static_key.as_str()
563            };
564
565            if let Some(relative) = schema_prefix
566                .strip_prefix(schema_path)
567                .and_then(|relative| relative.strip_prefix('/'))
568            {
569                return array_arc.pointer(&format!("/{}", relative)).cloned();
570            }
571        }
572
573        let mut subtree = self.evaluated_schema.pointer(schema_prefix)?.clone();
574
575        // Pre-build "prefix/" once for the starts_with check in the loop
576        let prefix_slash = format!("{}/", schema_prefix);
577
578        for (static_key, array_arc) in self.static_arrays.iter() {
579            // Derive the absolute schema path the same way resolve_static_markers_in_value does
580            let schema_path: &str = if static_key.starts_with("/$table") {
581                &static_key["/$table".len()..]
582            } else {
583                static_key.as_str()
584            };
585
586            // Compute the path relative to the subtree root
587            let relative: &str = if schema_path == schema_prefix {
588                // The subtree root itself is the marker — replace the whole subtree
589                ""
590            } else if schema_path.starts_with(&prefix_slash) {
591                // Strip the prefix: remainder is the sub-path within the cloned subtree
592                &schema_path[schema_prefix.len()..]
593            } else {
594                continue; // Not under the requested path — skip
595            };
596
597            if relative.is_empty() {
598                subtree = (**array_arc).clone();
599            } else if let Some(target) = subtree.pointer_mut(relative) {
600                *target = (**array_arc).clone();
601            }
602        }
603
604        Some(subtree)
605    }
606
607    /// Get specific schema value by path, resolving any `$static_array` markers at or
608    /// under that path.
609    pub fn get_schema_value_by_path(&self, path: &str) -> Option<Value> {
610        let pointer_path = path_utils::dot_notation_to_schema_pointer(path);
611        self.resolve_static_markers_at_path(pointer_path.trim_start_matches('#'))
612    }
613
614    /// Get all schema values (data view).
615    ///
616    /// Builds a consumer view from current data and evaluated values without mutating
617    /// evaluator state. Indexed subforms carry active-item wrappers at their root;
618    /// persisting this view would append that wrapper into later form evaluations.
619    pub fn get_schema_value(&mut self, include_subforms: Option<bool>) -> Value {
620        self.ensure_layout_resolved();
621        // Start with current authoritative data from eval_data
622        let mut current_data = self.eval_data.data().clone();
623
624        // Ensure it's an object
625        if !current_data.is_object() {
626            current_data = Value::Object(serde_json::Map::new());
627        }
628
629        // Strip $params and $context from data
630        if let Some(obj) = current_data.as_object_mut() {
631            obj.remove("$params");
632            obj.remove("$context");
633        }
634
635        // Prune hidden values from current_data (to remove user input in hidden fields)
636        self.prune_hidden_values(&mut current_data, "");
637
638        // Override data with values from value evaluations
639        // We use value_evaluations which stores the paths of fields with .value
640        for eval_key in self.value_evaluations.iter() {
641            let clean_key = eval_key.strip_prefix('#').unwrap_or(eval_key);
642
643            // Exclude rules.*.value, options.*.value, and $params
644            if clean_key.starts_with("/$params")
645                || (clean_key.ends_with("/value")
646                    && (clean_key.contains("/rules/") || clean_key.contains("/options/")))
647            {
648                continue;
649            }
650
651            let path = clean_key.replace("/properties", "").replace("/value", "");
652
653            // Check if field is effectively hidden
654            // Schema path is clean_key without /value
655            let schema_path = clean_key.strip_suffix("/value").unwrap_or(&clean_key);
656            if self.is_effective_hidden(schema_path) {
657                continue;
658            }
659
660            // Resolve static markers at this specific pointer (handles markers at or under this path)
661            let value = match self.resolve_static_markers_at_path(clean_key) {
662                Some(v) => v,
663                None => continue,
664            };
665
666            // Parse the path and create nested structure as needed
667            let path_parts: Vec<&str> = path.split('/').filter(|s| !s.is_empty()).collect();
668
669            if path_parts.is_empty() {
670                continue;
671            }
672
673            // Navigate/create nested structure
674            let mut current = &mut current_data;
675            for (i, part) in path_parts.iter().enumerate() {
676                let is_last = i == path_parts.len() - 1;
677
678                if is_last {
679                    // Only disabled calculated fields are library-owned. Editable fields
680                    // preserve non-null caller input even when their schema has `$evaluation`.
681                    let schema_value = self.schema.pointer(clean_key);
682                    let computed_value = schema_value
683                        .and_then(Value::as_object)
684                        .is_some_and(|value| value.contains_key("$evaluation"));
685                    let disabled = self
686                        .evaluated_schema
687                        .pointer(schema_path)
688                        .and_then(Value::as_object)
689                        .and_then(|field| field.get("condition"))
690                        .and_then(Value::as_object)
691                        .and_then(|condition| condition.get("disabled"))
692                        .is_some_and(|disabled| disabled == &Value::Bool(true));
693                    let computed_disabled =
694                        computed_value && (disabled || !self.is_mapped_in_any_layout(schema_path));
695                    if let Some(obj) = current.as_object_mut() {
696                        let should_update = computed_disabled
697                            || match obj.get(*part) {
698                                Some(v) => v.is_null(),
699                                None => true,
700                            };
701                        if should_update {
702                            obj.insert(
703                                (*part).to_string(),
704                                crate::utils::clean_float_noise(value.clone()),
705                            );
706                        }
707                    }
708                } else {
709                    // Ensure current is an object, then navigate/create intermediate objects
710                    if let Some(obj) = current.as_object_mut() {
711                        if !obj.contains_key(*part) {
712                            obj.insert((*part).to_string(), Value::Object(serde_json::Map::new()));
713                        }
714
715                        current = obj.get_mut(*part).unwrap();
716                    } else {
717                        // Skip this path if current is not an object and can't be made into one
718                        break;
719                    }
720                }
721            }
722        }
723
724        if include_subforms.unwrap_or(false) {
725            let subform_keys: Vec<String> = self.subforms.keys().cloned().collect();
726
727            // Sync parent params and static arrays to subforms once before the item loop
728            for subform_path in &subform_keys {
729                if let Some(subform) = self.subforms.get_mut(subform_path) {
730                    if let Some(params) = self.evaluated_schema.pointer("/$params") {
731                        if let Some(sub_params) = subform.evaluated_schema.pointer_mut("/$params") {
732                            *sub_params = params.clone();
733                        }
734                    }
735                    subform.static_arrays = std::sync::Arc::clone(&self.static_arrays);
736                    subform
737                        .engine
738                        .set_static_arrays(std::sync::Arc::clone(&subform.static_arrays));
739                }
740            }
741
742            for subform_path in subform_keys {
743                let data_ptr = path_utils::schema_path_to_data_pointer(&subform_path);
744
745                let schema_pointer = if subform_path.starts_with("#/") {
746                    &subform_path[1..]
747                } else if subform_path.starts_with('#') {
748                    &subform_path[1..]
749                } else {
750                    &subform_path
751                };
752
753                let original_field_key = subform_path
754                    .split('/')
755                    .filter(|seg| !seg.is_empty() && *seg != "properties")
756                    .last()
757                    .unwrap_or(&subform_path)
758                    .to_string();
759
760                let root_key = path_utils::get_value_by_pointer(&self.schema, schema_pointer)
761                    .and_then(|node| node.get("itemsRootKey"))
762                    .and_then(|v| v.as_str())
763                    .unwrap_or(&original_field_key)
764                    .to_string();
765
766                let item_count = current_data
767                    .pointer(&data_ptr)
768                    .and_then(Value::as_array)
769                    .map(|a| a.len())
770                    .unwrap_or(0);
771
772                if item_count > 0 {
773                    let full_data = self.eval_data.snapshot_data_clone();
774                    let context_value = self
775                        .eval_data
776                        .data()
777                        .get("$context")
778                        .cloned()
779                        .unwrap_or_else(|| Value::Object(serde_json::Map::new()));
780
781                    let existing_items = current_data
782                        .pointer(&data_ptr)
783                        .and_then(Value::as_array)
784                        .cloned()
785                        .unwrap_or_default();
786
787                    let mut new_items = Vec::with_capacity(item_count);
788
789                    for (idx, raw_item) in existing_items.into_iter().enumerate() {
790                        let evaluated_item_res = self.with_item_cache_swap(
791                            &subform_path,
792                            idx,
793                            full_data.clone(),
794                            context_value.clone(),
795                            false,
796                            |sf| {
797                                sf.evaluate_internal_pre_diffed(None, None)?;
798                                if sf.apply_visible_static_defaults_with_dependents(None)? {
799                                    sf.evaluate_internal_pre_diffed(None, None)?;
800                                }
801                                Ok(sf.get_schema_value(Some(true)))
802                            },
803                        );
804
805                        match evaluated_item_res {
806                            Ok(mut val) => {
807                                let item_val = val
808                                    .as_object_mut()
809                                    .and_then(|obj| obj.remove(&root_key))
810                                    .unwrap_or(raw_item);
811                                new_items.push(item_val);
812                            }
813                            Err(_) => {
814                                new_items.push(raw_item);
815                            }
816                        }
817                    }
818
819                    if let Some(target) = current_data.pointer_mut(&data_ptr) {
820                        *target = Value::Array(new_items);
821                    }
822                }
823            }
824        }
825
826        crate::utils::clean_float_noise(current_data)
827    }
828
829    /// Get all schema values as array of path-value pairs
830    /// Returns [{path: "", value: ""}, ...]
831    ///
832    /// # Returns
833    ///
834    /// Array of objects containing path (dotted notation) and value pairs from value evaluations
835    pub fn get_schema_value_array(&self) -> Value {
836        self.ensure_layout_resolved();
837        let mut result = Vec::new();
838
839        for eval_key in self.value_evaluations.iter() {
840            let clean_key = eval_key.strip_prefix('#').unwrap_or(eval_key);
841
842            // Exclude rules.*.value, options.*.value, and $params
843            if clean_key.starts_with("/$params")
844                || (clean_key.ends_with("/value")
845                    && (clean_key.contains("/rules/") || clean_key.contains("/options/")))
846            {
847                continue;
848            }
849
850            // Check if field is effectively hidden
851            let schema_path = clean_key.strip_suffix("/value").unwrap_or(&clean_key);
852            if self.is_effective_hidden(schema_path) {
853                continue;
854            }
855
856            // Convert JSON pointer to dotted notation
857            let dotted_path = clean_key
858                .replace("/properties", "")
859                .replace("/value", "")
860                .trim_start_matches('/')
861                .replace('/', ".");
862
863            if dotted_path.is_empty() {
864                continue;
865            }
866
867            // Resolve static markers at this specific pointer (handles markers at or under this path)
868            let value = match self.resolve_static_markers_at_path(clean_key) {
869                Some(v) => crate::utils::clean_float_noise(v),
870                None => continue,
871            };
872
873            // Create {path, value} object
874            let mut item = serde_json::Map::new();
875            item.insert("path".to_string(), Value::String(dotted_path));
876            item.insert("value".to_string(), value);
877            result.push(Value::Object(item));
878        }
879
880        Value::Array(result)
881    }
882
883    /// Get all schema values as object with dotted path keys
884    /// Returns {path: value, ...}
885    ///
886    /// # Returns
887    ///
888    /// Flat object with dotted notation paths as keys and evaluated values
889    pub fn get_schema_value_object(&self) -> Value {
890        self.ensure_layout_resolved();
891        let mut result = serde_json::Map::new();
892
893        for eval_key in self.value_evaluations.iter() {
894            let clean_key = eval_key.strip_prefix('#').unwrap_or(eval_key);
895
896            // Exclude rules.*.value, options.*.value, and $params
897            if clean_key.starts_with("/$params")
898                || (clean_key.ends_with("/value")
899                    && (clean_key.contains("/rules/") || clean_key.contains("/options/")))
900            {
901                continue;
902            }
903
904            // Check if field is effectively hidden
905            let schema_path = clean_key.strip_suffix("/value").unwrap_or(&clean_key);
906            if self.is_effective_hidden(schema_path) {
907                continue;
908            }
909
910            // Convert JSON pointer to dotted notation
911            let dotted_path = clean_key
912                .replace("/properties", "")
913                .replace("/value", "")
914                .trim_start_matches('/')
915                .replace('/', ".");
916
917            if dotted_path.is_empty() {
918                continue;
919            }
920
921            // Resolve static markers at this specific pointer (handles markers at or under this path)
922            let value = match self.resolve_static_markers_at_path(clean_key) {
923                Some(v) => crate::utils::clean_float_noise(v),
924                None => continue,
925            };
926
927            result.insert(dotted_path, value);
928        }
929
930        Value::Object(result)
931    }
932
933    /// Get evaluated schema without $params
934    pub fn get_evaluated_schema_without_params(&mut self) -> Value {
935        time_block!("get_evaluated_schema_without_params()", {
936            let mut schema = if let Value::Object(map) = &self.evaluated_schema {
937                let mut filtered = serde_json::Map::with_capacity(map.len().saturating_sub(1));
938                for (k, v) in map {
939                    if k != "$params" {
940                        filtered.insert(k.clone(), v.clone());
941                    }
942                }
943                Value::Object(filtered)
944            } else {
945                self.evaluated_schema.clone()
946            };
947            self.resolve_static_markers_in_value(&mut schema);
948            schema
949        })
950    }
951
952    fn process_params_static_arrays(
953        &self,
954        params_val: &Value,
955        with_static_array: bool,
956        resolve_tables: bool,
957    ) -> Value {
958        let mut params = params_val.clone();
959
960        // Evaluated $table arrays in $params are dynamic results and must always be resolved in evaluated params
961        if resolve_tables {
962            for (static_key, array_arc) in self.static_arrays.iter() {
963                if let Some(rel_path) = static_key.strip_prefix("/$table/$params") {
964                    if let Some(target) = params.pointer_mut(rel_path) {
965                        *target = (**array_arc).clone();
966                    }
967                }
968            }
969        }
970
971        if with_static_array {
972            for (static_key, array_arc) in self.static_arrays.iter() {
973                if let Some(rel_path) = static_key.strip_prefix("/$params") {
974                    if let Some(target) = params.pointer_mut(rel_path) {
975                        *target = (**array_arc).clone();
976                    }
977                }
978            }
979        } else {
980            Self::strip_static_array_markers(&mut params);
981        }
982        params
983    }
984
985    fn strip_static_array_markers(val: &mut Value) {
986        match val {
987            Value::Object(map) => {
988                map.retain(|_, v| {
989                    if let Value::Object(child_map) = v {
990                        if let Some(Value::String(marker_path)) = child_map.get("$static_array") {
991                            !marker_path.starts_with("/$params")
992                        } else {
993                            !child_map.contains_key("$static_array")
994                        }
995                    } else {
996                        true
997                    }
998                });
999                for v in map.values_mut() {
1000                    Self::strip_static_array_markers(v);
1001                }
1002            }
1003            Value::Array(arr) => {
1004                arr.retain(|v| {
1005                    if let Value::Object(child_map) = v {
1006                        if let Some(Value::String(marker_path)) = child_map.get("$static_array") {
1007                            !marker_path.starts_with("/$params")
1008                        } else {
1009                            !child_map.contains_key("$static_array")
1010                        }
1011                    } else {
1012                        true
1013                    }
1014                });
1015                for v in arr.iter_mut() {
1016                    Self::strip_static_array_markers(v);
1017                }
1018            }
1019            _ => {}
1020        }
1021    }
1022
1023    /// Get plain `$params` from the original schema (without static array data).
1024    pub fn get_plain_params(&self) -> Option<Value> {
1025        let raw_params = self.schema.get("$params")?;
1026        Some(self.process_params_static_arrays(raw_params, false, false))
1027    }
1028
1029    /// Get evaluated `$params` from `evaluated_schema`.
1030    ///
1031    /// # Arguments
1032    /// * `with_static_array` - If true, static arrays extracted to `static_arrays` are resolved
1033    ///   back into `$params`. If false, static array keys are omitted/stripped.
1034    pub fn get_evaluated_params(&mut self, with_static_array: bool) -> Option<Value> {
1035        let raw_params = self.evaluated_schema.get("$params")?;
1036        Some(self.process_params_static_arrays(raw_params, with_static_array, true))
1037    }
1038
1039    /// Get evaluated schema as MessagePack bytes (compact, without $layout resolution)
1040    pub fn get_evaluated_schema_msgpack(&mut self) -> Result<Vec<u8>, String> {
1041        let schema = self.get_evaluated_schema();
1042        rmp_serde::to_vec(&schema).map_err(|e| format!("MessagePack serialization failed: {}", e))
1043    }
1044
1045    /// Get layout-resolved evaluated schema as MessagePack bytes.
1046    ///
1047    /// Reuses `get_evaluated_schema_resolved`, which omits `$params` and merges
1048    /// resolved `$layout` overlays.
1049    pub fn get_evaluated_schema_resolved_msgpack(&mut self) -> Result<Vec<u8>, String> {
1050        let schema = self.get_evaluated_schema_resolved();
1051        rmp_serde::to_vec(&schema).map_err(|e| format!("MessagePack serialization failed: {}", e))
1052    }
1053
1054    /// Get value from evaluated schema by path
1055    pub fn get_evaluated_schema_by_path(&mut self, path: &str) -> Option<Value> {
1056        self.get_schema_value_by_path(path)
1057    }
1058
1059    /// Get evaluated schema parts by multiple paths
1060    pub fn get_evaluated_schema_by_paths(
1061        &mut self,
1062        paths: &[String],
1063        format: Option<ReturnFormat>,
1064    ) -> Value {
1065        match format.unwrap_or(ReturnFormat::Nested) {
1066            ReturnFormat::Nested => {
1067                let mut result = Value::Object(serde_json::Map::new());
1068                for path in paths {
1069                    if let Some(val) = self.get_schema_value_by_path(path) {
1070                        // Insert into result object at proper path nesting
1071                        Self::insert_at_path(&mut result, path, val);
1072                    }
1073                }
1074                result
1075            }
1076            ReturnFormat::Flat => {
1077                let mut result = serde_json::Map::new();
1078                for path in paths {
1079                    if let Some(val) = self.get_schema_value_by_path(path) {
1080                        result.insert(path.clone(), val);
1081                    }
1082                }
1083                Value::Object(result)
1084            }
1085            ReturnFormat::Array => {
1086                let mut result = Vec::new();
1087                for path in paths {
1088                    if let Some(val) = self.get_schema_value_by_path(path) {
1089                        result.push(val);
1090                    } else {
1091                        result.push(Value::Null);
1092                    }
1093                }
1094                Value::Array(result)
1095            }
1096        }
1097    }
1098
1099    /// Get original (unevaluated) schema by path
1100    pub fn get_schema_by_path(&self, path: &str) -> Option<Value> {
1101        let pointer_path = path_utils::dot_notation_to_schema_pointer(path);
1102        self.schema
1103            .pointer(&pointer_path.trim_start_matches('#'))
1104            .cloned()
1105    }
1106
1107    /// Get original schema by multiple paths
1108    pub fn get_schema_by_paths(&self, paths: &[String], format: Option<ReturnFormat>) -> Value {
1109        match format.unwrap_or(ReturnFormat::Nested) {
1110            ReturnFormat::Nested => {
1111                let mut result = Value::Object(serde_json::Map::new());
1112                for path in paths {
1113                    if let Some(val) = self.get_schema_by_path(path) {
1114                        Self::insert_at_path(&mut result, path, val);
1115                    }
1116                }
1117                result
1118            }
1119            ReturnFormat::Flat => {
1120                let mut result = serde_json::Map::new();
1121                for path in paths {
1122                    if let Some(val) = self.get_schema_by_path(path) {
1123                        result.insert(path.clone(), val);
1124                    }
1125                }
1126                Value::Object(result)
1127            }
1128            ReturnFormat::Array => {
1129                let mut result = Vec::new();
1130                for path in paths {
1131                    if let Some(val) = self.get_schema_by_path(path) {
1132                        result.push(val);
1133                    } else {
1134                        result.push(Value::Null);
1135                    }
1136                }
1137                Value::Array(result)
1138            }
1139        }
1140    }
1141
1142    /// Helper to insert value into nested object at dotted path
1143    pub(crate) fn insert_at_path(root: &mut Value, path: &str, value: Value) {
1144        let parts: Vec<&str> = path.split('.').collect();
1145        let mut current = root;
1146
1147        for (i, part) in parts.iter().enumerate() {
1148            if i == parts.len() - 1 {
1149                // Last part - set value
1150                if let Value::Object(map) = current {
1151                    map.insert(part.to_string(), value);
1152                    return; // Done
1153                }
1154            } else {
1155                // Intermediate part - traverse or create
1156                // We need to temporarily take the value or use raw pointer manipulation?
1157                // serde_json pointer is read-only or requires mutable reference
1158
1159                if !current.is_object() {
1160                    *current = Value::Object(serde_json::Map::new());
1161                }
1162
1163                if let Value::Object(map) = current {
1164                    if !map.contains_key(*part) {
1165                        map.insert(part.to_string(), Value::Object(serde_json::Map::new()));
1166                    }
1167                    current = map.get_mut(*part).unwrap();
1168                }
1169            }
1170        }
1171    }
1172
1173    /// Flatten a nested object key-value pair to dotted keys
1174    pub fn flatten_object(
1175        prefix: &str,
1176        value: &Value,
1177        result: &mut serde_json::Map<String, Value>,
1178    ) {
1179        match value {
1180            Value::Object(map) => {
1181                for (k, v) in map {
1182                    let new_key = if prefix.is_empty() {
1183                        k.clone()
1184                    } else {
1185                        format!("{}.{}", prefix, k)
1186                    };
1187                    Self::flatten_object(&new_key, v, result);
1188                }
1189            }
1190            _ => {
1191                result.insert(prefix.to_string(), value.clone());
1192            }
1193        }
1194    }
1195
1196    pub fn convert_to_format(value: Value, format: ReturnFormat) -> Value {
1197        match format {
1198            ReturnFormat::Nested => value,
1199            ReturnFormat::Flat => {
1200                let mut result = serde_json::Map::new();
1201                Self::flatten_object("", &value, &mut result);
1202                Value::Object(result)
1203            }
1204            ReturnFormat::Array => {
1205                if let Value::Object(map) = value {
1206                    Value::Array(map.values().cloned().collect())
1207                } else if let Value::Array(arr) = value {
1208                    Value::Array(arr)
1209                } else {
1210                    Value::Array(vec![value])
1211                }
1212            }
1213        }
1214    }
1215
1216    /// Evaluate and return the options for a specific field on demand.
1217    ///
1218    /// Accepts dotted notation (`form.occupation`), JSON pointer
1219    /// (`/properties/form/properties/occupation`), or schema ref
1220    /// (`#/properties/form/properties/occupation`).
1221    ///
1222    /// Returns `None` when the field does not have an `options` key.
1223    /// Returns the resolved options value (array, URL string, or null) otherwise.
1224    pub fn get_field_options(&mut self, field_path: &str) -> Option<Value> {
1225        // Normalize the input to a schema pointer (e.g. #/properties/form/properties/occupation)
1226        let schema_ptr = if field_path.starts_with('#') || field_path.starts_with('/') {
1227            path_utils::normalize_to_json_pointer(field_path).into_owned()
1228        } else {
1229            path_utils::dot_notation_to_schema_pointer(field_path)
1230        };
1231
1232        // Build the JSON pointer path to the /options node (strip leading # for serde pointer())
1233        let options_schema_key = format!("{}/options", schema_ptr);
1234        let options_pointer =
1235            path_utils::normalize_to_json_pointer(&options_schema_key).into_owned();
1236
1237        // Check if the options node exists in the evaluated schema
1238        let options_node = self.evaluated_schema.pointer(&options_pointer)?.clone();
1239
1240        // If the options node is an object with $evaluation, evaluate it now (deferred)
1241        if let Value::Object(ref map) = options_node {
1242            if map.contains_key("$evaluation") {
1243                let eval_key = options_schema_key.clone();
1244
1245                if let Some(logic_id) = self.evaluations.get(&eval_key).copied() {
1246                    let snap = self.eval_data.snapshot_data();
1247                    if let Ok(result) = self.engine.run(&logic_id, &*snap) {
1248                        let cleaned = clean_float_noise_scalar(result);
1249                        if let Some(node) = self.evaluated_schema.pointer_mut(&options_pointer) {
1250                            *node = cleaned.clone();
1251                        }
1252                        return Some(cleaned);
1253                    }
1254                }
1255                // No compiled logic found — options cannot be resolved
1256                return None;
1257            }
1258        }
1259
1260        // Check options_templates for a URL template at this field's options/url path
1261        let url_pointer =
1262            path_utils::normalize_to_json_pointer(&format!("{}/options/url", schema_ptr))
1263                .into_owned();
1264
1265        let templates = self.options_templates.clone();
1266        for (tmpl_url_path, tmpl_str, tmpl_params_path) in templates.iter() {
1267            if *tmpl_url_path == url_pointer {
1268                if let Some(params) = self.evaluated_schema.pointer(tmpl_params_path) {
1269                    let params = params.clone();
1270                    if let Ok(resolved_url) = self.evaluate_template(tmpl_str, &params) {
1271                        if let Some(target) = self.evaluated_schema.pointer_mut(&url_pointer) {
1272                            *target = Value::String(resolved_url);
1273                        }
1274                        return self.evaluated_schema.pointer(&options_pointer).cloned();
1275                    }
1276                }
1277                break;
1278            }
1279        }
1280
1281        // Static options (already-evaluated array or plain value)
1282        Some(options_node)
1283    }
1284}