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        // Resolve indexed static-array paths directly.
355        for (static_key, array_arc) in self.static_arrays.iter() {
356            let schema_path: &str = if static_key.starts_with("/$table") {
357                &static_key["/$table".len()..]
358            } else {
359                static_key.as_str()
360            };
361
362            if let Some(relative) = schema_prefix
363                .strip_prefix(schema_path)
364                .and_then(|relative| relative.strip_prefix('/'))
365            {
366                return array_arc.pointer(&format!("/{}", relative)).cloned();
367            }
368        }
369
370        let mut subtree = self.evaluated_schema.pointer(schema_prefix)?.clone();
371
372        // Pre-build "prefix/" once for the starts_with check in the loop
373        let prefix_slash = format!("{}/", schema_prefix);
374
375        for (static_key, array_arc) in self.static_arrays.iter() {
376            // Derive the absolute schema path the same way resolve_static_markers_in_value does
377            let schema_path: &str = if static_key.starts_with("/$table") {
378                &static_key["/$table".len()..]
379            } else {
380                static_key.as_str()
381            };
382
383            // Compute the path relative to the subtree root
384            let relative: &str = if schema_path == schema_prefix {
385                // The subtree root itself is the marker — replace the whole subtree
386                ""
387            } else if schema_path.starts_with(&prefix_slash) {
388                // Strip the prefix: remainder is the sub-path within the cloned subtree
389                &schema_path[schema_prefix.len()..]
390            } else {
391                continue; // Not under the requested path — skip
392            };
393
394            if relative.is_empty() {
395                subtree = (**array_arc).clone();
396            } else if let Some(target) = subtree.pointer_mut(relative) {
397                *target = (**array_arc).clone();
398            }
399        }
400
401        Some(subtree)
402    }
403
404    /// Get specific schema value by path, resolving any `$static_array` markers at or
405    /// under that path.
406    pub fn get_schema_value_by_path(&self, path: &str) -> Option<Value> {
407        let pointer_path = path_utils::dot_notation_to_schema_pointer(path);
408        self.resolve_static_markers_at_path(pointer_path.trim_start_matches('#'))
409    }
410
411    /// Get all schema values (data view).
412    ///
413    /// Builds a consumer view from current data and evaluated values without mutating
414    /// evaluator state. Indexed subforms carry active-item wrappers at their root;
415    /// persisting this view would append that wrapper into later form evaluations.
416    pub fn get_schema_value(&mut self) -> Value {
417        // Start with current authoritative data from eval_data
418        let mut current_data = self.eval_data.data().clone();
419
420        // Ensure it's an object
421        if !current_data.is_object() {
422            current_data = Value::Object(serde_json::Map::new());
423        }
424
425        // Strip $params and $context from data
426        if let Some(obj) = current_data.as_object_mut() {
427            obj.remove("$params");
428            obj.remove("$context");
429        }
430
431        // Prune hidden values from current_data (to remove user input in hidden fields)
432        self.prune_hidden_values(&mut current_data, "");
433
434        // Override data with values from value evaluations
435        // We use value_evaluations which stores the paths of fields with .value
436        for eval_key in self.value_evaluations.iter() {
437            let clean_key = eval_key.strip_prefix('#').unwrap_or(eval_key);
438
439            // Exclude rules.*.value, options.*.value, and $params
440            if clean_key.starts_with("/$params")
441                || (clean_key.ends_with("/value")
442                    && (clean_key.contains("/rules/") || clean_key.contains("/options/")))
443            {
444                continue;
445            }
446
447            let path = clean_key.replace("/properties", "").replace("/value", "");
448
449            // Check if field is effectively hidden
450            // Schema path is clean_key without /value
451            let schema_path = clean_key.strip_suffix("/value").unwrap_or(&clean_key);
452            if self.is_effective_hidden(schema_path) {
453                continue;
454            }
455
456            // Resolve static markers at this specific pointer (handles markers at or under this path)
457            let value = match self.resolve_static_markers_at_path(clean_key) {
458                Some(v) => v,
459                None => continue,
460            };
461
462            // Parse the path and create nested structure as needed
463            let path_parts: Vec<&str> = path.split('/').filter(|s| !s.is_empty()).collect();
464
465            if path_parts.is_empty() {
466                continue;
467            }
468
469            // Navigate/create nested structure
470            let mut current = &mut current_data;
471            for (i, part) in path_parts.iter().enumerate() {
472                let is_last = i == path_parts.len() - 1;
473
474                if is_last {
475                    // Only disabled calculated fields are library-owned. Editable fields
476                    // preserve non-null caller input even when their schema has `$evaluation`.
477                    let schema_value = self.schema.pointer(clean_key);
478                    let computed_value = schema_value
479                        .and_then(Value::as_object)
480                        .is_some_and(|value| value.contains_key("$evaluation"));
481                    let disabled = self
482                        .evaluated_schema
483                        .pointer(schema_path)
484                        .and_then(Value::as_object)
485                        .and_then(|field| field.get("condition"))
486                        .and_then(Value::as_object)
487                        .and_then(|condition| condition.get("disabled"))
488                        .is_some_and(|disabled| disabled == &Value::Bool(true));
489                    let computed_disabled =
490                        computed_value && (disabled || !self.is_mapped_in_any_layout(schema_path));
491                    if let Some(obj) = current.as_object_mut() {
492                        let should_update = computed_disabled
493                            || match obj.get(*part) {
494                                Some(v) => v.is_null(),
495                                None => true,
496                            };
497                        if should_update {
498                            obj.insert(
499                                (*part).to_string(),
500                                crate::utils::clean_float_noise(value.clone()),
501                            );
502                        }
503                    }
504                } else {
505                    // Ensure current is an object, then navigate/create intermediate objects
506                    if let Some(obj) = current.as_object_mut() {
507                        if !obj.contains_key(*part) {
508                            obj.insert((*part).to_string(), Value::Object(serde_json::Map::new()));
509                        }
510
511                        current = obj.get_mut(*part).unwrap();
512                    } else {
513                        // Skip this path if current is not an object and can't be made into one
514                        break;
515                    }
516                }
517            }
518        }
519
520        crate::utils::clean_float_noise(current_data)
521    }
522
523    /// Get all schema values as array of path-value pairs
524    /// Returns [{path: "", value: ""}, ...]
525    ///
526    /// # Returns
527    ///
528    /// Array of objects containing path (dotted notation) and value pairs from value evaluations
529    pub fn get_schema_value_array(&self) -> Value {
530        let mut result = Vec::new();
531
532        for eval_key in self.value_evaluations.iter() {
533            let clean_key = eval_key.strip_prefix('#').unwrap_or(eval_key);
534
535            // Exclude rules.*.value, options.*.value, and $params
536            if clean_key.starts_with("/$params")
537                || (clean_key.ends_with("/value")
538                    && (clean_key.contains("/rules/") || clean_key.contains("/options/")))
539            {
540                continue;
541            }
542
543            // Check if field is effectively hidden
544            let schema_path = clean_key.strip_suffix("/value").unwrap_or(&clean_key);
545            if self.is_effective_hidden(schema_path) {
546                continue;
547            }
548
549            // Convert JSON pointer to dotted notation
550            let dotted_path = clean_key
551                .replace("/properties", "")
552                .replace("/value", "")
553                .trim_start_matches('/')
554                .replace('/', ".");
555
556            if dotted_path.is_empty() {
557                continue;
558            }
559
560            // Resolve static markers at this specific pointer (handles markers at or under this path)
561            let value = match self.resolve_static_markers_at_path(clean_key) {
562                Some(v) => crate::utils::clean_float_noise(v),
563                None => continue,
564            };
565
566            // Create {path, value} object
567            let mut item = serde_json::Map::new();
568            item.insert("path".to_string(), Value::String(dotted_path));
569            item.insert("value".to_string(), value);
570            result.push(Value::Object(item));
571        }
572
573        Value::Array(result)
574    }
575
576    /// Get all schema values as object with dotted path keys
577    /// Returns {path: value, ...}
578    ///
579    /// # Returns
580    ///
581    /// Flat object with dotted notation paths as keys and evaluated values
582    pub fn get_schema_value_object(&self) -> Value {
583        let mut result = serde_json::Map::new();
584
585        for eval_key in self.value_evaluations.iter() {
586            let clean_key = eval_key.strip_prefix('#').unwrap_or(eval_key);
587
588            // Exclude rules.*.value, options.*.value, and $params
589            if clean_key.starts_with("/$params")
590                || (clean_key.ends_with("/value")
591                    && (clean_key.contains("/rules/") || clean_key.contains("/options/")))
592            {
593                continue;
594            }
595
596            // Check if field is effectively hidden
597            let schema_path = clean_key.strip_suffix("/value").unwrap_or(&clean_key);
598            if self.is_effective_hidden(schema_path) {
599                continue;
600            }
601
602            // Convert JSON pointer to dotted notation
603            let dotted_path = clean_key
604                .replace("/properties", "")
605                .replace("/value", "")
606                .trim_start_matches('/')
607                .replace('/', ".");
608
609            if dotted_path.is_empty() {
610                continue;
611            }
612
613            // Resolve static markers at this specific pointer (handles markers at or under this path)
614            let value = match self.resolve_static_markers_at_path(clean_key) {
615                Some(v) => crate::utils::clean_float_noise(v),
616                None => continue,
617            };
618
619            result.insert(dotted_path, value);
620        }
621
622        Value::Object(result)
623    }
624
625    /// Get evaluated schema without $params
626    pub fn get_evaluated_schema_without_params(&mut self) -> Value {
627        let mut schema = self.get_evaluated_schema();
628        if let Value::Object(ref mut map) = schema {
629            map.remove("$params");
630        }
631        schema
632    }
633
634    /// Get evaluated schema as MessagePack bytes (compact, without $layout resolution)
635    pub fn get_evaluated_schema_msgpack(&mut self) -> Result<Vec<u8>, String> {
636        let schema = self.get_evaluated_schema();
637        rmp_serde::to_vec(&schema).map_err(|e| format!("MessagePack serialization failed: {}", e))
638    }
639
640    /// Get layout-resolved evaluated schema as MessagePack bytes.
641    ///
642    /// Reuses `get_evaluated_schema_resolved`, which omits `$params` and merges
643    /// resolved `$layout` overlays.
644    pub fn get_evaluated_schema_resolved_msgpack(&mut self) -> Result<Vec<u8>, String> {
645        let schema = self.get_evaluated_schema_resolved();
646        rmp_serde::to_vec(&schema).map_err(|e| format!("MessagePack serialization failed: {}", e))
647    }
648
649    /// Get value from evaluated schema by path
650    pub fn get_evaluated_schema_by_path(&mut self, path: &str) -> Option<Value> {
651        self.get_schema_value_by_path(path)
652    }
653
654    /// Get evaluated schema parts by multiple paths
655    pub fn get_evaluated_schema_by_paths(
656        &mut self,
657        paths: &[String],
658        format: Option<ReturnFormat>,
659    ) -> Value {
660        match format.unwrap_or(ReturnFormat::Nested) {
661            ReturnFormat::Nested => {
662                let mut result = Value::Object(serde_json::Map::new());
663                for path in paths {
664                    if let Some(val) = self.get_schema_value_by_path(path) {
665                        // Insert into result object at proper path nesting
666                        Self::insert_at_path(&mut result, path, val);
667                    }
668                }
669                result
670            }
671            ReturnFormat::Flat => {
672                let mut result = serde_json::Map::new();
673                for path in paths {
674                    if let Some(val) = self.get_schema_value_by_path(path) {
675                        result.insert(path.clone(), val);
676                    }
677                }
678                Value::Object(result)
679            }
680            ReturnFormat::Array => {
681                let mut result = Vec::new();
682                for path in paths {
683                    if let Some(val) = self.get_schema_value_by_path(path) {
684                        result.push(val);
685                    } else {
686                        result.push(Value::Null);
687                    }
688                }
689                Value::Array(result)
690            }
691        }
692    }
693
694    /// Get original (unevaluated) schema by path
695    pub fn get_schema_by_path(&self, path: &str) -> Option<Value> {
696        let pointer_path = path_utils::dot_notation_to_schema_pointer(path);
697        self.schema
698            .pointer(&pointer_path.trim_start_matches('#'))
699            .cloned()
700    }
701
702    /// Get original schema by multiple paths
703    pub fn get_schema_by_paths(&self, paths: &[String], format: Option<ReturnFormat>) -> Value {
704        match format.unwrap_or(ReturnFormat::Nested) {
705            ReturnFormat::Nested => {
706                let mut result = Value::Object(serde_json::Map::new());
707                for path in paths {
708                    if let Some(val) = self.get_schema_by_path(path) {
709                        Self::insert_at_path(&mut result, path, val);
710                    }
711                }
712                result
713            }
714            ReturnFormat::Flat => {
715                let mut result = serde_json::Map::new();
716                for path in paths {
717                    if let Some(val) = self.get_schema_by_path(path) {
718                        result.insert(path.clone(), val);
719                    }
720                }
721                Value::Object(result)
722            }
723            ReturnFormat::Array => {
724                let mut result = Vec::new();
725                for path in paths {
726                    if let Some(val) = self.get_schema_by_path(path) {
727                        result.push(val);
728                    } else {
729                        result.push(Value::Null);
730                    }
731                }
732                Value::Array(result)
733            }
734        }
735    }
736
737    /// Helper to insert value into nested object at dotted path
738    pub(crate) fn insert_at_path(root: &mut Value, path: &str, value: Value) {
739        let parts: Vec<&str> = path.split('.').collect();
740        let mut current = root;
741
742        for (i, part) in parts.iter().enumerate() {
743            if i == parts.len() - 1 {
744                // Last part - set value
745                if let Value::Object(map) = current {
746                    map.insert(part.to_string(), value);
747                    return; // Done
748                }
749            } else {
750                // Intermediate part - traverse or create
751                // We need to temporarily take the value or use raw pointer manipulation?
752                // serde_json pointer is read-only or requires mutable reference
753
754                if !current.is_object() {
755                    *current = Value::Object(serde_json::Map::new());
756                }
757
758                if let Value::Object(map) = current {
759                    if !map.contains_key(*part) {
760                        map.insert(part.to_string(), Value::Object(serde_json::Map::new()));
761                    }
762                    current = map.get_mut(*part).unwrap();
763                }
764            }
765        }
766    }
767
768    /// Flatten a nested object key-value pair to dotted keys
769    pub fn flatten_object(
770        prefix: &str,
771        value: &Value,
772        result: &mut serde_json::Map<String, Value>,
773    ) {
774        match value {
775            Value::Object(map) => {
776                for (k, v) in map {
777                    let new_key = if prefix.is_empty() {
778                        k.clone()
779                    } else {
780                        format!("{}.{}", prefix, k)
781                    };
782                    Self::flatten_object(&new_key, v, result);
783                }
784            }
785            _ => {
786                result.insert(prefix.to_string(), value.clone());
787            }
788        }
789    }
790
791    pub fn convert_to_format(value: Value, format: ReturnFormat) -> Value {
792        match format {
793            ReturnFormat::Nested => value,
794            ReturnFormat::Flat => {
795                let mut result = serde_json::Map::new();
796                Self::flatten_object("", &value, &mut result);
797                Value::Object(result)
798            }
799            ReturnFormat::Array => {
800                if let Value::Object(map) = value {
801                    Value::Array(map.values().cloned().collect())
802                } else if let Value::Array(arr) = value {
803                    Value::Array(arr)
804                } else {
805                    Value::Array(vec![value])
806                }
807            }
808        }
809    }
810
811    /// Evaluate and return the options for a specific field on demand.
812    ///
813    /// Accepts dotted notation (`form.occupation`), JSON pointer
814    /// (`/properties/form/properties/occupation`), or schema ref
815    /// (`#/properties/form/properties/occupation`).
816    ///
817    /// Returns `None` when the field does not have an `options` key.
818    /// Returns the resolved options value (array, URL string, or null) otherwise.
819    pub fn get_field_options(&mut self, field_path: &str) -> Option<Value> {
820        // Normalize the input to a schema pointer (e.g. #/properties/form/properties/occupation)
821        let schema_ptr = if field_path.starts_with('#') || field_path.starts_with('/') {
822            path_utils::normalize_to_json_pointer(field_path).into_owned()
823        } else {
824            path_utils::dot_notation_to_schema_pointer(field_path)
825        };
826
827        // Build the JSON pointer path to the /options node (strip leading # for serde pointer())
828        let options_schema_key = format!("{}/options", schema_ptr);
829        let options_pointer =
830            path_utils::normalize_to_json_pointer(&options_schema_key).into_owned();
831
832        // Check if the options node exists in the evaluated schema
833        let options_node = self.evaluated_schema.pointer(&options_pointer)?.clone();
834
835        // If the options node is an object with $evaluation, evaluate it now (deferred)
836        if let Value::Object(ref map) = options_node {
837            if map.contains_key("$evaluation") {
838                let eval_key = options_schema_key.clone();
839
840                if let Some(logic_id) = self.evaluations.get(&eval_key).copied() {
841                    let snap = self.eval_data.snapshot_data();
842                    if let Ok(result) = self.engine.run(&logic_id, &*snap) {
843                        let cleaned = clean_float_noise_scalar(result);
844                        if let Some(node) = self.evaluated_schema.pointer_mut(&options_pointer) {
845                            *node = cleaned.clone();
846                        }
847                        return Some(cleaned);
848                    }
849                }
850                // No compiled logic found — options cannot be resolved
851                return None;
852            }
853        }
854
855        // Check options_templates for a URL template at this field's options/url path
856        let url_pointer =
857            path_utils::normalize_to_json_pointer(&format!("{}/options/url", schema_ptr))
858                .into_owned();
859
860        let templates = self.options_templates.clone();
861        for (tmpl_url_path, tmpl_str, tmpl_params_path) in templates.iter() {
862            if *tmpl_url_path == url_pointer {
863                if let Some(params) = self.evaluated_schema.pointer(tmpl_params_path) {
864                    let params = params.clone();
865                    if let Ok(resolved_url) = self.evaluate_template(tmpl_str, &params) {
866                        if let Some(target) = self.evaluated_schema.pointer_mut(&url_pointer) {
867                            *target = Value::String(resolved_url);
868                        }
869                        return self.evaluated_schema.pointer(&options_pointer).cloned();
870                    }
871                }
872                break;
873            }
874        }
875
876        // Static options (already-evaluated array or plain value)
877        Some(options_node)
878    }
879}