oak-json 0.0.11

High-performance incremental JSON parser for the oak ecosystem with flexible configuration, optimized for data interchange and configuration files.
Documentation
use oak_core::{GreenNode, GreenTree, OakErrorKind, SourceText, TextEdit, parser::ParseSession};
use oak_json::{JsonElementType as JsonSyntaxKind, language::JsonLanguage, lexer::JsonLexer, parser::JsonParser};
use oak_testing::parsing::ParserTester;
use std::path::PathBuf;

#[test]
#[cfg(feature = "serde")]
fn run_compliance_tests() -> Result<(), oak_core::OakError> {
    let mut root = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
    root.push("tests/compliance");

    let binding = JsonLanguage::standard();
    let parser = JsonParser::new(&binding);

    let tester = ParserTester::new(root).with_extension("json").with_timeout(std::time::Duration::from_secs(5));

    // Run all tests.
    // Note: For files in the invalid directory, ParserTester expects parsing failure or Error nodes.
    let _ = tester.run_tests(&parser);
    Ok(())
}

fn fingerprint(node: &GreenNode<JsonLanguage>, out: &mut Vec<(JsonSyntaxKind, usize, usize)>) {
    out.push((node.kind, node.byte_length as usize, node.children.len()));
    for child in node.children {
        match child {
            GreenTree::Node(n) => fingerprint(n, out),
            GreenTree::Leaf(l) => out.push((l.kind.into(), l.length as usize, 0)),
        }
    }
}

#[test]
fn parse_trailing_comma_error_offset_standard() {
    let text = "{\"a\":1,}";
    let source = SourceText::new(text);
    let binding = JsonLanguage::standard();
    let parser = JsonParser::new(&binding);
    let lexer = JsonLexer::new(&binding);
    let mut cache = ParseSession::<JsonLanguage>::default();

    let out = oak_core::parser::parse(&parser, &lexer, &source, &[], &mut cache);
    let err = out.result.err().or_else(|| out.diagnostics.last().cloned()).expect("expected trailing comma error");
    match err.kind() {
        OakErrorKind::TrailingCommaNotAllowed { offset, .. } => {
            assert_eq!(*offset, text.find('}').unwrap());
        }
        OakErrorKind::SyntaxError { message, offset, .. } => {
            assert!(message.contains("Trailing comma not allowed"));
            assert_eq!(*offset, text.find('}').unwrap());
        }
        _ => panic!("unexpected error kind: {:?}", err.kind()),
    }
}

#[test]
fn parse_unexpected_token_on_bare_key() {
    let text = "{a: 1}";
    let source = SourceText::new(text);
    let binding = JsonLanguage::standard();
    let parser = JsonParser::new(&binding);
    let lexer = JsonLexer::new(&binding);
    let mut cache = ParseSession::<JsonLanguage>::default();

    let out = oak_core::parser::parse(&parser, &lexer, &source, &[], &mut cache);
    let err = out.result.err().or_else(|| out.diagnostics.last().cloned()).expect("expected error for bare key");
    match err.kind() {
        OakErrorKind::UnexpectedCharacter { offset, .. } => assert_eq!(*offset, 1),
        OakErrorKind::SyntaxError { offset, .. } => assert_eq!(*offset, 1),
        OakErrorKind::ExpectedToken { offset, .. } => assert_eq!(*offset, 1),
        OakErrorKind::UnexpectedToken { offset, .. } => assert_eq!(*offset, 1),
        _ => panic!("unexpected error kind: {:?}", err.kind()),
    }
}

#[test]
fn parse_unexpected_eof_in_object() {
    let text = "{\"a\":";
    let source = SourceText::new(text);
    let binding = JsonLanguage::standard();
    let parser = JsonParser::new(&binding);
    let lexer = JsonLexer::new(&binding);
    let mut cache = ParseSession::<JsonLanguage>::default();

    let out = oak_core::parser::parse(&parser, &lexer, &source, &[], &mut cache);
    let err = out.result.err().or_else(|| out.diagnostics.last().cloned()).expect("expected parse error");
    assert!(matches!(err.kind(), OakErrorKind::UnexpectedEof { .. }));
}

#[test]
fn incremental_multi_edits_matches_full_parse_shape() {
    let binding = JsonLanguage::standard();
    let parser = JsonParser::new(&binding);
    let lexer = JsonLexer::new(&binding);
    let mut cache = ParseSession::<JsonLanguage>::default();

    let original = "{\"a\": 1, \"b\": 2, \"c\": 3}";
    let source1 = SourceText::new(original);
    let out1 = oak_core::parser::parse(&parser, &lexer, &source1, &[], &mut cache);
    assert!(out1.result.is_ok());
    assert!(out1.diagnostics.is_empty());

    let a_pos = original.find("\"a\": 1").unwrap();
    let b_pos = original.find("\"b\": 2").unwrap();

    let mut edited = original.to_string();
    edited.replace_range(b_pos + "\"b\": ".len()..b_pos + "\"b\": 2".len(), "20");
    edited.replace_range(a_pos + "\"a\": ".len()..a_pos + "\"a\": 1".len(), "10");

    let source2 = SourceText::new(edited.as_str());
    let edit1 = TextEdit { span: (a_pos + "\"a\": ".len()..a_pos + "\"a\": 1".len()).into(), text: "10".to_string().into() };
    let edit2 = TextEdit { span: (b_pos + "\"b\": ".len()..b_pos + "\"b\": 2".len()).into(), text: "20".to_string().into() };
    let edits = vec![edit1, edit2];

    let out2 = oak_core::parser::parse(&parser, &lexer, &source2, &edits, &mut cache);
    assert!(out2.result.is_ok());

    let mut cache_full = ParseSession::<JsonLanguage>::default();
    let out3 = oak_core::parser::parse(&parser, &lexer, &source2, &[], &mut cache_full);

    let mut a = Vec::new();
    let mut b = Vec::new();
    let inc_root = out2.result.unwrap();
    let full_root = out3.result.unwrap();

    fingerprint(inc_root, &mut a);
    fingerprint(full_root, &mut b);

    assert_eq!(a, b);
}