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(&self, params_val: &Value, with_static_array: bool) -> Value {
953        let mut params = params_val.clone();
954        if with_static_array {
955            for (static_key, array_arc) in self.static_arrays.iter() {
956                let rel_path = if let Some(path) = static_key.strip_prefix("/$params") {
957                    path
958                } else if let Some(path) = static_key.strip_prefix("/$table/$params") {
959                    path
960                } else {
961                    continue;
962                };
963
964                if let Some(target) = params.pointer_mut(rel_path) {
965                    *target = (**array_arc).clone();
966                }
967            }
968        } else {
969            Self::strip_static_array_markers(&mut params);
970        }
971        params
972    }
973
974    fn strip_static_array_markers(val: &mut Value) {
975        match val {
976            Value::Object(map) => {
977                map.retain(|_, v| {
978                    if let Value::Object(child_map) = v {
979                        !child_map.contains_key("$static_array")
980                    } else {
981                        true
982                    }
983                });
984                for v in map.values_mut() {
985                    Self::strip_static_array_markers(v);
986                }
987            }
988            Value::Array(arr) => {
989                arr.retain(|v| {
990                    if let Value::Object(child_map) = v {
991                        !child_map.contains_key("$static_array")
992                    } else {
993                        true
994                    }
995                });
996                for v in arr.iter_mut() {
997                    Self::strip_static_array_markers(v);
998                }
999            }
1000            _ => {}
1001        }
1002    }
1003
1004    /// Get plain `$params` from the original schema (without static array data).
1005    pub fn get_plain_params(&self) -> Option<Value> {
1006        let raw_params = self.schema.get("$params")?;
1007        Some(self.process_params_static_arrays(raw_params, false))
1008    }
1009
1010    /// Get evaluated `$params` from `evaluated_schema`.
1011    ///
1012    /// # Arguments
1013    /// * `with_static_array` - If true, static arrays extracted to `static_arrays` are resolved
1014    ///   back into `$params`. If false, static array keys are omitted/stripped.
1015    pub fn get_evaluated_params(&mut self, with_static_array: bool) -> Option<Value> {
1016        let raw_params = self.evaluated_schema.get("$params")?;
1017        Some(self.process_params_static_arrays(raw_params, with_static_array))
1018    }
1019
1020    /// Get evaluated schema as MessagePack bytes (compact, without $layout resolution)
1021    pub fn get_evaluated_schema_msgpack(&mut self) -> Result<Vec<u8>, String> {
1022        let schema = self.get_evaluated_schema();
1023        rmp_serde::to_vec(&schema).map_err(|e| format!("MessagePack serialization failed: {}", e))
1024    }
1025
1026    /// Get layout-resolved evaluated schema as MessagePack bytes.
1027    ///
1028    /// Reuses `get_evaluated_schema_resolved`, which omits `$params` and merges
1029    /// resolved `$layout` overlays.
1030    pub fn get_evaluated_schema_resolved_msgpack(&mut self) -> Result<Vec<u8>, String> {
1031        let schema = self.get_evaluated_schema_resolved();
1032        rmp_serde::to_vec(&schema).map_err(|e| format!("MessagePack serialization failed: {}", e))
1033    }
1034
1035    /// Get value from evaluated schema by path
1036    pub fn get_evaluated_schema_by_path(&mut self, path: &str) -> Option<Value> {
1037        self.get_schema_value_by_path(path)
1038    }
1039
1040    /// Get evaluated schema parts by multiple paths
1041    pub fn get_evaluated_schema_by_paths(
1042        &mut self,
1043        paths: &[String],
1044        format: Option<ReturnFormat>,
1045    ) -> Value {
1046        match format.unwrap_or(ReturnFormat::Nested) {
1047            ReturnFormat::Nested => {
1048                let mut result = Value::Object(serde_json::Map::new());
1049                for path in paths {
1050                    if let Some(val) = self.get_schema_value_by_path(path) {
1051                        // Insert into result object at proper path nesting
1052                        Self::insert_at_path(&mut result, path, val);
1053                    }
1054                }
1055                result
1056            }
1057            ReturnFormat::Flat => {
1058                let mut result = serde_json::Map::new();
1059                for path in paths {
1060                    if let Some(val) = self.get_schema_value_by_path(path) {
1061                        result.insert(path.clone(), val);
1062                    }
1063                }
1064                Value::Object(result)
1065            }
1066            ReturnFormat::Array => {
1067                let mut result = Vec::new();
1068                for path in paths {
1069                    if let Some(val) = self.get_schema_value_by_path(path) {
1070                        result.push(val);
1071                    } else {
1072                        result.push(Value::Null);
1073                    }
1074                }
1075                Value::Array(result)
1076            }
1077        }
1078    }
1079
1080    /// Get original (unevaluated) schema by path
1081    pub fn get_schema_by_path(&self, path: &str) -> Option<Value> {
1082        let pointer_path = path_utils::dot_notation_to_schema_pointer(path);
1083        self.schema
1084            .pointer(&pointer_path.trim_start_matches('#'))
1085            .cloned()
1086    }
1087
1088    /// Get original schema by multiple paths
1089    pub fn get_schema_by_paths(&self, paths: &[String], format: Option<ReturnFormat>) -> Value {
1090        match format.unwrap_or(ReturnFormat::Nested) {
1091            ReturnFormat::Nested => {
1092                let mut result = Value::Object(serde_json::Map::new());
1093                for path in paths {
1094                    if let Some(val) = self.get_schema_by_path(path) {
1095                        Self::insert_at_path(&mut result, path, val);
1096                    }
1097                }
1098                result
1099            }
1100            ReturnFormat::Flat => {
1101                let mut result = serde_json::Map::new();
1102                for path in paths {
1103                    if let Some(val) = self.get_schema_by_path(path) {
1104                        result.insert(path.clone(), val);
1105                    }
1106                }
1107                Value::Object(result)
1108            }
1109            ReturnFormat::Array => {
1110                let mut result = Vec::new();
1111                for path in paths {
1112                    if let Some(val) = self.get_schema_by_path(path) {
1113                        result.push(val);
1114                    } else {
1115                        result.push(Value::Null);
1116                    }
1117                }
1118                Value::Array(result)
1119            }
1120        }
1121    }
1122
1123    /// Helper to insert value into nested object at dotted path
1124    pub(crate) fn insert_at_path(root: &mut Value, path: &str, value: Value) {
1125        let parts: Vec<&str> = path.split('.').collect();
1126        let mut current = root;
1127
1128        for (i, part) in parts.iter().enumerate() {
1129            if i == parts.len() - 1 {
1130                // Last part - set value
1131                if let Value::Object(map) = current {
1132                    map.insert(part.to_string(), value);
1133                    return; // Done
1134                }
1135            } else {
1136                // Intermediate part - traverse or create
1137                // We need to temporarily take the value or use raw pointer manipulation?
1138                // serde_json pointer is read-only or requires mutable reference
1139
1140                if !current.is_object() {
1141                    *current = Value::Object(serde_json::Map::new());
1142                }
1143
1144                if let Value::Object(map) = current {
1145                    if !map.contains_key(*part) {
1146                        map.insert(part.to_string(), Value::Object(serde_json::Map::new()));
1147                    }
1148                    current = map.get_mut(*part).unwrap();
1149                }
1150            }
1151        }
1152    }
1153
1154    /// Flatten a nested object key-value pair to dotted keys
1155    pub fn flatten_object(
1156        prefix: &str,
1157        value: &Value,
1158        result: &mut serde_json::Map<String, Value>,
1159    ) {
1160        match value {
1161            Value::Object(map) => {
1162                for (k, v) in map {
1163                    let new_key = if prefix.is_empty() {
1164                        k.clone()
1165                    } else {
1166                        format!("{}.{}", prefix, k)
1167                    };
1168                    Self::flatten_object(&new_key, v, result);
1169                }
1170            }
1171            _ => {
1172                result.insert(prefix.to_string(), value.clone());
1173            }
1174        }
1175    }
1176
1177    pub fn convert_to_format(value: Value, format: ReturnFormat) -> Value {
1178        match format {
1179            ReturnFormat::Nested => value,
1180            ReturnFormat::Flat => {
1181                let mut result = serde_json::Map::new();
1182                Self::flatten_object("", &value, &mut result);
1183                Value::Object(result)
1184            }
1185            ReturnFormat::Array => {
1186                if let Value::Object(map) = value {
1187                    Value::Array(map.values().cloned().collect())
1188                } else if let Value::Array(arr) = value {
1189                    Value::Array(arr)
1190                } else {
1191                    Value::Array(vec![value])
1192                }
1193            }
1194        }
1195    }
1196
1197    /// Evaluate and return the options for a specific field on demand.
1198    ///
1199    /// Accepts dotted notation (`form.occupation`), JSON pointer
1200    /// (`/properties/form/properties/occupation`), or schema ref
1201    /// (`#/properties/form/properties/occupation`).
1202    ///
1203    /// Returns `None` when the field does not have an `options` key.
1204    /// Returns the resolved options value (array, URL string, or null) otherwise.
1205    pub fn get_field_options(&mut self, field_path: &str) -> Option<Value> {
1206        // Normalize the input to a schema pointer (e.g. #/properties/form/properties/occupation)
1207        let schema_ptr = if field_path.starts_with('#') || field_path.starts_with('/') {
1208            path_utils::normalize_to_json_pointer(field_path).into_owned()
1209        } else {
1210            path_utils::dot_notation_to_schema_pointer(field_path)
1211        };
1212
1213        // Build the JSON pointer path to the /options node (strip leading # for serde pointer())
1214        let options_schema_key = format!("{}/options", schema_ptr);
1215        let options_pointer =
1216            path_utils::normalize_to_json_pointer(&options_schema_key).into_owned();
1217
1218        // Check if the options node exists in the evaluated schema
1219        let options_node = self.evaluated_schema.pointer(&options_pointer)?.clone();
1220
1221        // If the options node is an object with $evaluation, evaluate it now (deferred)
1222        if let Value::Object(ref map) = options_node {
1223            if map.contains_key("$evaluation") {
1224                let eval_key = options_schema_key.clone();
1225
1226                if let Some(logic_id) = self.evaluations.get(&eval_key).copied() {
1227                    let snap = self.eval_data.snapshot_data();
1228                    if let Ok(result) = self.engine.run(&logic_id, &*snap) {
1229                        let cleaned = clean_float_noise_scalar(result);
1230                        if let Some(node) = self.evaluated_schema.pointer_mut(&options_pointer) {
1231                            *node = cleaned.clone();
1232                        }
1233                        return Some(cleaned);
1234                    }
1235                }
1236                // No compiled logic found — options cannot be resolved
1237                return None;
1238            }
1239        }
1240
1241        // Check options_templates for a URL template at this field's options/url path
1242        let url_pointer =
1243            path_utils::normalize_to_json_pointer(&format!("{}/options/url", schema_ptr))
1244                .into_owned();
1245
1246        let templates = self.options_templates.clone();
1247        for (tmpl_url_path, tmpl_str, tmpl_params_path) in templates.iter() {
1248            if *tmpl_url_path == url_pointer {
1249                if let Some(params) = self.evaluated_schema.pointer(tmpl_params_path) {
1250                    let params = params.clone();
1251                    if let Ok(resolved_url) = self.evaluate_template(tmpl_str, &params) {
1252                        if let Some(target) = self.evaluated_schema.pointer_mut(&url_pointer) {
1253                            *target = Value::String(resolved_url);
1254                        }
1255                        return self.evaluated_schema.pointer(&options_pointer).cloned();
1256                    }
1257                }
1258                break;
1259            }
1260        }
1261
1262        // Static options (already-evaluated array or plain value)
1263        Some(options_node)
1264    }
1265}