over 0.6.5

OVER: the best data format.
Documentation
#![allow(clippy::cyclomatic_complexity)]

extern crate num_bigint;
extern crate num_rational;
extern crate num_traits;
#[macro_use]
extern crate over;

mod errors;

use num_traits::ToPrimitive;
use over::obj::Obj;
use over::types::Type;
use over::value::Value;

// Display nicely-formatted values on failure.
macro_rules! test_eq {
    ($left:expr, $right:expr) => {{
        if $left != $right {
            panic!(format!(
                "Left did not equal right.\nLeft: {:?}\nRight: {:?}\n",
                $left, $right
            ));
        }
    }};
}

// Make comparisons with ints a bit more concise.
fn get_int(obj: &Obj, field: &str) -> i64 {
    obj.get_int(field).unwrap().to_i64().unwrap()
}

// Test parsing of empty file.
#[test]
fn empty() {
    let obj = Obj::from_file("tests/test_files/empty.over").unwrap();

    test_eq!(obj.len(), 0);
}

// Test reading basic Ints, Strs, Bools, and Null.
// Also test that whitespace and comments are correctly ignored.
#[test]
fn basic() {
    let obj = Obj::from_file("tests/test_files/basic.over").unwrap();

    test_eq!(get_int(&obj, "_a1"), 1);
    test_eq!(get_int(&obj, "a2"), 2);
    test_eq!(get_int(&obj, "_"), 0);
    test_eq!(obj.get("b").unwrap(), "Smörgåsbord");
    test_eq!(get_int(&obj, "c"), 10);
    test_eq!(get_int(&obj, "d"), 20);
    test_eq!(get_int(&obj, "eee"), 2);
    test_eq!(get_int(&obj, "f"), 3);
    test_eq!(get_int(&obj, "g_"), 4);
    test_eq!(obj.get("Hello").unwrap(), "Hello");
    test_eq!(obj.get("i_robot").unwrap(), "not #a comment");
    test_eq!(get_int(&obj, "j"), 4);
    test_eq!(obj.get("k").unwrap(), "hi");
    test_eq!(obj.get("l").unwrap(), "$\\\"");
    test_eq!(obj.get("m").unwrap(), "m");
    test_eq!(obj.get("n").unwrap(), true);
    test_eq!(obj.get("o").unwrap(), false);
    test_eq!(obj.get("p").unwrap(), "Hello");
    test_eq!(get_int(&obj, "q"), 0);
    test_eq!(obj.get("r").unwrap(), Value::Null);
    test_eq!(obj.get("s").unwrap(), '\'');
    test_eq!(obj.get("t").unwrap(), '\n');
    test_eq!(obj.get("u").unwrap(), ' ');
    test_eq!(obj.get("v").unwrap(), '\'');
    test_eq!(obj.get("w").unwrap(), '$');
    test_eq!(obj.get_frac("x").unwrap(), frac!(1, 1));
    test_eq!(obj.get("x").unwrap().get_frac().unwrap(), frac!(1, 1));
}

// Test the example from the README.
#[test]
fn example() {
    let obj = Obj::from_file("tests/test_files/example.over").unwrap();

    assert_eq!(obj.get("receipt").unwrap(), "Oz-Ware Purchase Invoice");
    assert_eq!(obj.get("date").unwrap(), "2012-08-06");
    assert_eq!(
        obj.get("customer").unwrap(),
        obj! {
            "first_name" => "Dorothy",
            "family_name" => "Gale"
        }
    );

    assert_eq!(
        obj.get("items").unwrap(),
        arr![
            obj! {
                "part_no" => "A4786",
                "descrip" => "Water Bucket (Filled)",
                "price" => frac!(147,100),
                "quantity" => 4
            },
            obj! {
                "part_no" => "E1628",
                "descrip" => "High Heeled \"Ruby\" Slippers",
                "size" => 8,
                "price" => frac!(1337,10),
                "quantity" => 1
            },
        ]
    );

    assert!(
        obj.get("bill_to").unwrap()
            == obj! {
                "street" => "123 Tornado Alley\nSuite 16",
                "city" => "East Centerville",
                "state" => "KS",
            }
            || obj.get("bill_to").unwrap()
                == obj! {
                    "street" => "123 Tornado Alley\r\nSuite 16",
                    "city" => "East Centerville",
                    "state" => "KS",
                }
    );

    assert_eq!(obj.get("ship_to").unwrap(), obj.get("bill_to").unwrap());

    assert_eq!(
        obj.get("specialDelivery").unwrap(),
        "Follow the Yellow Brick Road to the Emerald City. \
         Pay no attention to the man behind the curtain."
    );
}

// Test parsing of sub-Objs.
#[test]
fn obj() {
    let obj = Obj::from_file("tests/test_files/obj.over").unwrap();

    test_eq!(obj.get_obj("empty").unwrap().len(), 0);
    test_eq!(obj.get_obj("empty2").unwrap().len(), 0);

    assert!(!obj.contains("bools"));
    let bools = obj! {"t" => true, "f" => false};

    let outie = obj.get_obj("outie").unwrap();
    test_eq!(outie.get_parent().unwrap(), bools);
    test_eq!(get_int(&outie, "z"), 0);

    let inner = outie.get_obj("inner").unwrap();
    test_eq!(get_int(&inner, "z"), 1);
    let innie = inner.get_obj("innie").unwrap();
    test_eq!(get_int(&innie, "a"), 1);
    test_eq!(inner.get("b").unwrap(), tup!(1, 2,));

    test_eq!(get_int(&outie, "c"), 3);
    test_eq!(outie.get("d").unwrap(), obj! {});

    let obj_arr = obj.get_obj("obj_arr").unwrap();
    test_eq!(obj_arr.get("arr").unwrap(), arr![1, 2, 3]);

    test_eq!(obj.get_int("dot").unwrap(), int!(1));
    test_eq!(obj.get_bool("dot_glob").unwrap(), true);
    test_eq!(obj.get_int("dot_tup1").unwrap(), int!(1));
    test_eq!(obj.get_int("dot_tup2").unwrap(), int!(2));
    test_eq!(obj.get_int("dot_arr").unwrap(), int!(1));
    test_eq!(obj.get_int("dot_op").unwrap(), int!(4));

    test_eq!(obj.get_str("dot_var").unwrap(), "test");

    assert_eq!(obj.iter().count(), 13);
    let value = obj.values().last();
    assert!(!value.unwrap().is_null());
}

// Test that globals are referenced correctly and don't get included as fields.
#[test]
fn globals() {
    let obj = Obj::from_file("tests/test_files/globals.over").unwrap();

    let sub = obj.get_obj("sub").unwrap();

    test_eq!(sub.get_int("a").unwrap(), int!(1));
    test_eq!(get_int(&sub, "b"), 2);
    test_eq!(sub.len(), 2);

    test_eq!(get_int(&obj, "c"), 2);
    test_eq!(obj.len(), 2);
}

// Test parsing of numbers.
#[test]
fn numbers() {
    let obj = Obj::from_file("tests/test_files/numbers.over").unwrap();

    test_eq!(get_int(&obj, "neg"), -4);
    test_eq!(obj.get_frac("pos").unwrap(), frac!(4, 1));
    test_eq!(obj.get_frac("neg_zero").unwrap(), frac!(0, 1));
    test_eq!(obj.get_frac("pos_zero").unwrap(), frac!(0, 1));

    test_eq!(obj.get("frac_from_dec").unwrap(), frac!(13, 10));
    test_eq!(obj.get("neg_ffd").unwrap(), frac!(-13, 10));
    test_eq!(obj.get("pos_ffd").unwrap(), frac!(13, 10));

    test_eq!(obj.get("add_dec").unwrap(), frac!(3, 1));
    test_eq!(obj.get("sub_dec").unwrap(), frac!(-3, 1));

    let frac = obj.get_frac("big_frac").unwrap();
    assert!(frac > frac!(91_000_000, 1));
    assert!(frac < frac!(92_000_000, 1));

    test_eq!(obj.get("frac1").unwrap(), frac!(1, 2));
    test_eq!(obj.get("frac2").unwrap(), frac!(1, 2));
    test_eq!(obj.get("frac3").unwrap(), frac!(0, 10));
    test_eq!(obj.get("frac4").unwrap(), frac!(-5, 4));
    test_eq!(obj.get("frac5").unwrap(), frac!(1, 1));

    test_eq!(obj.get("whole_frac").unwrap(), frac!(3, 2));
    test_eq!(obj.get("neg_whole_frac").unwrap(), frac!(-21, 4));
    test_eq!(obj.get("dec_frac").unwrap(), frac!(1, 2));
    test_eq!(obj.get("dec_frac2").unwrap(), frac!(-1, 2));

    test_eq!(
        obj.get("array").unwrap(),
        arr![
            obj.get_frac("whole_frac").unwrap(),
            frac!(-1, 2),
            frac!(3, 2),
            frac!(1, 1),
        ]
    );

    test_eq!(
        obj.get("tup").unwrap(),
        tup!(
            frac!(-1, 2),
            obj.get_frac("whole_frac").unwrap(),
            frac!(1, 1),
            frac!(3, 2),
        )
    );

    test_eq!(obj.get("var_frac").unwrap(), frac!(-1, 2));
}

#[test]
fn operations() {
    let obj = Obj::from_file("tests/test_files/operations.over").unwrap();

    test_eq!(obj.get("mod1").unwrap(), int!(5));
    test_eq!(obj.get("mod2").unwrap(), int!(0));

    test_eq!(obj.get("arr1").unwrap(), arr![3, 4]);
    test_eq!(obj.get("arr2").unwrap(), arr![3, 4]);
    test_eq!(obj.get("arr3").unwrap(), arr![3, 4]);
    test_eq!(obj.get("arr4").unwrap(), arr![arr![1]]);

    test_eq!(
        obj.get("arr_complex").unwrap(),
        arr![arr![arr![1, 2]], arr![arr![3]]]
    );

    test_eq!(obj.get("str1").unwrap(), "cat");
    test_eq!(obj.get("str2").unwrap(), "cat");
    test_eq!(obj.get("str3").unwrap(), "cat");
    test_eq!(obj.get("str4").unwrap(), "cat");

    test_eq!(
        obj.get("tup_complex").unwrap(),
        tup!(arr![arr![], arr![arr![], arr![1, 2, 3], arr![]]])
    );
}

#[test]
fn any_type() {
    let obj = Obj::from_file("tests/test_files/any_type.over").unwrap();

    let arr1 = obj.get("arr1").unwrap();
    let arr2 = arr![arr![arr![]], arr![arr![2]], arr![arr![]]];
    test_eq!(arr1.get_type(), Type::Arr(Box::new(arr2.inner_type())));
    test_eq!(arr1, arr2);

    test_eq!(
        obj.get("arr2").unwrap(),
        arr![
            tup!(arr![arr![]], arr![arr![2]]),
            tup!(arr![arr![2]], arr![arr![]]),
        ]
    );
}

#[test]
fn includes() {
    let obj = Obj::from_file("tests/test_files/includes.over").unwrap();

    // Test both \n and \r\n line endings.
    let s = "Multi-line string\nwhich should be included verbatim\r\n\
             in another file. \"Quotes\" and $$$\ndon't need to be escaped.\n";

    test_eq!(obj.get("include").unwrap(), s);
    test_eq!(obj.get("include2").unwrap(), obj.get("include").unwrap());

    test_eq!(obj.get("include_arr").unwrap(), arr![1, 2, 3, 4, 5]);

    test_eq!(
        obj.get("include_tup").unwrap(),
        tup!("hello", 1, 'c', frac!(3, 3))
    );

    let o = obj.get_obj("include_obj").unwrap();
    test_eq!(
        o,
        obj! {
            "obj2" => obj!{"test" => 1},
            "obj3" => obj!{"test" => 2},
            "dup" => obj!{"test" => 2},
        }
    );

    assert!(o.ptr_eq(&obj.get_obj("include_obj2").unwrap()));
}

// TODO: Test multi-line.over (need substitution)

// Test writing objects to files.
#[test]
fn write() {
    let write_path = "tests/test_files/write.over";

    macro_rules! write_helper {
        ($filename:expr) => {{
            let obj1 = Obj::from_file($filename).unwrap();
            obj1.write_to_file(write_path).unwrap();

            let obj2 = Obj::from_file(write_path).unwrap();
            test_eq!(obj1, obj2);
        }};
    }

    write_helper!("tests/test_files/basic.over");
    write_helper!("tests/test_files/empty.over");
    write_helper!("tests/test_files/includes.over");
    write_helper!("tests/test_files/obj.over");
    write_helper!("tests/test_files/numbers.over");
    write_helper!("tests/test_files/example.over");

    write_helper!("tests/test_files/fuzz1.over");
    write_helper!("tests/test_files/fuzz2.over");
    write_helper!("tests/test_files/fuzz3.over");
}