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