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;
7use std::sync::Arc;
8
9impl JSONEval {
10    /// Check if a field is effectively hidden by checking its condition and all parents
11    /// Also checks for $layout.hideLayout.all on parents
12    pub(crate) fn is_effective_hidden(&self, schema_pointer: &str) -> bool {
13        let schema_pointer = schema_pointer.trim_start_matches('#');
14        if self.layout_hidden_refs.iter().any(|hidden_ref| {
15            schema_pointer == hidden_ref
16                || schema_pointer
17                    .strip_prefix(hidden_ref)
18                    .is_some_and(|suffix| {
19                        suffix.starts_with("/properties/") || suffix.starts_with("/items/")
20                    })
21        }) {
22            return true;
23        }
24
25        let mut end = schema_pointer.len();
26
27        loop {
28            let current_path = &schema_pointer[..end];
29
30            if let Some(schema_node) = self.evaluated_schema.pointer(current_path) {
31                if let Value::Object(map) = schema_node {
32                    if let Some(Value::Object(condition)) = map.get("condition") {
33                        if let Some(Value::Bool(true)) = condition.get("hidden") {
34                            return true;
35                        }
36                    }
37
38                    if let Some(Value::Object(layout)) = map.get("$layout") {
39                        if let Some(Value::Object(hide_layout)) = layout.get("hideLayout") {
40                            if let Some(Value::Bool(true)) = hide_layout.get("all") {
41                                return true;
42                            }
43                        }
44                    }
45                }
46            }
47
48            if end == 0 {
49                break;
50            }
51
52            // Move to parent: find last '/' and strip /properties or /items suffixes
53            match schema_pointer[..end].rfind('/') {
54                Some(0) | None => {
55                    end = 0;
56                }
57                Some(last_slash) => {
58                    end = last_slash;
59                    let parent = &schema_pointer[..end];
60                    if parent.ends_with("/properties") {
61                        end -= "/properties".len();
62                    } else if parent.ends_with("/items") {
63                        end -= "/items".len();
64                    }
65                }
66            }
67        }
68
69        false
70    }
71
72    /// Prune hidden values from data object recursively
73    fn prune_hidden_values(&self, data: &mut Value, current_path: &str) {
74        if let Value::Object(map) = data {
75            // Collect keys to remove to avoid borrow checker issues
76            let mut keys_to_remove = Vec::new();
77
78            for (key, value) in map.iter_mut() {
79                // Skip special keys
80                if key == "$params" || key == "$context" {
81                    continue;
82                }
83
84                // Construct schema path for this key
85                // For root fields: /properties/key
86                // For nested fields: current_path/properties/key
87                let schema_path = if current_path.is_empty() {
88                    format!("/properties/{}", key)
89                } else {
90                    format!("{}/properties/{}", current_path, key)
91                };
92
93                // Check if hidden
94                if self.is_effective_hidden(&schema_path) {
95                    keys_to_remove.push(key.clone());
96                } else {
97                    // Recurse if object
98                    if value.is_object() {
99                        self.prune_hidden_values(value, &schema_path);
100                    }
101                }
102            }
103
104            // Remove hidden keys
105            for key in keys_to_remove {
106                map.remove(&key);
107            }
108        }
109    }
110
111    /// Replace any `{"$static_array": "/$table/..."}` and `{"$static_array": "/$params/..."}` markers in `schema_output`
112    /// with the actual evaluated array data from `eval_data`.
113    ///
114    /// By iterating only over tracked `static_arrays`, we replace markers in O(markers) time
115    /// instead of requiring an expensive O(schema_nodes) recursive tree walk.
116    fn resolve_static_markers_in_value(&self, schema_output: &mut Value) {
117        for (static_key, array_arc) in self.static_arrays.iter() {
118            // Determine the schema pointer path where this marker was placed
119            let schema_path = if static_key.starts_with("/$table") {
120                &static_key["/$table".len()..] // e.g. /properties/product_benefit/...
121            } else {
122                static_key.as_str() // e.g. /$params/references/...
123            };
124
125            // Only attempt replacement if the exact path exists in the cloned schema output
126            if let Some(target_val) = schema_output.pointer_mut(schema_path) {
127                // The actual evaluated array is seamlessly stored right in the map's value
128                *target_val = (**array_arc).clone();
129            }
130        }
131    }
132
133    /// Get the evaluated schema (compact — $ref intact, no layout expansion).
134    ///
135    /// # Returns
136    ///
137    /// The evaluated schema as a JSON value, with all `$static_array` markers resolved
138    /// to their actual evaluated data.
139    pub fn get_evaluated_schema(&mut self) -> Value {
140        time_block!("get_evaluated_schema()", {
141            let mut schema = self.evaluated_schema.clone();
142            self.resolve_static_markers_in_value(&mut schema);
143            schema
144        })
145    }
146
147    /// Get layout overlay entries — the delta properties per layout element.
148    /// Consumer merges these into compact schema to get fully resolved layout.
149    pub fn get_resolved_layout(&mut self) -> ResolvedLayoutResult {
150        time_block!("get_resolved_layout()", {
151            // Check cache
152            if let Some(ref cached) = self.resolved_layout_cache {
153                return cached.as_ref().clone();
154            }
155            // Resolve and cache
156            let result = match self.resolve_layout(false) {
157                Ok(entries) => entries,
158                Err(e) => {
159                    eprintln!("Warning: Layout resolution failed: {}", e);
160                    Vec::new()
161                }
162            };
163            self.resolved_layout_cache = Some(Arc::new(result.clone()));
164            result
165        })
166    }
167
168    /// Get evaluated schema with layout overlays already applied.
169    /// Convenience: returns compact schema + overlays merged.
170    ///
171    /// Two-pass approach to handle nested elements:
172    /// 1. First pass: resolve $ref and apply overlay for entries whose target
173    ///    path exists in the compact schema (top-level elements).
174    /// 2. Second pass: apply overlay-only for entries whose path appears
175    ///    after parent $ref resolution (nested elements).
176    pub fn get_evaluated_schema_resolved(&mut self) -> Value {
177        time_block!("get_evaluated_schema_resolved()", {
178            let mut schema = self.get_evaluated_schema_without_params();
179            let overlays = self.get_resolved_layout();
180
181            struct ResolveEntry {
182                layout_path: String,
183                element_idx: usize,
184                overlay: indexmap::IndexMap<String, Value>,
185            }
186
187            let mut entries: Vec<ResolveEntry> = overlays
188                .iter()
189                .map(|entry| {
190                    let layout_path =
191                        path_utils::normalize_to_json_pointer(&entry.layout_path).into_owned();
192                    ResolveEntry {
193                        layout_path,
194                        element_idx: entry.element_idx,
195                        overlay: entry.overlay.clone(),
196                    }
197                })
198                .collect();
199            drop(overlays);
200
201            // Sort entries shallow-first so parent elements are expanded before their children.
202            // Child entries (e.g. layout_path = ".../elements/1/elements") depend on the parent
203            // ("…/elements") being resolved first so the nested `elements` array exists in `schema`.
204            entries.sort_by(|a, b| {
205                let depth_a = a.layout_path.matches('/').count();
206                let depth_b = b.layout_path.matches('/').count();
207                depth_a
208                    .cmp(&depth_b)
209                    .then_with(|| a.element_idx.cmp(&b.element_idx))
210            });
211
212            // ── Phase 2 (mutable): resolve $ref + apply overlays (parent-first order) ──
213            // Entries are sorted shallowest layout_path first, so parent elements are
214            // expanded before any child entries that path through them.
215            for entry in entries {
216                // Resolve $ref from the current (already partially mutated) schema so that
217                // parent expansions are visible when we process child entries.
218                let resolved_value: Option<Value> = (|| -> Option<Value> {
219                    let arr = schema.pointer(&entry.layout_path)?.as_array()?;
220                    let element = arr.get(entry.element_idx)?;
221                    let ref_str = element.get("$ref")?.as_str()?;
222
223                    let ref_pointer = if ref_str.starts_with('#') || ref_str.starts_with('/') {
224                        path_utils::normalize_to_json_pointer(ref_str).into_owned()
225                    } else {
226                        let schema_pointer = path_utils::dot_notation_to_schema_pointer(ref_str);
227                        let normalized =
228                            path_utils::normalize_to_json_pointer(&schema_pointer).into_owned();
229                        if schema.pointer(&normalized).is_some() {
230                            normalized
231                        } else {
232                            format!("/properties/{}", ref_str.replace('.', "/properties/"))
233                        }
234                    };
235
236                    let mut resolved = schema.pointer(&ref_pointer)?.clone();
237
238                    // Flatten $layout into top level
239                    if let Value::Object(ref mut resolved_map) = resolved {
240                        if let Some(Value::Object(layout_obj)) = resolved_map.remove("$layout") {
241                            let mut result = layout_obj;
242                            for (key, value) in resolved_map.clone().into_iter() {
243                                if key != "type" || !result.contains_key("type") {
244                                    result.insert(key, value);
245                                }
246                            }
247                            resolved = Value::Object(result);
248                        }
249                    }
250
251                    Some(resolved)
252                })();
253
254                if let Some(Value::Array(arr)) = schema.pointer_mut(&entry.layout_path) {
255                    if entry.element_idx < arr.len() {
256                        let element = &mut arr[entry.element_idx];
257
258                        // Apply $ref resolution
259                        if let Some(resolved) = resolved_value {
260                            if let Value::Object(mut resolved_map) = resolved {
261                                if let Value::Object(mut map) = element.take() {
262                                    map.remove("$ref");
263                                    for (key, value) in map {
264                                        resolved_map.insert(key, value);
265                                    }
266                                }
267                                *element = Value::Object(resolved_map);
268                            } else {
269                                *element = resolved;
270                            }
271                        }
272
273                        // Apply overlay on top
274                        if let Value::Object(ref mut map) = element {
275                            for (k, v) in &entry.overlay {
276                                map.insert(k.clone(), v.clone());
277                            }
278                        }
279                    }
280                }
281            }
282
283            Self::stamp_property_metadata(&mut schema);
284            schema
285        })
286    }
287
288    /// Stamp every schema property with raw pointer-style dotted metadata.
289    fn stamp_property_metadata(schema: &mut Value) {
290        fn walk(value: &mut Value, path: &str, parent_hidden: bool) {
291            let Some(map) = value.as_object_mut() else {
292                return;
293            };
294
295            let hidden = parent_hidden
296                || map
297                    .get("condition")
298                    .and_then(Value::as_object)
299                    .and_then(|condition| condition.get("hidden"))
300                    .is_some_and(|hidden| hidden == &Value::Bool(true));
301
302            if let Some(Value::Object(properties)) = map.get_mut("properties") {
303                for (name, property) in properties {
304                    let property_path = if path.is_empty() {
305                        format!("properties.{}", name)
306                    } else {
307                        format!("{}.properties.{}", path, name)
308                    };
309                    if let Value::Object(property_map) = property {
310                        property_map.insert(
311                            "$fullpath".to_string(),
312                            Value::String(property_path.clone()),
313                        );
314                        property_map.insert("$path".to_string(), Value::String(name.clone()));
315                        property_map.insert("$parentHide".to_string(), Value::Bool(hidden));
316                    }
317                    walk(property, &property_path, hidden);
318                }
319            }
320
321            for (name, child) in map {
322                if name != "properties" && !name.starts_with('$') && child.is_object() {
323                    let child_path = if path.is_empty() {
324                        name.clone()
325                    } else {
326                        format!("{}.{}", path, name)
327                    };
328                    walk(child, &child_path, hidden);
329                }
330            }
331        }
332
333        walk(schema, "", false);
334    }
335
336    /// Resolve `$static_array` markers within the subtree rooted at `schema_prefix`.
337    ///
338    /// Clones only the node at `schema_prefix` from `evaluated_schema`, then iterates
339    /// the tracked `static_arrays` list filtering to entries whose schema path is at or
340    /// under `schema_prefix`. Only those markers are replaced inside the cloned subtree;
341    /// unrelated entries are skipped entirely.
342    ///
343    /// # Examples
344    /// - `schema_prefix = "/$params/references"` → resolves only arrays nested under that key
345    /// - `schema_prefix = "/properties/foo/value"` → resolves a single marker if the field itself is one
346    fn resolve_static_markers_at_path(&self, schema_prefix: &str) -> Option<Value> {
347        let mut subtree = self.evaluated_schema.pointer(schema_prefix)?.clone();
348
349        // Pre-build "prefix/" once for the starts_with check in the loop
350        let prefix_slash = format!("{}/", schema_prefix);
351
352        for (static_key, array_arc) in self.static_arrays.iter() {
353            // Derive the absolute schema path the same way resolve_static_markers_in_value does
354            let schema_path: &str = if static_key.starts_with("/$table") {
355                &static_key["/$table".len()..]
356            } else {
357                static_key.as_str()
358            };
359
360            // Compute the path relative to the subtree root
361            let relative: &str = if schema_path == schema_prefix {
362                // The subtree root itself is the marker — replace the whole subtree
363                ""
364            } else if schema_path.starts_with(&prefix_slash) {
365                // Strip the prefix: remainder is the sub-path within the cloned subtree
366                &schema_path[schema_prefix.len()..]
367            } else {
368                continue; // Not under the requested path — skip
369            };
370
371            if relative.is_empty() {
372                subtree = (**array_arc).clone();
373            } else if let Some(target) = subtree.pointer_mut(relative) {
374                *target = (**array_arc).clone();
375            }
376        }
377
378        Some(subtree)
379    }
380
381    /// Get specific schema value by path, resolving any `$static_array` markers at or
382    /// under that path.
383    pub fn get_schema_value_by_path(&self, path: &str) -> Option<Value> {
384        let pointer_path = path_utils::dot_notation_to_schema_pointer(path);
385        self.resolve_static_markers_at_path(pointer_path.trim_start_matches('#'))
386    }
387
388    /// Get all schema values (data view)
389    /// Mutates internal data state by overriding with values from value evaluations
390    /// This corresponds to subform.get_schema_value() usage
391    pub fn get_schema_value(&mut self) -> Value {
392        // Start with current authoritative data from eval_data
393        let mut current_data = self.eval_data.data().clone();
394
395        // Ensure it's an object
396        if !current_data.is_object() {
397            current_data = Value::Object(serde_json::Map::new());
398        }
399
400        // Strip $params and $context from data
401        if let Some(obj) = current_data.as_object_mut() {
402            obj.remove("$params");
403            obj.remove("$context");
404        }
405
406        // Prune hidden values from current_data (to remove user input in hidden fields)
407        self.prune_hidden_values(&mut current_data, "");
408
409        // Override data with values from value evaluations
410        // We use value_evaluations which stores the paths of fields with .value
411        for eval_key in self.value_evaluations.iter() {
412            let clean_key = eval_key.strip_prefix('#').unwrap_or(eval_key);
413
414            // Exclude rules.*.value, options.*.value, and $params
415            if clean_key.starts_with("/$params")
416                || (clean_key.ends_with("/value")
417                    && (clean_key.contains("/rules/") || clean_key.contains("/options/")))
418            {
419                continue;
420            }
421
422            let path = clean_key.replace("/properties", "").replace("/value", "");
423
424            // Check if field is effectively hidden
425            // Schema path is clean_key without /value
426            let schema_path = clean_key.strip_suffix("/value").unwrap_or(&clean_key);
427            if self.is_effective_hidden(schema_path) {
428                continue;
429            }
430
431            // Resolve static markers at this specific pointer (handles markers at or under this path)
432            let value = match self.resolve_static_markers_at_path(clean_key) {
433                Some(v) => v,
434                None => continue,
435            };
436
437            // Parse the path and create nested structure as needed
438            let path_parts: Vec<&str> = path.split('/').filter(|s| !s.is_empty()).collect();
439
440            if path_parts.is_empty() {
441                continue;
442            }
443
444            // Navigate/create nested structure
445            let mut current = &mut current_data;
446            for (i, part) in path_parts.iter().enumerate() {
447                let is_last = i == path_parts.len() - 1;
448
449                if is_last {
450                    // Set the value at the final key
451                    if let Some(obj) = current.as_object_mut() {
452                        let should_update = match obj.get(*part) {
453                            Some(v) => v.is_null(),
454                            None => true,
455                        };
456
457                        if should_update {
458                            obj.insert(
459                                (*part).to_string(),
460                                crate::utils::clean_float_noise(value.clone()),
461                            );
462                        }
463                    }
464                } else {
465                    // Ensure current is an object, then navigate/create intermediate objects
466                    if let Some(obj) = current.as_object_mut() {
467                        if !obj.contains_key(*part) {
468                            obj.insert((*part).to_string(), Value::Object(serde_json::Map::new()));
469                        }
470
471                        current = obj.get_mut(*part).unwrap();
472                    } else {
473                        // Skip this path if current is not an object and can't be made into one
474                        break;
475                    }
476                }
477            }
478        }
479
480        // Update self.data to persist the view changes (matching backup behavior)
481        self.data = current_data.clone();
482
483        crate::utils::clean_float_noise(current_data)
484    }
485
486    /// Get all schema values as array of path-value pairs
487    /// Returns [{path: "", value: ""}, ...]
488    ///
489    /// # Returns
490    ///
491    /// Array of objects containing path (dotted notation) and value pairs from value evaluations
492    pub fn get_schema_value_array(&self) -> Value {
493        let mut result = Vec::new();
494
495        for eval_key in self.value_evaluations.iter() {
496            let clean_key = eval_key.strip_prefix('#').unwrap_or(eval_key);
497
498            // Exclude rules.*.value, options.*.value, and $params
499            if clean_key.starts_with("/$params")
500                || (clean_key.ends_with("/value")
501                    && (clean_key.contains("/rules/") || clean_key.contains("/options/")))
502            {
503                continue;
504            }
505
506            // Check if field is effectively hidden
507            let schema_path = clean_key.strip_suffix("/value").unwrap_or(&clean_key);
508            if self.is_effective_hidden(schema_path) {
509                continue;
510            }
511
512            // Convert JSON pointer to dotted notation
513            let dotted_path = clean_key
514                .replace("/properties", "")
515                .replace("/value", "")
516                .trim_start_matches('/')
517                .replace('/', ".");
518
519            if dotted_path.is_empty() {
520                continue;
521            }
522
523            // Resolve static markers at this specific pointer (handles markers at or under this path)
524            let value = match self.resolve_static_markers_at_path(clean_key) {
525                Some(v) => crate::utils::clean_float_noise(v),
526                None => continue,
527            };
528
529            // Create {path, value} object
530            let mut item = serde_json::Map::new();
531            item.insert("path".to_string(), Value::String(dotted_path));
532            item.insert("value".to_string(), value);
533            result.push(Value::Object(item));
534        }
535
536        Value::Array(result)
537    }
538
539    /// Get all schema values as object with dotted path keys
540    /// Returns {path: value, ...}
541    ///
542    /// # Returns
543    ///
544    /// Flat object with dotted notation paths as keys and evaluated values
545    pub fn get_schema_value_object(&self) -> Value {
546        let mut result = serde_json::Map::new();
547
548        for eval_key in self.value_evaluations.iter() {
549            let clean_key = eval_key.strip_prefix('#').unwrap_or(eval_key);
550
551            // Exclude rules.*.value, options.*.value, and $params
552            if clean_key.starts_with("/$params")
553                || (clean_key.ends_with("/value")
554                    && (clean_key.contains("/rules/") || clean_key.contains("/options/")))
555            {
556                continue;
557            }
558
559            // Check if field is effectively hidden
560            let schema_path = clean_key.strip_suffix("/value").unwrap_or(&clean_key);
561            if self.is_effective_hidden(schema_path) {
562                continue;
563            }
564
565            // Convert JSON pointer to dotted notation
566            let dotted_path = clean_key
567                .replace("/properties", "")
568                .replace("/value", "")
569                .trim_start_matches('/')
570                .replace('/', ".");
571
572            if dotted_path.is_empty() {
573                continue;
574            }
575
576            // Resolve static markers at this specific pointer (handles markers at or under this path)
577            let value = match self.resolve_static_markers_at_path(clean_key) {
578                Some(v) => crate::utils::clean_float_noise(v),
579                None => continue,
580            };
581
582            result.insert(dotted_path, value);
583        }
584
585        Value::Object(result)
586    }
587
588    /// Get evaluated schema without $params
589    pub fn get_evaluated_schema_without_params(&mut self) -> Value {
590        let mut schema = self.get_evaluated_schema();
591        if let Value::Object(ref mut map) = schema {
592            map.remove("$params");
593        }
594        schema
595    }
596
597    /// Get evaluated schema as MessagePack bytes
598    pub fn get_evaluated_schema_msgpack(&mut self) -> Result<Vec<u8>, String> {
599        let schema = self.get_evaluated_schema();
600        rmp_serde::to_vec(&schema).map_err(|e| format!("MessagePack serialization failed: {}", e))
601    }
602
603    /// Get value from evaluated schema by path
604    pub fn get_evaluated_schema_by_path(&mut self, path: &str) -> Option<Value> {
605        self.get_schema_value_by_path(path)
606    }
607
608    /// Get evaluated schema parts by multiple paths
609    pub fn get_evaluated_schema_by_paths(
610        &mut self,
611        paths: &[String],
612        format: Option<ReturnFormat>,
613    ) -> Value {
614        match format.unwrap_or(ReturnFormat::Nested) {
615            ReturnFormat::Nested => {
616                let mut result = Value::Object(serde_json::Map::new());
617                for path in paths {
618                    if let Some(val) = self.get_schema_value_by_path(path) {
619                        // Insert into result object at proper path nesting
620                        Self::insert_at_path(&mut result, path, val);
621                    }
622                }
623                result
624            }
625            ReturnFormat::Flat => {
626                let mut result = serde_json::Map::new();
627                for path in paths {
628                    if let Some(val) = self.get_schema_value_by_path(path) {
629                        result.insert(path.clone(), val);
630                    }
631                }
632                Value::Object(result)
633            }
634            ReturnFormat::Array => {
635                let mut result = Vec::new();
636                for path in paths {
637                    if let Some(val) = self.get_schema_value_by_path(path) {
638                        result.push(val);
639                    } else {
640                        result.push(Value::Null);
641                    }
642                }
643                Value::Array(result)
644            }
645        }
646    }
647
648    /// Get original (unevaluated) schema by path
649    pub fn get_schema_by_path(&self, path: &str) -> Option<Value> {
650        let pointer_path = path_utils::dot_notation_to_schema_pointer(path);
651        self.schema
652            .pointer(&pointer_path.trim_start_matches('#'))
653            .cloned()
654    }
655
656    /// Get original schema by multiple paths
657    pub fn get_schema_by_paths(&self, paths: &[String], format: Option<ReturnFormat>) -> Value {
658        match format.unwrap_or(ReturnFormat::Nested) {
659            ReturnFormat::Nested => {
660                let mut result = Value::Object(serde_json::Map::new());
661                for path in paths {
662                    if let Some(val) = self.get_schema_by_path(path) {
663                        Self::insert_at_path(&mut result, path, val);
664                    }
665                }
666                result
667            }
668            ReturnFormat::Flat => {
669                let mut result = serde_json::Map::new();
670                for path in paths {
671                    if let Some(val) = self.get_schema_by_path(path) {
672                        result.insert(path.clone(), val);
673                    }
674                }
675                Value::Object(result)
676            }
677            ReturnFormat::Array => {
678                let mut result = Vec::new();
679                for path in paths {
680                    if let Some(val) = self.get_schema_by_path(path) {
681                        result.push(val);
682                    } else {
683                        result.push(Value::Null);
684                    }
685                }
686                Value::Array(result)
687            }
688        }
689    }
690
691    /// Helper to insert value into nested object at dotted path
692    pub(crate) fn insert_at_path(root: &mut Value, path: &str, value: Value) {
693        let parts: Vec<&str> = path.split('.').collect();
694        let mut current = root;
695
696        for (i, part) in parts.iter().enumerate() {
697            if i == parts.len() - 1 {
698                // Last part - set value
699                if let Value::Object(map) = current {
700                    map.insert(part.to_string(), value);
701                    return; // Done
702                }
703            } else {
704                // Intermediate part - traverse or create
705                // We need to temporarily take the value or use raw pointer manipulation?
706                // serde_json pointer is read-only or requires mutable reference
707
708                if !current.is_object() {
709                    *current = Value::Object(serde_json::Map::new());
710                }
711
712                if let Value::Object(map) = current {
713                    if !map.contains_key(*part) {
714                        map.insert(part.to_string(), Value::Object(serde_json::Map::new()));
715                    }
716                    current = map.get_mut(*part).unwrap();
717                }
718            }
719        }
720    }
721
722    /// Flatten a nested object key-value pair to dotted keys
723    pub fn flatten_object(
724        prefix: &str,
725        value: &Value,
726        result: &mut serde_json::Map<String, Value>,
727    ) {
728        match value {
729            Value::Object(map) => {
730                for (k, v) in map {
731                    let new_key = if prefix.is_empty() {
732                        k.clone()
733                    } else {
734                        format!("{}.{}", prefix, k)
735                    };
736                    Self::flatten_object(&new_key, v, result);
737                }
738            }
739            _ => {
740                result.insert(prefix.to_string(), value.clone());
741            }
742        }
743    }
744
745    pub fn convert_to_format(value: Value, format: ReturnFormat) -> Value {
746        match format {
747            ReturnFormat::Nested => value,
748            ReturnFormat::Flat => {
749                let mut result = serde_json::Map::new();
750                Self::flatten_object("", &value, &mut result);
751                Value::Object(result)
752            }
753            ReturnFormat::Array => {
754                if let Value::Object(map) = value {
755                    Value::Array(map.values().cloned().collect())
756                } else if let Value::Array(arr) = value {
757                    Value::Array(arr)
758                } else {
759                    Value::Array(vec![value])
760                }
761            }
762        }
763    }
764
765    /// Evaluate and return the options for a specific field on demand.
766    ///
767    /// Accepts dotted notation (`form.occupation`), JSON pointer
768    /// (`/properties/form/properties/occupation`), or schema ref
769    /// (`#/properties/form/properties/occupation`).
770    ///
771    /// Returns `None` when the field does not have an `options` key.
772    /// Returns the resolved options value (array, URL string, or null) otherwise.
773    pub fn get_field_options(&mut self, field_path: &str) -> Option<Value> {
774        // Normalize the input to a schema pointer (e.g. #/properties/form/properties/occupation)
775        let schema_ptr = if field_path.starts_with('#') || field_path.starts_with('/') {
776            path_utils::normalize_to_json_pointer(field_path).into_owned()
777        } else {
778            path_utils::dot_notation_to_schema_pointer(field_path)
779        };
780
781        // Build the JSON pointer path to the /options node (strip leading # for serde pointer())
782        let options_schema_key = format!("{}/options", schema_ptr);
783        let options_pointer =
784            path_utils::normalize_to_json_pointer(&options_schema_key).into_owned();
785
786        // Check if the options node exists in the evaluated schema
787        let options_node = self.evaluated_schema.pointer(&options_pointer)?.clone();
788
789        // If the options node is an object with $evaluation, evaluate it now (deferred)
790        if let Value::Object(ref map) = options_node {
791            if map.contains_key("$evaluation") {
792                let eval_key = options_schema_key.clone();
793
794                if let Some(logic_id) = self.evaluations.get(&eval_key).copied() {
795                    let snap = self.eval_data.snapshot_data();
796                    if let Ok(result) = self.engine.run(&logic_id, &*snap) {
797                        let cleaned = clean_float_noise_scalar(result);
798                        if let Some(node) = self.evaluated_schema.pointer_mut(&options_pointer) {
799                            *node = cleaned.clone();
800                        }
801                        return Some(cleaned);
802                    }
803                }
804                // No compiled logic found — options cannot be resolved
805                return None;
806            }
807        }
808
809        // Check options_templates for a URL template at this field's options/url path
810        let url_pointer =
811            path_utils::normalize_to_json_pointer(&format!("{}/options/url", schema_ptr))
812                .into_owned();
813
814        let templates = self.options_templates.clone();
815        for (tmpl_url_path, tmpl_str, tmpl_params_path) in templates.iter() {
816            if *tmpl_url_path == url_pointer {
817                if let Some(params) = self.evaluated_schema.pointer(tmpl_params_path) {
818                    let params = params.clone();
819                    if let Ok(resolved_url) = self.evaluate_template(tmpl_str, &params) {
820                        if let Some(target) = self.evaluated_schema.pointer_mut(&url_pointer) {
821                            *target = Value::String(resolved_url);
822                        }
823                        return self.evaluated_schema.pointer(&options_pointer).cloned();
824                    }
825                }
826                break;
827            }
828        }
829
830        // Static options (already-evaluated array or plain value)
831        Some(options_node)
832    }
833}