jiq 2.21.1

Interactive JSON query tool with real-time output
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
//! State management for result statistics
//!
//! This module provides the `StatsState` struct which caches computed statistics
//! about query results and provides display formatting.

use crate::app::App;
use crate::stats::parser::StatsParser;
use crate::stats::types::ResultStats;

/// Update stats based on the last successful result from the App
///
/// This is the delegation function called by `App::update_stats()`.
/// It extracts the last successful unformatted result and computes stats if available.
pub fn update_stats_from_app(app: &mut App) {
    if let Some(result) = &app.query.last_successful_result_unformatted {
        app.stats.compute(result);
    }
}

/// State for managing cached result statistics
///
/// `StatsState` caches the computed statistics for the current query result.
/// When a syntax error occurs, the cached stats are preserved to show the
/// stats from the last successful result (per Requirement 4.4).
#[derive(Debug, Clone, Default)]
pub struct StatsState {
    /// Cached stats for the current/last successful result
    stats: Option<ResultStats>,
}

impl StatsState {
    /// Compute stats from a result string and cache them
    ///
    /// This parses the result string using the fast character-based parser
    /// and caches the computed statistics.
    pub fn compute(&mut self, result: &str) {
        let trimmed = result.trim();
        if trimmed.is_empty() {
            // Don't update stats for empty results (preserves last stats)
            return;
        }
        self.stats = Some(StatsParser::parse(result));
    }

    /// Get the display string for the stats bar
    ///
    /// Returns `None` if no stats have been computed yet.
    pub fn display(&self) -> Option<String> {
        self.stats.as_ref().map(|s| s.to_string())
    }

    /// Get a reference to the cached stats (used in tests)
    #[cfg(test)]
    pub fn stats(&self) -> Option<&ResultStats> {
        self.stats.as_ref()
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::stats::types::ElementType;
    use proptest::prelude::*;

    #[test]
    fn test_default_state_has_no_stats() {
        let state = StatsState::default();
        assert!(state.stats().is_none());
        assert!(state.display().is_none());
    }

    #[test]
    fn test_compute_array_stats() {
        let mut state = StatsState::default();
        state.compute("[1, 2, 3]");

        assert!(state.stats().is_some());
        assert_eq!(
            state.stats(),
            Some(&ResultStats::Array {
                count: 3,
                element_type: ElementType::Numbers
            })
        );
        assert_eq!(state.display(), Some("Array [3 numbers]".to_string()));
    }

    #[test]
    fn test_compute_object_stats() {
        let mut state = StatsState::default();
        state.compute(r#"{"key": "value"}"#);

        assert_eq!(state.stats(), Some(&ResultStats::Object));
        assert_eq!(state.display(), Some("Object".to_string()));
    }

    #[test]
    fn test_compute_string_stats() {
        let mut state = StatsState::default();
        state.compute(r#""hello world""#);

        assert_eq!(state.stats(), Some(&ResultStats::String));
        assert_eq!(state.display(), Some("String".to_string()));
    }

    #[test]
    fn test_compute_number_stats() {
        let mut state = StatsState::default();
        state.compute("42");

        assert_eq!(state.stats(), Some(&ResultStats::Number));
        assert_eq!(state.display(), Some("Number".to_string()));
    }

    #[test]
    fn test_compute_boolean_stats() {
        let mut state = StatsState::default();
        state.compute("true");

        assert_eq!(state.stats(), Some(&ResultStats::Boolean));
        assert_eq!(state.display(), Some("Boolean".to_string()));
    }

    #[test]
    fn test_compute_null_stats() {
        let mut state = StatsState::default();
        state.compute("null");

        assert_eq!(state.stats(), Some(&ResultStats::Null));
        assert_eq!(state.display(), Some("null".to_string()));
    }

    #[test]
    fn test_compute_stream_stats() {
        let mut state = StatsState::default();
        state.compute("{}\n{}\n{}");

        assert_eq!(state.stats(), Some(&ResultStats::Stream { count: 3 }));
        assert_eq!(state.display(), Some("Stream [3]".to_string()));
    }

    #[test]
    fn test_empty_result_preserves_stats() {
        let mut state = StatsState::default();
        state.compute("[1, 2, 3]");
        let original_stats = state.stats().cloned();

        // Empty result should not update stats (preserves last successful)
        state.compute("");
        assert_eq!(state.stats().cloned(), original_stats);

        // Whitespace-only result should also preserve
        state.compute("   ");
        assert_eq!(state.stats().cloned(), original_stats);
    }

    #[test]
    fn test_stats_update_on_new_result() {
        let mut state = StatsState::default();

        state.compute("[1, 2, 3]");
        assert_eq!(state.display(), Some("Array [3 numbers]".to_string()));

        state.compute(r#"{"a": 1}"#);
        assert_eq!(state.display(), Some("Object".to_string()));
    }

    // =========================================================================
    // Property-Based Tests
    // =========================================================================

    /// Strategy to generate a simple JSON value (non-container)
    fn arb_simple_json_value() -> impl Strategy<Value = String> {
        prop_oneof![
            // Numbers
            (-1000i64..1000).prop_map(|n| n.to_string()),
            // Strings (simple, no special chars)
            "[a-zA-Z0-9]{0,10}".prop_map(|s| format!(r#""{}""#, s)),
            // Booleans
            Just("true".to_string()),
            Just("false".to_string()),
            // Null
            Just("null".to_string()),
        ]
    }

    /// Strategy to generate valid JSON values
    fn arb_valid_json() -> impl Strategy<Value = String> {
        prop_oneof![
            // Simple values
            arb_simple_json_value(),
            // Arrays
            prop::collection::vec(arb_simple_json_value(), 0..10)
                .prop_map(|elements| format!("[{}]", elements.join(", "))),
            // Objects
            prop::collection::vec(("[a-z]{1,5}", arb_simple_json_value()), 0..5).prop_map(
                |pairs| {
                    let fields: Vec<String> = pairs
                        .iter()
                        .map(|(k, v)| format!(r#""{}": {}"#, k, v))
                        .collect();
                    format!("{{{}}}", fields.join(", "))
                }
            ),
        ]
    }

    /// Strategy to generate "error" results (empty or whitespace-only strings)
    /// These simulate what happens when a query fails - the result is empty
    fn arb_error_result() -> impl Strategy<Value = String> {
        prop_oneof![
            Just("".to_string()),
            Just("   ".to_string()),
            Just("\n".to_string()),
            Just("\t".to_string()),
            Just("  \n  ".to_string()),
        ]
    }

    // Feature: stats-bar, Property 5: Stats persistence on error
    // *For any* sequence of queries where a valid query is followed by an invalid
    // query, the stats SHALL continue to display the stats from the last successful result.
    // **Validates: Requirements 4.4**
    proptest! {
        #![proptest_config(ProptestConfig::with_cases(100))]

        #[test]
        fn prop_stats_persist_on_error(
            valid_json in arb_valid_json(),
            error_result in arb_error_result()
        ) {
            let mut state = StatsState::default();

            // First, compute stats from a valid result
            state.compute(&valid_json);
            let stats_after_valid = state.stats().cloned();
            let display_after_valid = state.display();

            // Stats should be computed (not None) for valid JSON
            prop_assert!(
                stats_after_valid.is_some(),
                "Stats should be computed for valid JSON: '{}'",
                valid_json
            );

            // Now simulate an error (empty result)
            state.compute(&error_result);
            let stats_after_error = state.stats().cloned();
            let display_after_error = state.display();

            // Stats should be preserved (same as before the error)
            prop_assert_eq!(
                &stats_after_error, &stats_after_valid,
                "Stats should persist after error. Before: {:?}, After: {:?}",
                &stats_after_valid, &stats_after_error
            );

            // Display should also be preserved
            prop_assert_eq!(
                &display_after_error, &display_after_valid,
                "Display should persist after error. Before: {:?}, After: {:?}",
                &display_after_valid, &display_after_error
            );
        }

        #[test]
        fn prop_multiple_errors_preserve_last_valid_stats(
            valid_json in arb_valid_json(),
            error_count in 1usize..5
        ) {
            let mut state = StatsState::default();

            // Compute stats from valid result
            state.compute(&valid_json);
            let original_stats = state.stats().cloned();
            let original_display = state.display();

            prop_assert!(
                original_stats.is_some(),
                "Stats should be computed for valid JSON"
            );

            // Apply multiple error results
            for _ in 0..error_count {
                state.compute("");
            }

            // Stats should still be preserved
            prop_assert_eq!(
                state.stats().cloned(), original_stats,
                "Stats should persist after {} errors",
                error_count
            );
            prop_assert_eq!(
                state.display(), original_display,
                "Display should persist after {} errors",
                error_count
            );
        }

        #[test]
        fn prop_new_valid_result_updates_stats(
            first_json in arb_valid_json(),
            second_json in arb_valid_json()
        ) {
            let mut state = StatsState::default();

            // Compute stats from first valid result
            state.compute(&first_json);
            let first_stats = state.stats().cloned();

            // Compute stats from second valid result
            state.compute(&second_json);
            let second_stats = state.stats().cloned();

            // Both should have stats
            prop_assert!(first_stats.is_some(), "First result should have stats");
            prop_assert!(second_stats.is_some(), "Second result should have stats");

            // The second stats should reflect the second JSON
            // (We can't easily compare the exact stats without parsing,
            // but we can verify that stats are computed)
            let expected_stats = StatsParser::parse(&second_json);
            prop_assert_eq!(
                second_stats, Some(expected_stats),
                "Stats should be updated to reflect second JSON"
            );
        }
    }

    // =========================================================================
    // App Integration Tests for Stats
    // =========================================================================
    // These tests verify the update_stats_from_app() delegation function

    use crate::test_utils::test_helpers::test_app;

    #[test]
    fn test_update_stats_from_app_with_object() {
        let json = r#"{"name": "Alice", "age": 30}"#;
        let mut app = test_app(json);

        // Initial query executes identity filter, which sets last_successful_result_unformatted
        update_stats_from_app(&mut app);

        assert_eq!(app.stats.display(), Some("Object".to_string()));
    }

    #[test]
    fn test_update_stats_from_app_with_array() {
        let json = r#"[1, 2, 3, 4, 5]"#;
        let mut app = test_app(json);

        update_stats_from_app(&mut app);

        assert_eq!(app.stats.display(), Some("Array [5 numbers]".to_string()));
    }

    #[test]
    fn test_update_stats_from_app_no_result() {
        let json = r#"{"test": true}"#;
        let mut app = test_app(json);

        // Clear the last successful result to simulate no result available
        app.query.last_successful_result_unformatted = None;

        // Stats should remain unchanged (None)
        let stats_before = app.stats.display();
        update_stats_from_app(&mut app);
        let stats_after = app.stats.display();

        assert_eq!(stats_before, stats_after);
    }

    #[test]
    fn test_update_stats_from_app_preserves_on_error() {
        let json = r#"[1, 2, 3]"#;
        let mut app = test_app(json);

        // First update with valid result
        update_stats_from_app(&mut app);
        assert_eq!(app.stats.display(), Some("Array [3 numbers]".to_string()));

        // Simulate an error by setting result to error but keeping last_successful_result_unformatted
        app.query.result = Err("syntax error".to_string());
        // Note: last_successful_result_unformatted is still set from the initial query

        // Update stats again - should still show the last successful stats
        update_stats_from_app(&mut app);
        assert_eq!(app.stats.display(), Some("Array [3 numbers]".to_string()));
    }

    #[test]
    fn test_update_stats_from_app_updates_on_new_query() {
        let json = r#"{"items": [1, 2, 3]}"#;
        let mut app = test_app(json);

        // Initial stats for the object
        update_stats_from_app(&mut app);
        assert_eq!(app.stats.display(), Some("Object".to_string()));

        // Execute a new query that returns an array
        app.query.execute(".items");
        update_stats_from_app(&mut app);

        assert_eq!(app.stats.display(), Some("Array [3 numbers]".to_string()));
    }
}