jiq 3.21.0

Interactive JSON query tool with real-time output
Documentation
use super::common::{DEFAULT_ARRAY_SAMPLE_SIZE, empty_field_names, tracker_for};
use crate::autocomplete::BraceTracker;
use crate::autocomplete::context::{SuggestionContext, analyze_context};
use crate::autocomplete::get_suggestions;

fn get_var_suggestions(query: &str, cursor_pos: usize) -> Vec<String> {
    let tracker = tracker_for(query);
    get_suggestions(
        query,
        cursor_pos,
        None,
        None,
        None,
        empty_field_names(),
        &tracker,
        DEFAULT_ARRAY_SAMPLE_SIZE,
    )
    .into_iter()
    .map(|s| s.text)
    .collect()
}

fn assert_context_is_variable(before_cursor: &str) {
    let tracker = tracker_for(before_cursor);
    let (context, _) = analyze_context(before_cursor, &tracker);
    assert_eq!(
        context,
        SuggestionContext::VariableContext,
        "Expected VariableContext for '{}'",
        before_cursor
    );
}

fn assert_context_is_not_variable(before_cursor: &str) {
    let tracker = tracker_for(before_cursor);
    let (context, _) = analyze_context(before_cursor, &tracker);
    assert_ne!(
        context,
        SuggestionContext::VariableContext,
        "Expected non-VariableContext for '{}'",
        before_cursor
    );
}

mod basic_variable_usage {
    use super::*;

    #[test]
    fn typing_dollar_triggers_variable_context() {
        assert_context_is_variable("$");
    }

    #[test]
    fn typing_variable_name_triggers_context() {
        assert_context_is_variable("$x");
        assert_context_is_variable("$foo");
        assert_context_is_variable("$my_var");
    }

    #[test]
    fn variable_after_pipe() {
        assert_context_is_variable(". | $");
        assert_context_is_variable(". | $x");
    }

    #[test]
    fn variable_inside_parens() {
        assert_context_is_variable("map($");
        assert_context_is_variable("select($x");
    }

    #[test]
    fn variable_after_operator() {
        assert_context_is_variable(". + $");
        assert_context_is_variable(".x == $");
    }
}

mod variable_definition_no_suggestions {
    use super::*;

    #[test]
    fn after_as_keyword() {
        assert_context_is_not_variable(". as $");
        assert_context_is_not_variable(". as $x");
    }

    #[test]
    fn after_as_with_whitespace() {
        assert_context_is_not_variable(". as  $");
        assert_context_is_not_variable(".as $");
    }

    #[test]
    fn in_reduce_definition() {
        assert_context_is_not_variable("reduce .[] as $");
        assert_context_is_not_variable("reduce .[] as $item");
    }

    #[test]
    fn in_foreach_definition() {
        assert_context_is_not_variable("foreach .[] as $");
        assert_context_is_not_variable("foreach .[] as $x");
    }

    #[test]
    fn after_label_keyword() {
        assert_context_is_not_variable("label $");
        assert_context_is_not_variable("label $out");
    }

    #[test]
    fn in_array_destructuring() {
        assert_context_is_not_variable(". as [$");
        assert_context_is_not_variable(". as [$a, $");
        assert_context_is_not_variable(". as [$first, $second");
    }

    #[test]
    fn in_object_destructuring() {
        assert_context_is_not_variable(". as {name: $");
        assert_context_is_not_variable(". as {name: $n, age: $");
    }
}

mod suggestion_generation {
    use super::*;

    #[test]
    fn includes_builtin_variables() {
        let suggestions = get_var_suggestions("$", 1);
        assert!(suggestions.contains(&"$ENV".to_string()));
        assert!(suggestions.contains(&"$__loc__".to_string()));
    }

    #[test]
    fn includes_user_defined_variable() {
        let query = ". as $x | $";
        let suggestions = get_var_suggestions(query, query.len());
        assert!(suggestions.contains(&"$x".to_string()));
    }

    #[test]
    fn includes_variable_from_reduce() {
        let query = "reduce .[] as $item (0; $";
        let suggestions = get_var_suggestions(query, query.len());
        assert!(suggestions.contains(&"$item".to_string()));
    }

    #[test]
    fn includes_multiple_variables() {
        let query = ". as $a | . as $b | $";
        let suggestions = get_var_suggestions(query, query.len());
        assert!(suggestions.contains(&"$a".to_string()));
        assert!(suggestions.contains(&"$b".to_string()));
    }

    #[test]
    fn deduplicates_repeated_definitions() {
        let query = ". as $x | . as $x | $";
        let suggestions = get_var_suggestions(query, query.len());
        let count = suggestions.iter().filter(|s| *s == "$x").count();
        assert_eq!(count, 1, "Variable $x should appear only once");
    }

    #[test]
    fn includes_variables_from_entire_query() {
        let query = ". as $x | $ | . as $y";
        let cursor_pos = query.find("$ |").unwrap() + 1;
        let suggestions = get_var_suggestions(query, cursor_pos);
        assert!(suggestions.contains(&"$x".to_string()));
        assert!(suggestions.contains(&"$y".to_string()));
    }
}

mod case_sensitive_filtering {
    use super::*;

    #[test]
    fn filters_case_sensitively() {
        let query = ". as $Item | $it";
        let suggestions = get_var_suggestions(query, query.len());
        assert!(!suggestions.contains(&"$Item".to_string()));
    }

    #[test]
    fn matches_case_sensitive_prefix() {
        let query = ". as $item | $it";
        let suggestions = get_var_suggestions(query, query.len());
        assert!(suggestions.contains(&"$item".to_string()));
    }

    #[test]
    fn matches_uppercase_variable() {
        let query = ". as $Item | $It";
        let suggestions = get_var_suggestions(query, query.len());
        assert!(suggestions.contains(&"$Item".to_string()));
    }

    #[test]
    fn filters_env_by_prefix() {
        let suggestions = get_var_suggestions("$E", 2);
        assert!(suggestions.contains(&"$ENV".to_string()));
        assert!(!suggestions.contains(&"$__loc__".to_string()));
    }

    #[test]
    fn filters_loc_by_prefix() {
        let suggestions = get_var_suggestions("$__", 3);
        assert!(suggestions.contains(&"$__loc__".to_string()));
        assert!(!suggestions.contains(&"$ENV".to_string()));
    }
}

mod destructuring_variables {
    use super::*;

    #[test]
    fn extracts_from_array_destructuring() {
        let query = ". as [$first, $second] | $";
        let suggestions = get_var_suggestions(query, query.len());
        assert!(suggestions.contains(&"$first".to_string()));
        assert!(suggestions.contains(&"$second".to_string()));
    }

    #[test]
    fn extracts_from_object_destructuring() {
        let query = ". as {name: $n, age: $a} | $";
        let suggestions = get_var_suggestions(query, query.len());
        assert!(suggestions.contains(&"$n".to_string()));
        assert!(suggestions.contains(&"$a".to_string()));
    }
}

mod edge_cases {
    use super::*;

    #[test]
    fn empty_query_with_dollar() {
        let suggestions = get_var_suggestions("$", 1);
        assert!(suggestions.contains(&"$ENV".to_string()));
        assert!(suggestions.contains(&"$__loc__".to_string()));
        assert_eq!(suggestions.len(), 2);
    }

    #[test]
    fn variable_with_underscore() {
        let query = ". as $my_var | $my";
        let suggestions = get_var_suggestions(query, query.len());
        assert!(suggestions.contains(&"$my_var".to_string()));
    }

    #[test]
    fn variable_with_numbers() {
        let query = ". as $x123 | $x";
        let suggestions = get_var_suggestions(query, query.len());
        assert!(suggestions.contains(&"$x123".to_string()));
    }

    #[test]
    fn has_function_not_matched_as_keyword() {
        let tracker = BraceTracker::new();
        let (context, _) = analyze_context("has($", &tracker);
        assert_eq!(context, SuggestionContext::VariableContext);
    }

    #[test]
    fn variable_in_nested_map() {
        let query = ".items as $data | map(. + $";
        let suggestions = get_var_suggestions(query, query.len());
        assert!(suggestions.contains(&"$data".to_string()));
    }
}

mod mid_query_editing {
    use super::*;

    #[test]
    fn editing_at_start() {
        let query = "$ | . as $z";
        let suggestions = get_var_suggestions(query, 1);
        assert!(suggestions.contains(&"$z".to_string()));
    }

    #[test]
    fn editing_in_middle() {
        let query = ". as $x | $ | . as $y";
        let cursor_pos = query.find("$ |").unwrap() + 1;
        let suggestions = get_var_suggestions(query, cursor_pos);
        assert!(suggestions.contains(&"$x".to_string()));
        assert!(suggestions.contains(&"$y".to_string()));
    }
}

mod definition_context_edge_cases {
    use super::*;

    #[test]
    fn no_dollar_sign_in_query() {
        assert_context_is_not_variable("map(.x)");
        assert_context_is_not_variable(". | select(.y > 5)");
    }

    #[test]
    fn as_keyword_at_start() {
        assert_context_is_not_variable("as $x");
    }

    #[test]
    fn label_keyword_at_start() {
        assert_context_is_not_variable("label $out");
    }

    #[test]
    fn as_followed_by_array_bracket() {
        assert_context_is_not_variable(". as [$");
    }

    #[test]
    fn as_followed_by_object_brace() {
        assert_context_is_not_variable(". as {$");
    }

    #[test]
    fn as_with_opening_bracket_no_space() {
        assert_context_is_not_variable(". as[$");
    }

    #[test]
    fn as_with_opening_brace_no_space() {
        assert_context_is_not_variable(". as{$");
    }
}