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