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
289                // For nested fields: current_path/properties/key
290                let schema_path = if current_path.is_empty() {
291                    format!("/properties/{}", key)
292                } else {
293                    format!("{}/properties/{}", current_path, key)
294                };
295
296                // Check if hidden
297                if self.is_effective_hidden(&schema_path) {
298                    keys_to_remove.push(key.clone());
299                } else {
300                    // Recurse if object
301                    if value.is_object() {
302                        self.prune_hidden_values(value, &schema_path);
303                    }
304                }
305            }
306
307            // Remove hidden keys
308            for key in keys_to_remove {
309                map.remove(&key);
310            }
311        }
312    }
313
314    /// Replace any `{"$static_array": "/$table/..."}` and `{"$static_array": "/$params/..."}` markers in `schema_output`
315    /// with the actual evaluated array data from `eval_data`.
316    ///
317    /// By iterating only over tracked `static_arrays`, we replace markers in O(markers) time
318    /// instead of requiring an expensive O(schema_nodes) recursive tree walk.
319    fn resolve_static_markers_in_value(&self, schema_output: &mut Value) {
320        for (static_key, array_arc) in self.static_arrays.iter() {
321            // Determine the schema pointer path where this marker was placed
322            let schema_path = if static_key.starts_with("/$table") {
323                &static_key["/$table".len()..] // e.g. /properties/product_benefit/...
324            } else {
325                static_key.as_str() // e.g. /$params/references/...
326            };
327
328            // Only attempt replacement if the exact path exists in the cloned schema output
329            if let Some(target_val) = schema_output.pointer_mut(schema_path) {
330                // The actual evaluated array is seamlessly stored right in the map's value
331                *target_val = (**array_arc).clone();
332            }
333        }
334    }
335
336    /// Get the evaluated schema (compact — $ref intact, no layout expansion).
337    ///
338    /// # Returns
339    ///
340    /// The evaluated schema as a JSON value, with all `$static_array` markers resolved
341    /// to their actual evaluated data.
342    pub fn get_evaluated_schema(&mut self) -> Value {
343        time_block!("get_evaluated_schema()", {
344            let mut schema = self.evaluated_schema.clone();
345            self.resolve_static_markers_in_value(&mut schema);
346            schema
347        })
348    }
349
350    /// Get layout overlay entries — the delta properties per layout element.
351    /// Consumer merges these into compact schema to get fully resolved layout.
352    pub fn get_resolved_layout(&mut self) -> ResolvedLayoutResult {
353        time_block!("get_resolved_layout()", {
354            self.ensure_layout_resolved();
355            self.layout_state
356                .read()
357                .unwrap()
358                .cache
359                .as_ref()
360                .map(|c| (**c).clone())
361                .unwrap_or_default()
362        })
363    }
364
365    /// Get evaluated schema with layout overlays already applied.
366    /// Convenience: returns compact schema + overlays merged.
367    ///
368    /// Two-pass approach to handle nested elements:
369    /// 1. First pass: resolve $ref and apply overlay for entries whose target
370    ///    path exists in the compact schema (top-level elements).
371    /// 2. Second pass: apply overlay-only for entries whose path appears
372    ///    after parent $ref resolution (nested elements).
373    pub fn get_evaluated_schema_resolved(&mut self) -> Value {
374        time_block!("get_evaluated_schema_resolved()", {
375            let mut schema = self.get_evaluated_schema_without_params();
376            let overlays = self.get_resolved_layout();
377
378            struct ResolveEntry {
379                layout_path: String,
380                element_idx: usize,
381                overlay: indexmap::IndexMap<String, Value>,
382            }
383
384            let mut entries: Vec<ResolveEntry> = overlays
385                .iter()
386                .map(|entry| {
387                    let layout_path =
388                        path_utils::normalize_to_json_pointer(&entry.layout_path).into_owned();
389                    ResolveEntry {
390                        layout_path,
391                        element_idx: entry.element_idx,
392                        overlay: entry.overlay.clone(),
393                    }
394                })
395                .collect();
396            drop(overlays);
397
398            // Sort entries shallow-first so parent elements are expanded before their children.
399            // Child entries (e.g. layout_path = ".../elements/1/elements") depend on the parent
400            // ("…/elements") being resolved first so the nested `elements` array exists in `schema`.
401            entries.sort_by(|a, b| {
402                let depth_a = a.layout_path.matches('/').count();
403                let depth_b = b.layout_path.matches('/').count();
404                depth_a
405                    .cmp(&depth_b)
406                    .then_with(|| a.element_idx.cmp(&b.element_idx))
407            });
408
409            // ── Phase 2 (mutable): resolve $ref + apply overlays (parent-first order) ──
410            // Entries are sorted shallowest layout_path first, so parent elements are
411            // expanded before any child entries that path through them.
412            for entry in entries {
413                // Resolve $ref from the current (already partially mutated) schema so that
414                // parent expansions are visible when we process child entries.
415                let resolved_value: Option<Value> = (|| -> Option<Value> {
416                    let arr = schema.pointer(&entry.layout_path)?.as_array()?;
417                    let element = arr.get(entry.element_idx)?;
418                    let ref_str = element.get("$ref")?.as_str()?;
419
420                    let ref_pointer = if ref_str.starts_with('#') || ref_str.starts_with('/') {
421                        path_utils::normalize_to_json_pointer(ref_str).into_owned()
422                    } else {
423                        let schema_pointer = path_utils::dot_notation_to_schema_pointer(ref_str);
424                        let normalized =
425                            path_utils::normalize_to_json_pointer(&schema_pointer).into_owned();
426                        if schema.pointer(&normalized).is_some() {
427                            normalized
428                        } else {
429                            format!("/properties/{}", ref_str.replace('.', "/properties/"))
430                        }
431                    };
432
433                    let mut resolved = schema.pointer(&ref_pointer)?.clone();
434
435                    // Flatten $layout into top level
436                    if let Value::Object(ref mut resolved_map) = resolved {
437                        if let Some(Value::Object(layout_obj)) = resolved_map.remove("$layout") {
438                            let mut result = layout_obj;
439                            for (key, value) in resolved_map.clone().into_iter() {
440                                if key != "type" || !result.contains_key("type") {
441                                    result.insert(key, value);
442                                }
443                            }
444                            resolved = Value::Object(result);
445                        }
446                    }
447
448                    Some(resolved)
449                })();
450
451                if let Some(Value::Array(arr)) = schema.pointer_mut(&entry.layout_path) {
452                    if entry.element_idx < arr.len() {
453                        let element = &mut arr[entry.element_idx];
454
455                        // Apply $ref resolution
456                        if let Some(resolved) = resolved_value {
457                            if let Value::Object(mut resolved_map) = resolved {
458                                if let Value::Object(mut map) = element.take() {
459                                    map.remove("$ref");
460                                    for (key, value) in map {
461                                        resolved_map.insert(key, value);
462                                    }
463                                }
464                                *element = Value::Object(resolved_map);
465                            } else {
466                                *element = resolved;
467                            }
468                        }
469
470                        // Apply overlay on top
471                        if let Value::Object(ref mut map) = element {
472                            for (k, v) in &entry.overlay {
473                                map.insert(k.clone(), v.clone());
474                            }
475                        }
476                    }
477                }
478            }
479
480            Self::stamp_property_metadata(&mut schema);
481            schema
482        })
483    }
484
485    /// Stamp every schema property with raw pointer-style dotted metadata.
486    fn stamp_property_metadata(schema: &mut Value) {
487        fn walk(value: &mut Value, path: &str, parent_hidden: bool) {
488            let Some(map) = value.as_object_mut() else {
489                return;
490            };
491
492            let hidden = parent_hidden
493                || map
494                    .get("condition")
495                    .and_then(Value::as_object)
496                    .and_then(|condition| condition.get("hidden"))
497                    .is_some_and(|hidden| hidden == &Value::Bool(true));
498
499            if let Some(Value::Object(properties)) = map.get_mut("properties") {
500                for (name, property) in properties {
501                    let property_path = if path.is_empty() {
502                        format!("properties.{}", name)
503                    } else {
504                        format!("{}.properties.{}", path, name)
505                    };
506                    if let Value::Object(property_map) = property {
507                        property_map.insert(
508                            "$fullpath".to_string(),
509                            Value::String(property_path.clone()),
510                        );
511                        property_map.insert("$path".to_string(), Value::String(name.clone()));
512                        property_map.insert("$parentHide".to_string(), Value::Bool(hidden));
513                    }
514                    walk(property, &property_path, hidden);
515                }
516            }
517
518            for (name, child) in map {
519                if name != "properties" && !name.starts_with('$') && child.is_object() {
520                    let child_path = if path.is_empty() {
521                        name.clone()
522                    } else {
523                        format!("{}.{}", path, name)
524                    };
525                    walk(child, &child_path, hidden);
526                }
527            }
528        }
529
530        walk(schema, "", false);
531    }
532
533    /// Resolve `$static_array` markers within the subtree rooted at `schema_prefix`.
534    ///
535    /// Clones only the node at `schema_prefix` from `evaluated_schema`, then iterates
536    /// the tracked `static_arrays` list filtering to entries whose schema path is at or
537    /// under `schema_prefix`. Only those markers are replaced inside the cloned subtree;
538    /// unrelated entries are skipped entirely.
539    ///
540    /// # Examples
541    /// - `schema_prefix = "/$params/references"` → resolves only arrays nested under that key
542    /// - `schema_prefix = "/properties/foo/value"` → resolves a single marker if the field itself is one
543    pub(crate) fn resolve_static_markers_at_path(&self, schema_prefix: &str) -> Option<Value> {
544        // Resolve indexed static-array paths directly.
545        for (static_key, array_arc) in self.static_arrays.iter() {
546            let schema_path: &str = if static_key.starts_with("/$table") {
547                &static_key["/$table".len()..]
548            } else {
549                static_key.as_str()
550            };
551
552            if let Some(relative) = schema_prefix
553                .strip_prefix(schema_path)
554                .and_then(|relative| relative.strip_prefix('/'))
555            {
556                return array_arc.pointer(&format!("/{}", relative)).cloned();
557            }
558        }
559
560        let mut subtree = self.evaluated_schema.pointer(schema_prefix)?.clone();
561
562        // Pre-build "prefix/" once for the starts_with check in the loop
563        let prefix_slash = format!("{}/", schema_prefix);
564
565        for (static_key, array_arc) in self.static_arrays.iter() {
566            // Derive the absolute schema path the same way resolve_static_markers_in_value does
567            let schema_path: &str = if static_key.starts_with("/$table") {
568                &static_key["/$table".len()..]
569            } else {
570                static_key.as_str()
571            };
572
573            // Compute the path relative to the subtree root
574            let relative: &str = if schema_path == schema_prefix {
575                // The subtree root itself is the marker — replace the whole subtree
576                ""
577            } else if schema_path.starts_with(&prefix_slash) {
578                // Strip the prefix: remainder is the sub-path within the cloned subtree
579                &schema_path[schema_prefix.len()..]
580            } else {
581                continue; // Not under the requested path — skip
582            };
583
584            if relative.is_empty() {
585                subtree = (**array_arc).clone();
586            } else if let Some(target) = subtree.pointer_mut(relative) {
587                *target = (**array_arc).clone();
588            }
589        }
590
591        Some(subtree)
592    }
593
594    /// Get specific schema value by path, resolving any `$static_array` markers at or
595    /// under that path.
596    pub fn get_schema_value_by_path(&self, path: &str) -> Option<Value> {
597        let pointer_path = path_utils::dot_notation_to_schema_pointer(path);
598        self.resolve_static_markers_at_path(pointer_path.trim_start_matches('#'))
599    }
600
601    /// Get all schema values (data view).
602    ///
603    /// Builds a consumer view from current data and evaluated values without mutating
604    /// evaluator state. Indexed subforms carry active-item wrappers at their root;
605    /// persisting this view would append that wrapper into later form evaluations.
606    pub fn get_schema_value(&mut self) -> Value {
607        self.ensure_layout_resolved();
608        // Start with current authoritative data from eval_data
609        let mut current_data = self.eval_data.data().clone();
610
611        // Ensure it's an object
612        if !current_data.is_object() {
613            current_data = Value::Object(serde_json::Map::new());
614        }
615
616        // Strip $params and $context from data
617        if let Some(obj) = current_data.as_object_mut() {
618            obj.remove("$params");
619            obj.remove("$context");
620        }
621
622        // Prune hidden values from current_data (to remove user input in hidden fields)
623        self.prune_hidden_values(&mut current_data, "");
624
625        // Override data with values from value evaluations
626        // We use value_evaluations which stores the paths of fields with .value
627        for eval_key in self.value_evaluations.iter() {
628            let clean_key = eval_key.strip_prefix('#').unwrap_or(eval_key);
629
630            // Exclude rules.*.value, options.*.value, and $params
631            if clean_key.starts_with("/$params")
632                || (clean_key.ends_with("/value")
633                    && (clean_key.contains("/rules/") || clean_key.contains("/options/")))
634            {
635                continue;
636            }
637
638            let path = clean_key.replace("/properties", "").replace("/value", "");
639
640            // Check if field is effectively hidden
641            // Schema path is clean_key without /value
642            let schema_path = clean_key.strip_suffix("/value").unwrap_or(&clean_key);
643            if self.is_effective_hidden(schema_path) {
644                continue;
645            }
646
647            // Resolve static markers at this specific pointer (handles markers at or under this path)
648            let value = match self.resolve_static_markers_at_path(clean_key) {
649                Some(v) => v,
650                None => continue,
651            };
652
653            // Parse the path and create nested structure as needed
654            let path_parts: Vec<&str> = path.split('/').filter(|s| !s.is_empty()).collect();
655
656            if path_parts.is_empty() {
657                continue;
658            }
659
660            // Navigate/create nested structure
661            let mut current = &mut current_data;
662            for (i, part) in path_parts.iter().enumerate() {
663                let is_last = i == path_parts.len() - 1;
664
665                if is_last {
666                    // Only disabled calculated fields are library-owned. Editable fields
667                    // preserve non-null caller input even when their schema has `$evaluation`.
668                    let schema_value = self.schema.pointer(clean_key);
669                    let computed_value = schema_value
670                        .and_then(Value::as_object)
671                        .is_some_and(|value| value.contains_key("$evaluation"));
672                    let disabled = self
673                        .evaluated_schema
674                        .pointer(schema_path)
675                        .and_then(Value::as_object)
676                        .and_then(|field| field.get("condition"))
677                        .and_then(Value::as_object)
678                        .and_then(|condition| condition.get("disabled"))
679                        .is_some_and(|disabled| disabled == &Value::Bool(true));
680                    let computed_disabled =
681                        computed_value && (disabled || !self.is_mapped_in_any_layout(schema_path));
682                    if let Some(obj) = current.as_object_mut() {
683                        let should_update = computed_disabled
684                            || match obj.get(*part) {
685                                Some(v) => v.is_null(),
686                                None => true,
687                            };
688                        if should_update {
689                            obj.insert(
690                                (*part).to_string(),
691                                crate::utils::clean_float_noise(value.clone()),
692                            );
693                        }
694                    }
695                } else {
696                    // Ensure current is an object, then navigate/create intermediate objects
697                    if let Some(obj) = current.as_object_mut() {
698                        if !obj.contains_key(*part) {
699                            obj.insert((*part).to_string(), Value::Object(serde_json::Map::new()));
700                        }
701
702                        current = obj.get_mut(*part).unwrap();
703                    } else {
704                        // Skip this path if current is not an object and can't be made into one
705                        break;
706                    }
707                }
708            }
709        }
710
711        crate::utils::clean_float_noise(current_data)
712    }
713
714    /// Get all schema values as array of path-value pairs
715    /// Returns [{path: "", value: ""}, ...]
716    ///
717    /// # Returns
718    ///
719    /// Array of objects containing path (dotted notation) and value pairs from value evaluations
720    pub fn get_schema_value_array(&self) -> Value {
721        self.ensure_layout_resolved();
722        let mut result = Vec::new();
723
724        for eval_key in self.value_evaluations.iter() {
725            let clean_key = eval_key.strip_prefix('#').unwrap_or(eval_key);
726
727            // Exclude rules.*.value, options.*.value, and $params
728            if clean_key.starts_with("/$params")
729                || (clean_key.ends_with("/value")
730                    && (clean_key.contains("/rules/") || clean_key.contains("/options/")))
731            {
732                continue;
733            }
734
735            // Check if field is effectively hidden
736            let schema_path = clean_key.strip_suffix("/value").unwrap_or(&clean_key);
737            if self.is_effective_hidden(schema_path) {
738                continue;
739            }
740
741            // Convert JSON pointer to dotted notation
742            let dotted_path = clean_key
743                .replace("/properties", "")
744                .replace("/value", "")
745                .trim_start_matches('/')
746                .replace('/', ".");
747
748            if dotted_path.is_empty() {
749                continue;
750            }
751
752            // Resolve static markers at this specific pointer (handles markers at or under this path)
753            let value = match self.resolve_static_markers_at_path(clean_key) {
754                Some(v) => crate::utils::clean_float_noise(v),
755                None => continue,
756            };
757
758            // Create {path, value} object
759            let mut item = serde_json::Map::new();
760            item.insert("path".to_string(), Value::String(dotted_path));
761            item.insert("value".to_string(), value);
762            result.push(Value::Object(item));
763        }
764
765        Value::Array(result)
766    }
767
768    /// Get all schema values as object with dotted path keys
769    /// Returns {path: value, ...}
770    ///
771    /// # Returns
772    ///
773    /// Flat object with dotted notation paths as keys and evaluated values
774    pub fn get_schema_value_object(&self) -> Value {
775        self.ensure_layout_resolved();
776        let mut result = serde_json::Map::new();
777
778        for eval_key in self.value_evaluations.iter() {
779            let clean_key = eval_key.strip_prefix('#').unwrap_or(eval_key);
780
781            // Exclude rules.*.value, options.*.value, and $params
782            if clean_key.starts_with("/$params")
783                || (clean_key.ends_with("/value")
784                    && (clean_key.contains("/rules/") || clean_key.contains("/options/")))
785            {
786                continue;
787            }
788
789            // Check if field is effectively hidden
790            let schema_path = clean_key.strip_suffix("/value").unwrap_or(&clean_key);
791            if self.is_effective_hidden(schema_path) {
792                continue;
793            }
794
795            // Convert JSON pointer to dotted notation
796            let dotted_path = clean_key
797                .replace("/properties", "")
798                .replace("/value", "")
799                .trim_start_matches('/')
800                .replace('/', ".");
801
802            if dotted_path.is_empty() {
803                continue;
804            }
805
806            // Resolve static markers at this specific pointer (handles markers at or under this path)
807            let value = match self.resolve_static_markers_at_path(clean_key) {
808                Some(v) => crate::utils::clean_float_noise(v),
809                None => continue,
810            };
811
812            result.insert(dotted_path, value);
813        }
814
815        Value::Object(result)
816    }
817
818    /// Get evaluated schema without $params
819    pub fn get_evaluated_schema_without_params(&mut self) -> Value {
820        time_block!("get_evaluated_schema_without_params()", {
821            let mut schema = if let Value::Object(map) = &self.evaluated_schema {
822                let mut filtered = serde_json::Map::with_capacity(map.len().saturating_sub(1));
823                for (k, v) in map {
824                    if k != "$params" {
825                        filtered.insert(k.clone(), v.clone());
826                    }
827                }
828                Value::Object(filtered)
829            } else {
830                self.evaluated_schema.clone()
831            };
832            self.resolve_static_markers_in_value(&mut schema);
833            schema
834        })
835    }
836
837    fn process_params_static_arrays(&self, params_val: &Value, with_static_array: bool) -> Value {
838        let mut params = params_val.clone();
839        if with_static_array {
840            for (static_key, array_arc) in self.static_arrays.iter() {
841                let rel_path = if let Some(path) = static_key.strip_prefix("/$params") {
842                    path
843                } else if let Some(path) = static_key.strip_prefix("/$table/$params") {
844                    path
845                } else {
846                    continue;
847                };
848
849                if let Some(target) = params.pointer_mut(rel_path) {
850                    *target = (**array_arc).clone();
851                }
852            }
853        } else {
854            Self::strip_static_array_markers(&mut params);
855        }
856        params
857    }
858
859    fn strip_static_array_markers(val: &mut Value) {
860        match val {
861            Value::Object(map) => {
862                map.retain(|_, v| {
863                    if let Value::Object(child_map) = v {
864                        !child_map.contains_key("$static_array")
865                    } else {
866                        true
867                    }
868                });
869                for v in map.values_mut() {
870                    Self::strip_static_array_markers(v);
871                }
872            }
873            Value::Array(arr) => {
874                arr.retain(|v| {
875                    if let Value::Object(child_map) = v {
876                        !child_map.contains_key("$static_array")
877                    } else {
878                        true
879                    }
880                });
881                for v in arr.iter_mut() {
882                    Self::strip_static_array_markers(v);
883                }
884            }
885            _ => {}
886        }
887    }
888
889    /// Get plain `$params` from the original schema (without static array data).
890    pub fn get_plain_params(&self) -> Option<Value> {
891        let raw_params = self.schema.get("$params")?;
892        Some(self.process_params_static_arrays(raw_params, false))
893    }
894
895    /// Get evaluated `$params` from `evaluated_schema`.
896    ///
897    /// # Arguments
898    /// * `with_static_array` - If true, static arrays extracted to `static_arrays` are resolved
899    ///   back into `$params`. If false, static array keys are omitted/stripped.
900    pub fn get_evaluated_params(&mut self, with_static_array: bool) -> Option<Value> {
901        let raw_params = self.evaluated_schema.get("$params")?;
902        Some(self.process_params_static_arrays(raw_params, with_static_array))
903    }
904
905    /// Get evaluated schema as MessagePack bytes (compact, without $layout resolution)
906    pub fn get_evaluated_schema_msgpack(&mut self) -> Result<Vec<u8>, String> {
907        let schema = self.get_evaluated_schema();
908        rmp_serde::to_vec(&schema).map_err(|e| format!("MessagePack serialization failed: {}", e))
909    }
910
911    /// Get layout-resolved evaluated schema as MessagePack bytes.
912    ///
913    /// Reuses `get_evaluated_schema_resolved`, which omits `$params` and merges
914    /// resolved `$layout` overlays.
915    pub fn get_evaluated_schema_resolved_msgpack(&mut self) -> Result<Vec<u8>, String> {
916        let schema = self.get_evaluated_schema_resolved();
917        rmp_serde::to_vec(&schema).map_err(|e| format!("MessagePack serialization failed: {}", e))
918    }
919
920    /// Get value from evaluated schema by path
921    pub fn get_evaluated_schema_by_path(&mut self, path: &str) -> Option<Value> {
922        self.get_schema_value_by_path(path)
923    }
924
925    /// Get evaluated schema parts by multiple paths
926    pub fn get_evaluated_schema_by_paths(
927        &mut self,
928        paths: &[String],
929        format: Option<ReturnFormat>,
930    ) -> Value {
931        match format.unwrap_or(ReturnFormat::Nested) {
932            ReturnFormat::Nested => {
933                let mut result = Value::Object(serde_json::Map::new());
934                for path in paths {
935                    if let Some(val) = self.get_schema_value_by_path(path) {
936                        // Insert into result object at proper path nesting
937                        Self::insert_at_path(&mut result, path, val);
938                    }
939                }
940                result
941            }
942            ReturnFormat::Flat => {
943                let mut result = serde_json::Map::new();
944                for path in paths {
945                    if let Some(val) = self.get_schema_value_by_path(path) {
946                        result.insert(path.clone(), val);
947                    }
948                }
949                Value::Object(result)
950            }
951            ReturnFormat::Array => {
952                let mut result = Vec::new();
953                for path in paths {
954                    if let Some(val) = self.get_schema_value_by_path(path) {
955                        result.push(val);
956                    } else {
957                        result.push(Value::Null);
958                    }
959                }
960                Value::Array(result)
961            }
962        }
963    }
964
965    /// Get original (unevaluated) schema by path
966    pub fn get_schema_by_path(&self, path: &str) -> Option<Value> {
967        let pointer_path = path_utils::dot_notation_to_schema_pointer(path);
968        self.schema
969            .pointer(&pointer_path.trim_start_matches('#'))
970            .cloned()
971    }
972
973    /// Get original schema by multiple paths
974    pub fn get_schema_by_paths(&self, paths: &[String], format: Option<ReturnFormat>) -> Value {
975        match format.unwrap_or(ReturnFormat::Nested) {
976            ReturnFormat::Nested => {
977                let mut result = Value::Object(serde_json::Map::new());
978                for path in paths {
979                    if let Some(val) = self.get_schema_by_path(path) {
980                        Self::insert_at_path(&mut result, path, val);
981                    }
982                }
983                result
984            }
985            ReturnFormat::Flat => {
986                let mut result = serde_json::Map::new();
987                for path in paths {
988                    if let Some(val) = self.get_schema_by_path(path) {
989                        result.insert(path.clone(), val);
990                    }
991                }
992                Value::Object(result)
993            }
994            ReturnFormat::Array => {
995                let mut result = Vec::new();
996                for path in paths {
997                    if let Some(val) = self.get_schema_by_path(path) {
998                        result.push(val);
999                    } else {
1000                        result.push(Value::Null);
1001                    }
1002                }
1003                Value::Array(result)
1004            }
1005        }
1006    }
1007
1008    /// Helper to insert value into nested object at dotted path
1009    pub(crate) fn insert_at_path(root: &mut Value, path: &str, value: Value) {
1010        let parts: Vec<&str> = path.split('.').collect();
1011        let mut current = root;
1012
1013        for (i, part) in parts.iter().enumerate() {
1014            if i == parts.len() - 1 {
1015                // Last part - set value
1016                if let Value::Object(map) = current {
1017                    map.insert(part.to_string(), value);
1018                    return; // Done
1019                }
1020            } else {
1021                // Intermediate part - traverse or create
1022                // We need to temporarily take the value or use raw pointer manipulation?
1023                // serde_json pointer is read-only or requires mutable reference
1024
1025                if !current.is_object() {
1026                    *current = Value::Object(serde_json::Map::new());
1027                }
1028
1029                if let Value::Object(map) = current {
1030                    if !map.contains_key(*part) {
1031                        map.insert(part.to_string(), Value::Object(serde_json::Map::new()));
1032                    }
1033                    current = map.get_mut(*part).unwrap();
1034                }
1035            }
1036        }
1037    }
1038
1039    /// Flatten a nested object key-value pair to dotted keys
1040    pub fn flatten_object(
1041        prefix: &str,
1042        value: &Value,
1043        result: &mut serde_json::Map<String, Value>,
1044    ) {
1045        match value {
1046            Value::Object(map) => {
1047                for (k, v) in map {
1048                    let new_key = if prefix.is_empty() {
1049                        k.clone()
1050                    } else {
1051                        format!("{}.{}", prefix, k)
1052                    };
1053                    Self::flatten_object(&new_key, v, result);
1054                }
1055            }
1056            _ => {
1057                result.insert(prefix.to_string(), value.clone());
1058            }
1059        }
1060    }
1061
1062    pub fn convert_to_format(value: Value, format: ReturnFormat) -> Value {
1063        match format {
1064            ReturnFormat::Nested => value,
1065            ReturnFormat::Flat => {
1066                let mut result = serde_json::Map::new();
1067                Self::flatten_object("", &value, &mut result);
1068                Value::Object(result)
1069            }
1070            ReturnFormat::Array => {
1071                if let Value::Object(map) = value {
1072                    Value::Array(map.values().cloned().collect())
1073                } else if let Value::Array(arr) = value {
1074                    Value::Array(arr)
1075                } else {
1076                    Value::Array(vec![value])
1077                }
1078            }
1079        }
1080    }
1081
1082    /// Evaluate and return the options for a specific field on demand.
1083    ///
1084    /// Accepts dotted notation (`form.occupation`), JSON pointer
1085    /// (`/properties/form/properties/occupation`), or schema ref
1086    /// (`#/properties/form/properties/occupation`).
1087    ///
1088    /// Returns `None` when the field does not have an `options` key.
1089    /// Returns the resolved options value (array, URL string, or null) otherwise.
1090    pub fn get_field_options(&mut self, field_path: &str) -> Option<Value> {
1091        // Normalize the input to a schema pointer (e.g. #/properties/form/properties/occupation)
1092        let schema_ptr = if field_path.starts_with('#') || field_path.starts_with('/') {
1093            path_utils::normalize_to_json_pointer(field_path).into_owned()
1094        } else {
1095            path_utils::dot_notation_to_schema_pointer(field_path)
1096        };
1097
1098        // Build the JSON pointer path to the /options node (strip leading # for serde pointer())
1099        let options_schema_key = format!("{}/options", schema_ptr);
1100        let options_pointer =
1101            path_utils::normalize_to_json_pointer(&options_schema_key).into_owned();
1102
1103        // Check if the options node exists in the evaluated schema
1104        let options_node = self.evaluated_schema.pointer(&options_pointer)?.clone();
1105
1106        // If the options node is an object with $evaluation, evaluate it now (deferred)
1107        if let Value::Object(ref map) = options_node {
1108            if map.contains_key("$evaluation") {
1109                let eval_key = options_schema_key.clone();
1110
1111                if let Some(logic_id) = self.evaluations.get(&eval_key).copied() {
1112                    let snap = self.eval_data.snapshot_data();
1113                    if let Ok(result) = self.engine.run(&logic_id, &*snap) {
1114                        let cleaned = clean_float_noise_scalar(result);
1115                        if let Some(node) = self.evaluated_schema.pointer_mut(&options_pointer) {
1116                            *node = cleaned.clone();
1117                        }
1118                        return Some(cleaned);
1119                    }
1120                }
1121                // No compiled logic found — options cannot be resolved
1122                return None;
1123            }
1124        }
1125
1126        // Check options_templates for a URL template at this field's options/url path
1127        let url_pointer =
1128            path_utils::normalize_to_json_pointer(&format!("{}/options/url", schema_ptr))
1129                .into_owned();
1130
1131        let templates = self.options_templates.clone();
1132        for (tmpl_url_path, tmpl_str, tmpl_params_path) in templates.iter() {
1133            if *tmpl_url_path == url_pointer {
1134                if let Some(params) = self.evaluated_schema.pointer(tmpl_params_path) {
1135                    let params = params.clone();
1136                    if let Ok(resolved_url) = self.evaluate_template(tmpl_str, &params) {
1137                        if let Some(target) = self.evaluated_schema.pointer_mut(&url_pointer) {
1138                            *target = Value::String(resolved_url);
1139                        }
1140                        return self.evaluated_schema.pointer(&options_pointer).cloned();
1141                    }
1142                }
1143                break;
1144            }
1145        }
1146
1147        // Static options (already-evaluated array or plain value)
1148        Some(options_node)
1149    }
1150}