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        let mut schema = self.get_evaluated_schema();
821        if let Value::Object(ref mut map) = schema {
822            map.remove("$params");
823        }
824        schema
825    }
826
827    /// Get evaluated schema as MessagePack bytes (compact, without $layout resolution)
828    pub fn get_evaluated_schema_msgpack(&mut self) -> Result<Vec<u8>, String> {
829        let schema = self.get_evaluated_schema();
830        rmp_serde::to_vec(&schema).map_err(|e| format!("MessagePack serialization failed: {}", e))
831    }
832
833    /// Get layout-resolved evaluated schema as MessagePack bytes.
834    ///
835    /// Reuses `get_evaluated_schema_resolved`, which omits `$params` and merges
836    /// resolved `$layout` overlays.
837    pub fn get_evaluated_schema_resolved_msgpack(&mut self) -> Result<Vec<u8>, String> {
838        let schema = self.get_evaluated_schema_resolved();
839        rmp_serde::to_vec(&schema).map_err(|e| format!("MessagePack serialization failed: {}", e))
840    }
841
842    /// Get value from evaluated schema by path
843    pub fn get_evaluated_schema_by_path(&mut self, path: &str) -> Option<Value> {
844        self.get_schema_value_by_path(path)
845    }
846
847    /// Get evaluated schema parts by multiple paths
848    pub fn get_evaluated_schema_by_paths(
849        &mut self,
850        paths: &[String],
851        format: Option<ReturnFormat>,
852    ) -> Value {
853        match format.unwrap_or(ReturnFormat::Nested) {
854            ReturnFormat::Nested => {
855                let mut result = Value::Object(serde_json::Map::new());
856                for path in paths {
857                    if let Some(val) = self.get_schema_value_by_path(path) {
858                        // Insert into result object at proper path nesting
859                        Self::insert_at_path(&mut result, path, val);
860                    }
861                }
862                result
863            }
864            ReturnFormat::Flat => {
865                let mut result = serde_json::Map::new();
866                for path in paths {
867                    if let Some(val) = self.get_schema_value_by_path(path) {
868                        result.insert(path.clone(), val);
869                    }
870                }
871                Value::Object(result)
872            }
873            ReturnFormat::Array => {
874                let mut result = Vec::new();
875                for path in paths {
876                    if let Some(val) = self.get_schema_value_by_path(path) {
877                        result.push(val);
878                    } else {
879                        result.push(Value::Null);
880                    }
881                }
882                Value::Array(result)
883            }
884        }
885    }
886
887    /// Get original (unevaluated) schema by path
888    pub fn get_schema_by_path(&self, path: &str) -> Option<Value> {
889        let pointer_path = path_utils::dot_notation_to_schema_pointer(path);
890        self.schema
891            .pointer(&pointer_path.trim_start_matches('#'))
892            .cloned()
893    }
894
895    /// Get original schema by multiple paths
896    pub fn get_schema_by_paths(&self, paths: &[String], format: Option<ReturnFormat>) -> Value {
897        match format.unwrap_or(ReturnFormat::Nested) {
898            ReturnFormat::Nested => {
899                let mut result = Value::Object(serde_json::Map::new());
900                for path in paths {
901                    if let Some(val) = self.get_schema_by_path(path) {
902                        Self::insert_at_path(&mut result, path, val);
903                    }
904                }
905                result
906            }
907            ReturnFormat::Flat => {
908                let mut result = serde_json::Map::new();
909                for path in paths {
910                    if let Some(val) = self.get_schema_by_path(path) {
911                        result.insert(path.clone(), val);
912                    }
913                }
914                Value::Object(result)
915            }
916            ReturnFormat::Array => {
917                let mut result = Vec::new();
918                for path in paths {
919                    if let Some(val) = self.get_schema_by_path(path) {
920                        result.push(val);
921                    } else {
922                        result.push(Value::Null);
923                    }
924                }
925                Value::Array(result)
926            }
927        }
928    }
929
930    /// Helper to insert value into nested object at dotted path
931    pub(crate) fn insert_at_path(root: &mut Value, path: &str, value: Value) {
932        let parts: Vec<&str> = path.split('.').collect();
933        let mut current = root;
934
935        for (i, part) in parts.iter().enumerate() {
936            if i == parts.len() - 1 {
937                // Last part - set value
938                if let Value::Object(map) = current {
939                    map.insert(part.to_string(), value);
940                    return; // Done
941                }
942            } else {
943                // Intermediate part - traverse or create
944                // We need to temporarily take the value or use raw pointer manipulation?
945                // serde_json pointer is read-only or requires mutable reference
946
947                if !current.is_object() {
948                    *current = Value::Object(serde_json::Map::new());
949                }
950
951                if let Value::Object(map) = current {
952                    if !map.contains_key(*part) {
953                        map.insert(part.to_string(), Value::Object(serde_json::Map::new()));
954                    }
955                    current = map.get_mut(*part).unwrap();
956                }
957            }
958        }
959    }
960
961    /// Flatten a nested object key-value pair to dotted keys
962    pub fn flatten_object(
963        prefix: &str,
964        value: &Value,
965        result: &mut serde_json::Map<String, Value>,
966    ) {
967        match value {
968            Value::Object(map) => {
969                for (k, v) in map {
970                    let new_key = if prefix.is_empty() {
971                        k.clone()
972                    } else {
973                        format!("{}.{}", prefix, k)
974                    };
975                    Self::flatten_object(&new_key, v, result);
976                }
977            }
978            _ => {
979                result.insert(prefix.to_string(), value.clone());
980            }
981        }
982    }
983
984    pub fn convert_to_format(value: Value, format: ReturnFormat) -> Value {
985        match format {
986            ReturnFormat::Nested => value,
987            ReturnFormat::Flat => {
988                let mut result = serde_json::Map::new();
989                Self::flatten_object("", &value, &mut result);
990                Value::Object(result)
991            }
992            ReturnFormat::Array => {
993                if let Value::Object(map) = value {
994                    Value::Array(map.values().cloned().collect())
995                } else if let Value::Array(arr) = value {
996                    Value::Array(arr)
997                } else {
998                    Value::Array(vec![value])
999                }
1000            }
1001        }
1002    }
1003
1004    /// Evaluate and return the options for a specific field on demand.
1005    ///
1006    /// Accepts dotted notation (`form.occupation`), JSON pointer
1007    /// (`/properties/form/properties/occupation`), or schema ref
1008    /// (`#/properties/form/properties/occupation`).
1009    ///
1010    /// Returns `None` when the field does not have an `options` key.
1011    /// Returns the resolved options value (array, URL string, or null) otherwise.
1012    pub fn get_field_options(&mut self, field_path: &str) -> Option<Value> {
1013        // Normalize the input to a schema pointer (e.g. #/properties/form/properties/occupation)
1014        let schema_ptr = if field_path.starts_with('#') || field_path.starts_with('/') {
1015            path_utils::normalize_to_json_pointer(field_path).into_owned()
1016        } else {
1017            path_utils::dot_notation_to_schema_pointer(field_path)
1018        };
1019
1020        // Build the JSON pointer path to the /options node (strip leading # for serde pointer())
1021        let options_schema_key = format!("{}/options", schema_ptr);
1022        let options_pointer =
1023            path_utils::normalize_to_json_pointer(&options_schema_key).into_owned();
1024
1025        // Check if the options node exists in the evaluated schema
1026        let options_node = self.evaluated_schema.pointer(&options_pointer)?.clone();
1027
1028        // If the options node is an object with $evaluation, evaluate it now (deferred)
1029        if let Value::Object(ref map) = options_node {
1030            if map.contains_key("$evaluation") {
1031                let eval_key = options_schema_key.clone();
1032
1033                if let Some(logic_id) = self.evaluations.get(&eval_key).copied() {
1034                    let snap = self.eval_data.snapshot_data();
1035                    if let Ok(result) = self.engine.run(&logic_id, &*snap) {
1036                        let cleaned = clean_float_noise_scalar(result);
1037                        if let Some(node) = self.evaluated_schema.pointer_mut(&options_pointer) {
1038                            *node = cleaned.clone();
1039                        }
1040                        return Some(cleaned);
1041                    }
1042                }
1043                // No compiled logic found — options cannot be resolved
1044                return None;
1045            }
1046        }
1047
1048        // Check options_templates for a URL template at this field's options/url path
1049        let url_pointer =
1050            path_utils::normalize_to_json_pointer(&format!("{}/options/url", schema_ptr))
1051                .into_owned();
1052
1053        let templates = self.options_templates.clone();
1054        for (tmpl_url_path, tmpl_str, tmpl_params_path) in templates.iter() {
1055            if *tmpl_url_path == url_pointer {
1056                if let Some(params) = self.evaluated_schema.pointer(tmpl_params_path) {
1057                    let params = params.clone();
1058                    if let Ok(resolved_url) = self.evaluate_template(tmpl_str, &params) {
1059                        if let Some(target) = self.evaluated_schema.pointer_mut(&url_pointer) {
1060                            *target = Value::String(resolved_url);
1061                        }
1062                        return self.evaluated_schema.pointer(&options_pointer).cloned();
1063                    }
1064                }
1065                break;
1066            }
1067        }
1068
1069        // Static options (already-evaluated array or plain value)
1070        Some(options_node)
1071    }
1072}