Skip to main content

json_eval_rs/jsoneval/
getters.rs

1use super::JSONEval;
2use crate::jsoneval::path_utils;
3use crate::jsoneval::types::ReturnFormat;
4
5use crate::time_block;
6use serde_json::Value;
7
8impl JSONEval {
9    /// Check if a field is effectively hidden by checking its condition and all parents
10    /// Also checks for $layout.hideLayout.all on parents
11    pub(crate) fn is_effective_hidden(&self, schema_pointer: &str) -> bool {
12        let mut end = schema_pointer.len();
13
14        loop {
15            let current_path = &schema_pointer[..end];
16
17            if let Some(schema_node) = self.evaluated_schema.pointer(current_path) {
18                if let Value::Object(map) = schema_node {
19                    if let Some(Value::Object(condition)) = map.get("condition") {
20                        if let Some(Value::Bool(true)) = condition.get("hidden") {
21                            return true;
22                        }
23                    }
24
25                    if let Some(Value::Object(layout)) = map.get("$layout") {
26                        if let Some(Value::Object(hide_layout)) = layout.get("hideLayout") {
27                            if let Some(Value::Bool(true)) = hide_layout.get("all") {
28                                return true;
29                            }
30                        }
31                    }
32                }
33            }
34
35            if end == 0 {
36                break;
37            }
38
39            // Move to parent: find last '/' and strip /properties or /items suffixes
40            match schema_pointer[..end].rfind('/') {
41                Some(0) | None => {
42                    end = 0;
43                }
44                Some(last_slash) => {
45                    end = last_slash;
46                    let parent = &schema_pointer[..end];
47                    if parent.ends_with("/properties") {
48                        end -= "/properties".len();
49                    } else if parent.ends_with("/items") {
50                        end -= "/items".len();
51                    }
52                }
53            }
54        }
55
56        false
57    }
58
59    /// Prune hidden values from data object recursively
60    fn prune_hidden_values(&self, data: &mut Value, current_path: &str) {
61        if let Value::Object(map) = data {
62            // Collect keys to remove to avoid borrow checker issues
63            let mut keys_to_remove = Vec::new();
64
65            for (key, value) in map.iter_mut() {
66                // Skip special keys
67                if key == "$params" || key == "$context" {
68                    continue;
69                }
70
71                // Construct schema path for this key
72                // For root fields: /properties/key
73                // For nested fields: current_path/properties/key
74                let schema_path = if current_path.is_empty() {
75                    format!("/properties/{}", key)
76                } else {
77                    format!("{}/properties/{}", current_path, key)
78                };
79
80                // Check if hidden
81                if self.is_effective_hidden(&schema_path) {
82                    keys_to_remove.push(key.clone());
83                } else {
84                    // Recurse if object
85                    if value.is_object() {
86                        self.prune_hidden_values(value, &schema_path);
87                    }
88                }
89            }
90
91            // Remove hidden keys
92            for key in keys_to_remove {
93                map.remove(&key);
94            }
95        }
96    }
97
98    /// Replace any `{"$static_array": "/$table/..."}` and `{"$static_array": "/$params/..."}` markers in `schema_output`
99    /// with the actual evaluated array data from `eval_data`.
100    ///
101    /// By iterating only over tracked `static_arrays`, we replace markers in O(markers) time
102    /// instead of requiring an expensive O(schema_nodes) recursive tree walk.
103    fn resolve_static_markers_in_value(&self, schema_output: &mut Value) {
104        for (static_key, array_arc) in self.static_arrays.iter() {
105            // Determine the schema pointer path where this marker was placed
106            let schema_path = if static_key.starts_with("/$table") {
107                &static_key["/$table".len()..] // e.g. /properties/product_benefit/...
108            } else {
109                static_key.as_str() // e.g. /$params/references/...
110            };
111
112            // Only attempt replacement if the exact path exists in the cloned schema output
113            if let Some(target_val) = schema_output.pointer_mut(schema_path) {
114                // The actual evaluated array is seamlessly stored right in the map's value
115                *target_val = (**array_arc).clone();
116            }
117        }
118    }
119
120
121    /// Get the evaluated schema with optional layout resolution.
122    ///
123    /// # Arguments
124    ///
125    /// * `skip_layout` - Whether to skip layout resolution.
126    ///
127    /// # Returns
128    ///
129    /// The evaluated schema as a JSON value, with all `$static_array` markers resolved
130    /// to their actual evaluated data.
131    pub fn get_evaluated_schema(&mut self, skip_layout: bool) -> Value {
132        time_block!("get_evaluated_schema()", {
133            if !skip_layout {
134                if let Err(e) = self.resolve_layout(false) {
135                    eprintln!(
136                        "Warning: Layout resolution failed in get_evaluated_schema: {}",
137                        e
138                    );
139                }
140            }
141            let mut schema = self.evaluated_schema.clone();
142            self.resolve_static_markers_in_value(&mut schema);
143            schema
144        })
145    }
146
147    /// Resolve `$static_array` markers within the subtree rooted at `schema_prefix`.
148    ///
149    /// Clones only the node at `schema_prefix` from `evaluated_schema`, then iterates
150    /// the tracked `static_arrays` list filtering to entries whose schema path is at or
151    /// under `schema_prefix`. Only those markers are replaced inside the cloned subtree;
152    /// unrelated entries are skipped entirely.
153    ///
154    /// # Examples
155    /// - `schema_prefix = "/$params/references"` → resolves only arrays nested under that key
156    /// - `schema_prefix = "/properties/foo/value"` → resolves a single marker if the field itself is one
157    fn resolve_static_markers_at_path(&self, schema_prefix: &str) -> Option<Value> {
158        let mut subtree = self.evaluated_schema.pointer(schema_prefix)?.clone();
159
160        // Pre-build "prefix/" once for the starts_with check in the loop
161        let prefix_slash = format!("{}/", schema_prefix);
162
163        for (static_key, array_arc) in self.static_arrays.iter() {
164            // Derive the absolute schema path the same way resolve_static_markers_in_value does
165            let schema_path: &str = if static_key.starts_with("/$table") {
166                &static_key["/$table".len()..]
167            } else {
168                static_key.as_str()
169            };
170
171            // Compute the path relative to the subtree root
172            let relative: &str = if schema_path == schema_prefix {
173                // The subtree root itself is the marker — replace the whole subtree
174                ""
175            } else if schema_path.starts_with(&prefix_slash) {
176                // Strip the prefix: remainder is the sub-path within the cloned subtree
177                &schema_path[schema_prefix.len()..]
178            } else {
179                continue; // Not under the requested path — skip
180            };
181
182            if relative.is_empty() {
183                subtree = (**array_arc).clone();
184            } else if let Some(target) = subtree.pointer_mut(relative) {
185                *target = (**array_arc).clone();
186            }
187        }
188
189        Some(subtree)
190    }
191
192    /// Get specific schema value by path, resolving any `$static_array` markers at or
193    /// under that path.
194    pub fn get_schema_value_by_path(&self, path: &str) -> Option<Value> {
195        let pointer_path = path_utils::dot_notation_to_schema_pointer(path);
196        self.resolve_static_markers_at_path(pointer_path.trim_start_matches('#'))
197    }
198
199    /// Get all schema values (data view)
200    /// Mutates internal data state by overriding with values from value evaluations
201    /// This corresponds to subform.get_schema_value() usage
202    pub fn get_schema_value(&mut self) -> Value {
203        // Start with current authoritative data from eval_data
204        let mut current_data = self.eval_data.data().clone();
205
206        // Ensure it's an object
207        if !current_data.is_object() {
208            current_data = Value::Object(serde_json::Map::new());
209        }
210
211        // Strip $params and $context from data
212        if let Some(obj) = current_data.as_object_mut() {
213            obj.remove("$params");
214            obj.remove("$context");
215        }
216
217        // Prune hidden values from current_data (to remove user input in hidden fields)
218        self.prune_hidden_values(&mut current_data, "");
219
220        // Override data with values from value evaluations
221        // We use value_evaluations which stores the paths of fields with .value
222        for eval_key in self.value_evaluations.iter() {
223            let clean_key = eval_key.strip_prefix('#').unwrap_or(eval_key);
224
225            // Exclude rules.*.value, options.*.value, and $params
226            if clean_key.starts_with("/$params")
227                || (clean_key.ends_with("/value")
228                    && (clean_key.contains("/rules/") || clean_key.contains("/options/")))
229            {
230                continue;
231            }
232
233            let path = clean_key.replace("/properties", "").replace("/value", "");
234
235            // Check if field is effectively hidden
236            // Schema path is clean_key without /value
237            let schema_path = clean_key.strip_suffix("/value").unwrap_or(&clean_key);
238            if self.is_effective_hidden(schema_path) {
239                continue;
240            }
241
242            // Resolve static markers at this specific pointer (handles markers at or under this path)
243            let value = match self.resolve_static_markers_at_path(clean_key) {
244                Some(v) => v,
245                None => continue,
246            };
247
248            // Parse the path and create nested structure as needed
249            let path_parts: Vec<&str> = path.split('/').filter(|s| !s.is_empty()).collect();
250
251            if path_parts.is_empty() {
252                continue;
253            }
254
255            // Navigate/create nested structure
256            let mut current = &mut current_data;
257            for (i, part) in path_parts.iter().enumerate() {
258                let is_last = i == path_parts.len() - 1;
259
260                if is_last {
261                    // Set the value at the final key
262                    if let Some(obj) = current.as_object_mut() {
263                        let should_update = match obj.get(*part) {
264                            Some(v) => v.is_null(),
265                            None => true,
266                        };
267
268                        if should_update {
269                            obj.insert(
270                                (*part).to_string(),
271                                crate::utils::clean_float_noise(value.clone()),
272                            );
273                        }
274                    }
275                } else {
276                    // Ensure current is an object, then navigate/create intermediate objects
277                    if let Some(obj) = current.as_object_mut() {
278                        if !obj.contains_key(*part) {
279                            obj.insert((*part).to_string(), Value::Object(serde_json::Map::new()));
280                        }
281
282                        current = obj.get_mut(*part).unwrap();
283                    } else {
284                        // Skip this path if current is not an object and can't be made into one
285                        break;
286                    }
287                }
288            }
289        }
290
291        // Update self.data to persist the view changes (matching backup behavior)
292        self.data = current_data.clone();
293
294        crate::utils::clean_float_noise(current_data)
295    }
296
297    /// Get all schema values as array of path-value pairs
298    /// Returns [{path: "", value: ""}, ...]
299    ///
300    /// # Returns
301    ///
302    /// Array of objects containing path (dotted notation) and value pairs from value evaluations
303    pub fn get_schema_value_array(&self) -> Value {
304        let mut result = Vec::new();
305
306        for eval_key in self.value_evaluations.iter() {
307            let clean_key = eval_key.strip_prefix('#').unwrap_or(eval_key);
308
309            // Exclude rules.*.value, options.*.value, and $params
310            if clean_key.starts_with("/$params")
311                || (clean_key.ends_with("/value")
312                    && (clean_key.contains("/rules/") || clean_key.contains("/options/")))
313            {
314                continue;
315            }
316
317            // Check if field is effectively hidden
318            let schema_path = clean_key.strip_suffix("/value").unwrap_or(&clean_key);
319            if self.is_effective_hidden(schema_path) {
320                continue;
321            }
322
323            // Convert JSON pointer to dotted notation
324            let dotted_path = clean_key
325                .replace("/properties", "")
326                .replace("/value", "")
327                .trim_start_matches('/')
328                .replace('/', ".");
329
330            if dotted_path.is_empty() {
331                continue;
332            }
333
334            // Resolve static markers at this specific pointer (handles markers at or under this path)
335            let value = match self.resolve_static_markers_at_path(clean_key) {
336                Some(v) => crate::utils::clean_float_noise(v),
337                None => continue,
338            };
339
340            // Create {path, value} object
341            let mut item = serde_json::Map::new();
342            item.insert("path".to_string(), Value::String(dotted_path));
343            item.insert("value".to_string(), value);
344            result.push(Value::Object(item));
345        }
346
347        Value::Array(result)
348    }
349
350    /// Get all schema values as object with dotted path keys
351    /// Returns {path: value, ...}
352    ///
353    /// # Returns
354    ///
355    /// Flat object with dotted notation paths as keys and evaluated values
356    pub fn get_schema_value_object(&self) -> Value {
357        let mut result = serde_json::Map::new();
358
359        for eval_key in self.value_evaluations.iter() {
360            let clean_key = eval_key.strip_prefix('#').unwrap_or(eval_key);
361
362            // Exclude rules.*.value, options.*.value, and $params
363            if clean_key.starts_with("/$params")
364                || (clean_key.ends_with("/value")
365                    && (clean_key.contains("/rules/") || clean_key.contains("/options/")))
366            {
367                continue;
368            }
369
370            // Check if field is effectively hidden
371            let schema_path = clean_key.strip_suffix("/value").unwrap_or(&clean_key);
372            if self.is_effective_hidden(schema_path) {
373                continue;
374            }
375
376            // Convert JSON pointer to dotted notation
377            let dotted_path = clean_key
378                .replace("/properties", "")
379                .replace("/value", "")
380                .trim_start_matches('/')
381                .replace('/', ".");
382
383            if dotted_path.is_empty() {
384                continue;
385            }
386
387            // Resolve static markers at this specific pointer (handles markers at or under this path)
388            let value = match self.resolve_static_markers_at_path(clean_key) {
389                Some(v) => crate::utils::clean_float_noise(v),
390                None => continue,
391            };
392
393            result.insert(dotted_path, value);
394        }
395
396        Value::Object(result)
397    }
398
399    /// Get evaluated schema without $params
400    pub fn get_evaluated_schema_without_params(&mut self, skip_layout: bool) -> Value {
401        let mut schema = self.get_evaluated_schema(skip_layout);
402        if let Value::Object(ref mut map) = schema {
403            map.remove("$params");
404        }
405        schema
406    }
407
408    /// Get evaluated schema as MessagePack bytes
409    pub fn get_evaluated_schema_msgpack(&mut self, skip_layout: bool) -> Result<Vec<u8>, String> {
410        let schema = self.get_evaluated_schema(skip_layout);
411        rmp_serde::to_vec(&schema).map_err(|e| format!("MessagePack serialization failed: {}", e))
412    }
413
414    /// Get value from evaluated schema by path
415    pub fn get_evaluated_schema_by_path(&mut self, path: &str, skip_layout: bool) -> Option<Value> {
416        if !skip_layout {
417            if let Err(e) = self.resolve_layout(false) {
418                eprintln!(
419                    "Warning: Layout resolution failed in get_evaluated_schema_by_path: {}",
420                    e
421                );
422            }
423        }
424        self.get_schema_value_by_path(path)
425    }
426
427    /// Get evaluated schema parts by multiple paths
428    pub fn get_evaluated_schema_by_paths(
429        &mut self,
430        paths: &[String],
431        skip_layout: bool,
432        format: Option<ReturnFormat>,
433    ) -> Value {
434        if !skip_layout {
435            if let Err(e) = self.resolve_layout(false) {
436                eprintln!(
437                    "Warning: Layout resolution failed in get_evaluated_schema_by_paths: {}",
438                    e
439                );
440            }
441        }
442
443        match format.unwrap_or(ReturnFormat::Nested) {
444            ReturnFormat::Nested => {
445                let mut result = Value::Object(serde_json::Map::new());
446                for path in paths {
447                    if let Some(val) = self.get_schema_value_by_path(path) {
448                        // Insert into result object at proper path nesting
449                        Self::insert_at_path(&mut result, path, val);
450                    }
451                }
452                result
453            }
454            ReturnFormat::Flat => {
455                let mut result = serde_json::Map::new();
456                for path in paths {
457                    if let Some(val) = self.get_schema_value_by_path(path) {
458                        result.insert(path.clone(), val);
459                    }
460                }
461                Value::Object(result)
462            }
463            ReturnFormat::Array => {
464                let mut result = Vec::new();
465                for path in paths {
466                    if let Some(val) = self.get_schema_value_by_path(path) {
467                        result.push(val);
468                    } else {
469                        result.push(Value::Null);
470                    }
471                }
472                Value::Array(result)
473            }
474        }
475    }
476
477    /// Get original (unevaluated) schema by path
478    pub fn get_schema_by_path(&self, path: &str) -> Option<Value> {
479        let pointer_path = path_utils::dot_notation_to_schema_pointer(path);
480        self.schema
481            .pointer(&pointer_path.trim_start_matches('#'))
482            .cloned()
483    }
484
485    /// Get original schema by multiple paths
486    pub fn get_schema_by_paths(&self, paths: &[String], format: Option<ReturnFormat>) -> Value {
487        match format.unwrap_or(ReturnFormat::Nested) {
488            ReturnFormat::Nested => {
489                let mut result = Value::Object(serde_json::Map::new());
490                for path in paths {
491                    if let Some(val) = self.get_schema_by_path(path) {
492                        Self::insert_at_path(&mut result, path, val);
493                    }
494                }
495                result
496            }
497            ReturnFormat::Flat => {
498                let mut result = serde_json::Map::new();
499                for path in paths {
500                    if let Some(val) = self.get_schema_by_path(path) {
501                        result.insert(path.clone(), val);
502                    }
503                }
504                Value::Object(result)
505            }
506            ReturnFormat::Array => {
507                let mut result = Vec::new();
508                for path in paths {
509                    if let Some(val) = self.get_schema_by_path(path) {
510                        result.push(val);
511                    } else {
512                        result.push(Value::Null);
513                    }
514                }
515                Value::Array(result)
516            }
517        }
518    }
519
520    /// Helper to insert value into nested object at dotted path
521    pub(crate) fn insert_at_path(root: &mut Value, path: &str, value: Value) {
522        let parts: Vec<&str> = path.split('.').collect();
523        let mut current = root;
524
525        for (i, part) in parts.iter().enumerate() {
526            if i == parts.len() - 1 {
527                // Last part - set value
528                if let Value::Object(map) = current {
529                    map.insert(part.to_string(), value);
530                    return; // Done
531                }
532            } else {
533                // Intermediate part - traverse or create
534                // We need to temporarily take the value or use raw pointer manipulation?
535                // serde_json pointer is read-only or requires mutable reference
536
537                if !current.is_object() {
538                    *current = Value::Object(serde_json::Map::new());
539                }
540
541                if let Value::Object(map) = current {
542                    if !map.contains_key(*part) {
543                        map.insert(part.to_string(), Value::Object(serde_json::Map::new()));
544                    }
545                    current = map.get_mut(*part).unwrap();
546                }
547            }
548        }
549    }
550
551    /// Flatten a nested object key-value pair to dotted keys
552    pub fn flatten_object(
553        prefix: &str,
554        value: &Value,
555        result: &mut serde_json::Map<String, Value>,
556    ) {
557        match value {
558            Value::Object(map) => {
559                for (k, v) in map {
560                    let new_key = if prefix.is_empty() {
561                        k.clone()
562                    } else {
563                        format!("{}.{}", prefix, k)
564                    };
565                    Self::flatten_object(&new_key, v, result);
566                }
567            }
568            _ => {
569                result.insert(prefix.to_string(), value.clone());
570            }
571        }
572    }
573
574    pub fn convert_to_format(value: Value, format: ReturnFormat) -> Value {
575        match format {
576            ReturnFormat::Nested => value,
577            ReturnFormat::Flat => {
578                let mut result = serde_json::Map::new();
579                Self::flatten_object("", &value, &mut result);
580                Value::Object(result)
581            }
582            ReturnFormat::Array => {
583                // Convert object values to array? Only if source was object?
584                // Or flattened values?
585                // Usually converting to array disregards keys.
586                if let Value::Object(map) = value {
587                    Value::Array(map.values().cloned().collect())
588                } else if let Value::Array(arr) = value {
589                    Value::Array(arr)
590                } else {
591                    Value::Array(vec![value])
592                }
593            }
594        }
595    }
596}