json-eval-rs 0.0.92

High-performance JSON Logic evaluator with schema validation and dependency tracking. Built on blazing-fast Rust engine.
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
use crate::jsoneval::path_utils;
use crate::topo_sort::common::{collect_transitive_deps, compute_evaluation_batches};
use crate::JSONEval;
/// Topological sorting for legacy JSONEval
use indexmap::{IndexMap, IndexSet};

pub fn topological_sort(lib: &JSONEval) -> Result<Vec<Vec<String>>, String> {
    let mut sorted = IndexSet::new();
    let mut visited = IndexSet::new();
    let mut visiting = IndexSet::new();

    // Filter evaluations to exclude layout, rules, config, dependents, options, condition, value
    let filtered_evaluations: IndexMap<String, IndexSet<String>> = lib
        .evaluations
        .keys()
        .filter(|key| {
            let is_params = key.contains("/$params/");
            if is_params {
                return true;
            }

            !key.contains("/dependents/")
                && !key.contains("/rules/")
                && !key.contains("/options/")
                && !key.contains("/condition/")
                && !key.contains("/$layout/")
                && !key.contains("/config/")
                && !key.contains("/items/")
                && !key.ends_with("/options")
                && !key.ends_with("/value")
                && (key.starts_with("#/$") && !key.contains("/value/"))
        })
        .map(|key| {
            let deps = lib.dependencies.get(key).cloned().unwrap_or_default();
            (key.clone(), deps)
        })
        .collect();

    // Group table evaluations and merge dependencies
    let mut table_groups: IndexMap<String, IndexSet<String>> = IndexMap::new();
    let mut evaluation_to_table: IndexMap<String, String> = IndexMap::new();

    // First pass: identify all table paths from $table keys
    let mut table_paths: IndexSet<String> = IndexSet::new();
    for table_key in lib.tables.keys() {
        // Extract table path by removing "/$table" suffix
        let table_path = table_key.to_string();
        table_paths.insert(table_path);
    }

    // Create a mapping of normalized names to table paths
    let mut normalized_to_table: IndexMap<String, String> = IndexMap::new();
    for tp in &table_paths {
        // Extract the last segment (table name) for matching
        if let Some(last_segment) = tp.rsplit('/').next() {
            normalized_to_table.insert(last_segment.to_string(), tp.clone());
        }
    }

    // Create a mapping from JSON pointer paths to evaluation keys for dependency resolution
    let mut pointer_to_eval: IndexMap<String, String> = IndexMap::new();
    for eval_key in filtered_evaluations.keys() {
        // Convert evaluation keys to JSON pointers
        let pointer = path_utils::normalize_to_json_pointer(eval_key).into_owned();
        pointer_to_eval.insert(pointer, eval_key.clone());
    }

    // Also add table paths to pointer_to_eval for dependency resolution
    for table_path in &table_paths {
        let pointer = path_utils::normalize_to_json_pointer(table_path).into_owned();
        pointer_to_eval.insert(pointer, table_path.clone());
    }

    // Second pass: group ALL evaluations by table and merge dependencies
    // Process ALL evaluations (not just filtered ones) to capture table dependencies
    for (eval_key, deps) in lib.evaluations.keys().map(|k| {
        let deps = lib.dependencies.get(k).cloned().unwrap_or_default();
        (k, deps)
    }) {
        // Find which table this evaluation belongs to
        // Use longest match to handle nested table names correctly
        // (e.g., ILB_SURRENDER vs ILB_SURRENDER_BENPAY_CLONE)
        let table_path_opt = table_paths
            .iter()
            .filter(|tp| eval_key.starts_with(tp.as_str()))
            .max_by_key(|tp| tp.len());

        if let Some(table_path) = table_path_opt {
            evaluation_to_table.insert(eval_key.clone(), table_path.clone());

            // Normalize dependencies to table paths where applicable
            let normalized_deps: IndexSet<String> = deps
                .iter()
                .filter_map(|dep| {
                    // Ignore self column dependencies (starts with $ and no dot/slash)
                    if dep.starts_with('$') && !dep.contains('.') && !dep.contains('/') {
                        return None;
                    }

                    // Check if dependency is a JSON pointer path that maps to an evaluation
                    if let Some(eval_key) = pointer_to_eval.get(dep) {
                        return Some(eval_key.clone());
                    }

                    // Check if dependency references another table path (flexible matching)
                    for tp in &table_paths {
                        let tp_str = tp.as_str();
                        let tp_with_slash = format!("{}/", tp_str);

                        // Match if:
                        // 1. dep equals table path exactly (for static tables)
                        // 2. dep starts with table path (for sub-fields like table.0.field)
                        if tp_str != table_path.as_str() {
                            if dep == tp_str || dep.starts_with(&tp_with_slash) {
                                return Some(tp.clone());
                            }
                        }
                    }

                    // Check if dependency matches a normalized table name
                    if let Some(target_table) = normalized_to_table.get(dep) {
                        if target_table != table_path {
                            return Some(target_table.clone());
                        }
                    }

                    // Keep non-table dependencies as-is (but not self-table deps)
                    let table_path_with_slash = format!("{}/", table_path.as_str());
                    if !dep.starts_with(table_path.as_str())
                        && !dep.starts_with(&table_path_with_slash)
                    {
                        Some(dep.clone())
                    } else {
                        None
                    }
                })
                .collect();

            table_groups
                .entry(table_path.clone())
                .or_insert_with(IndexSet::new)
                .extend(normalized_deps);
        }
    }

    // Create a unified graph and resolve JSON pointer dependencies in table groups
    let mut unified_graph: IndexMap<String, IndexSet<String>> = IndexMap::new();

    // Add table groups with resolved dependencies
    for (table_path, deps) in &table_groups {
        let resolved_deps: IndexSet<String> = deps
            .iter()
            .filter_map(|dep| {
                // Filter out self-references (table depending on itself)
                // This is common for iterative calculations within the same table
                if dep == table_path {
                    return None;
                }

                // Try to resolve JSON pointer path to evaluation key
                if let Some(eval_key) = pointer_to_eval.get(dep) {
                    // Also filter out resolved self-references
                    if eval_key == table_path {
                        return None;
                    }
                    Some(eval_key.clone())
                } else {
                    // Keep as-is if not resolvable
                    Some(dep.clone())
                }
            })
            .collect();
        unified_graph.insert(table_path.clone(), resolved_deps);
    }

    // Add non-table evaluations to the unified graph
    for (eval_key, deps) in &filtered_evaluations {
        if !evaluation_to_table.contains_key(eval_key) {
            // Normalize dependencies for non-table evaluations
            let mut normalized_deps: IndexSet<String> = IndexSet::new();

            for dep in deps {
                // Check if dependency is a JSON pointer path that maps to an evaluation
                if let Some(eval_key) = pointer_to_eval.get(dep) {
                    normalized_deps.insert(eval_key.clone());
                    continue;
                }

                // Check if dependency references a table/array path
                let mut found_table = false;
                for tp in &table_paths {
                    let tp_str = tp.as_str();
                    let tp_with_slash = format!("{}/", tp_str);

                    // Match if:
                    // 1. dep equals table path exactly (for static tables/arrays)
                    // 2. dep starts with table path/ (for sub-fields)
                    if dep == tp_str || dep.starts_with(&tp_with_slash) {
                        normalized_deps.insert(tp.clone());
                        found_table = true;
                        break;
                    }
                }

                if found_table {
                    continue;
                }

                // OPTIMIZED: Check if dependency is a static array with evaluated fields
                // Use consistent path utilities for conversion
                let dep_as_pointer = path_utils::normalize_to_json_pointer(dep);
                let dep_as_eval_prefix = format!("#{}", dep_as_pointer);
                let has_field_evaluations = lib.evaluations.keys().any(|k| {
                    k.starts_with(&dep_as_eval_prefix)
                        && k.len() > dep_as_eval_prefix.len()
                        && k[dep_as_eval_prefix.len()..].starts_with('/')
                });

                if has_field_evaluations {
                    // Add all field evaluations as dependencies
                    for field_eval_key in lib.evaluations.keys() {
                        if field_eval_key.starts_with(&dep_as_eval_prefix)
                            && field_eval_key.len() > dep_as_eval_prefix.len()
                            && field_eval_key[dep_as_eval_prefix.len()..].starts_with('/')
                        {
                            normalized_deps.insert(field_eval_key.clone());
                        }
                    }
                } else {
                    normalized_deps.insert(dep.clone());
                }
            }

            unified_graph.insert(eval_key.clone(), normalized_deps);
        }
    }

    // ==========================================
    // 3-PHASE PROCESSING: Dependencies → Tables → Rest
    // ==========================================

    // Identify all table dependencies (transitive)
    // This includes all non-table nodes that tables transitively depend on
    let mut table_dependencies = IndexSet::new();
    for table_path in &table_paths {
        if let Some(deps) = unified_graph.get(table_path) {
            collect_transitive_deps(deps, &unified_graph, &table_paths, &mut table_dependencies);
        }
    }

    // CRITICAL: Expand to complete transitive closure
    // Ensure ALL non-table dependencies of phase 1 nodes are also in phase 1
    // Example: If table depends on A, and A depends on B, then both A and B are in phase 1
    let mut expanded = true;
    while expanded {
        expanded = false;
        let current_deps: Vec<String> = table_dependencies.iter().cloned().collect();
        for dep in &current_deps {
            if let Some(sub_deps) = unified_graph.get(dep) {
                for sub_dep in sub_deps {
                    // Skip tables - they stay in phase 2
                    if !table_paths.contains(sub_dep) {
                        if table_dependencies.insert(sub_dep.clone()) {
                            expanded = true; // Found new dependency, need another pass
                        }
                    }
                }
            }
        }
    }

    // Separate nodes into phases
    let mut phase1_nodes = Vec::new(); // Table dependencies (non-tables needed by tables)
    let mut phase2_nodes = Vec::new(); // Tables
    let mut phase3_nodes = Vec::new(); // Everything else

    for node in unified_graph.keys() {
        if table_paths.contains(node) {
            // Phase 2: Tables
            phase2_nodes.push(node.clone());
        } else if table_dependencies.contains(node) {
            // Phase 1: Non-table dependencies of tables
            phase1_nodes.push(node.clone());
        } else {
            // Phase 3: Remaining nodes
            phase3_nodes.push(node.clone());
        }
    }

    // Sort phase 1 and phase 3 by dependency order (nodes with fewer deps first)
    // This provides a better starting order for topological processing
    let sort_by_deps = |a: &String, b: &String| {
        let a_deps = unified_graph.get(a).map(|d| d.len()).unwrap_or(0);
        let b_deps = unified_graph.get(b).map(|d| d.len()).unwrap_or(0);
        a_deps.cmp(&b_deps).then_with(|| a.cmp(b))
    };

    phase1_nodes.sort_by(sort_by_deps);
    phase3_nodes.sort_by(sort_by_deps);

    // PHASE 1: Process table dependencies (respecting their internal dependencies)
    for node in &phase1_nodes {
        if !visited.contains(node) {
            let deps = unified_graph.get(node).cloned().unwrap_or_default();
            // visit_node will recursively process dependencies in correct order
            visit_node(
                lib,
                node,
                &deps,
                &unified_graph,
                &mut visited,
                &mut visiting,
                &mut sorted,
            )?;
        }
    }

    // PHASE 2: Process tables in dependency order
    // Sort tables by their dependencies (tables with fewer/no table deps come first)
    phase2_nodes.sort_by(|a, b| {
        let a_deps = unified_graph.get(a).map(|d| d.len()).unwrap_or(0);
        let b_deps = unified_graph.get(b).map(|d| d.len()).unwrap_or(0);

        // Check if A depends on B or B depends on A
        let a_deps_on_b = unified_graph
            .get(a)
            .map(|deps| deps.contains(b))
            .unwrap_or(false);
        let b_deps_on_a = unified_graph
            .get(b)
            .map(|deps| deps.contains(a))
            .unwrap_or(false);

        if a_deps_on_b {
            std::cmp::Ordering::Greater // A depends on B, so B comes first
        } else if b_deps_on_a {
            std::cmp::Ordering::Less // B depends on A, so A comes first
        } else {
            // No direct dependency, sort by dependency count then alphabetically
            a_deps.cmp(&b_deps).then_with(|| a.cmp(b))
        }
    });

    for node in &phase2_nodes {
        if !visited.contains(node) {
            let deps = unified_graph.get(node).cloned().unwrap_or_default();
            visit_node(
                lib,
                node,
                &deps,
                &unified_graph,
                &mut visited,
                &mut visiting,
                &mut sorted,
            )?;
        }
    }

    // PHASE 3: Process remaining nodes (respecting their internal dependencies)
    for node in &phase3_nodes {
        if !visited.contains(node) {
            let deps = unified_graph.get(node).cloned().unwrap_or_default();
            // visit_node will recursively process dependencies in correct order
            visit_node(
                lib,
                node,
                &deps,
                &unified_graph,
                &mut visited,
                &mut visiting,
                &mut sorted,
            )?;
        }
    }

    // Now convert the flat sorted list into batches
    // Batch nodes by their "level" - all nodes at the same level can run together
    let batches = compute_evaluation_batches(&sorted, &unified_graph, &table_paths);

    Ok(batches)
}

/// Compute evaluation batches from a topologically sorted list
///
/// Algorithm: Assign each node to the earliest batch where all its dependencies
/// have been processed in previous batches.

pub fn visit_node_with_priority(
    lib: &JSONEval,
    node: &str,
    deps: &IndexSet<String>,
    graph: &IndexMap<String, IndexSet<String>>,
    visited: &mut IndexSet<String>,
    visiting: &mut IndexSet<String>,
    sorted: &mut IndexSet<String>,
    table_paths: &IndexSet<String>,
) -> Result<(), String> {
    if visiting.contains(node) {
        return Err(format!("Circular dependency detected involving: {}", node));
    }

    if visited.contains(node) {
        return Ok(());
    }

    visiting.insert(node.to_string());

    // Sort dependencies by priority: non-tables first
    let mut sorted_deps: Vec<String> = deps.iter().cloned().collect();
    sorted_deps.sort_by(|a, b| {
        let a_is_table = table_paths.contains(a);
        let b_is_table = table_paths.contains(b);

        match (a_is_table, b_is_table) {
            (false, true) => std::cmp::Ordering::Less, // non-table before table
            (true, false) => std::cmp::Ordering::Greater, // table after non-table
            _ => a.cmp(b),                             // same priority, sort alphabetically
        }
    });

    // Process dependencies in priority order
    for dep in sorted_deps {
        if let Some(dep_deps) = graph.get(&dep) {
            visit_node_with_priority(
                lib,
                &dep,
                dep_deps,
                graph,
                visited,
                visiting,
                sorted,
                table_paths,
            )?;
        }
    }

    visiting.swap_remove(node);
    visited.insert(node.to_string());
    sorted.insert(node.to_string());

    Ok(())
}

pub fn visit_node(
    lib: &JSONEval,
    node: &str,
    deps: &IndexSet<String>,
    graph: &IndexMap<String, IndexSet<String>>,
    visited: &mut IndexSet<String>,
    visiting: &mut IndexSet<String>,
    sorted: &mut IndexSet<String>,
) -> Result<(), String> {
    if visiting.contains(node) {
        return Err(format!("Circular dependency detected involving: {}", node));
    }

    if visited.contains(node) {
        return Ok(());
    }

    visiting.insert(node.to_string());

    for dep in deps {
        if let Some(dep_deps) = graph.get(dep) {
            visit_node(lib, dep, dep_deps, graph, visited, visiting, sorted)?;
        }
    }

    visiting.swap_remove(node);
    visited.insert(node.to_string());
    sorted.insert(node.to_string());

    Ok(())
}