rocfl 1.0.3

A Rust CLI for OCFL repositories
Documentation
use std::convert::TryFrom;
use std::path::PathBuf;
use std::rc::Rc;

use anyhow::Result;
use chrono::DateTime;
use maplit::hashmap;

use rocfl::{Diff, DiffType, FileDetails, ObjectVersion, ObjectVersionDetails, OcflRepo, VersionDetails, VersionNum};

#[test]
fn list_all_objects() -> Result<()> {
    let repo_root = create_repo_root("multiple-objects");
    let repo = OcflRepo::new_fs_repo(&repo_root)?;

    let mut objects: Vec<ObjectVersionDetails> = repo.list_objects(None)?.map(|o| o.unwrap()).collect();

    sort_obj_details(&mut objects);

    assert_eq!(3, objects.len());

    assert_eq!(objects.remove(0), ObjectVersionDetails {
        id: "o1".to_string(),
        object_root: repo_root.join("235").join("2da").join("728").join("2352da7280f1decc3acf1ba84eb945c9fc2b7b541094e1d0992dbffd1b6664cc")
            .to_string_lossy().to_string(),
        digest_algorithm: "sha512".to_string(),
        version_details: VersionDetails {
            version_num: VersionNum::try_from(1).unwrap(),
            created: DateTime::parse_from_rfc3339("2019-08-05T15:57:53Z").unwrap().into(),
            user_name: Some("Peter".to_string()),
            user_address: Some("peter@example.com".to_string()),
            message: Some("commit message".to_string())
        }
    });

    assert_eq!(objects.remove(0), ObjectVersionDetails {
        id: "o2".to_string(),
        object_root: repo_root.join("925").join("0b9").join("912").join("9250b9912ee91d6b46e23299459ecd6eb8154451d62558a3a0a708a77926ad04")
            .to_string_lossy().to_string(),
        digest_algorithm: "sha512".to_string(),
        version_details: o2_v3_details()
    });

    assert_eq!(objects.remove(0), ObjectVersionDetails {
        id: "o3".to_string(),
        object_root: repo_root.join("de2").join("d91").join("dc0").join("de2d91dc0a2580414e9a70f7dfc76af727b69cac0838f2cbe0a88d12642efcbf")
            .to_string_lossy().to_string(),
        digest_algorithm: "sha512".to_string(),
        version_details: VersionDetails {
            version_num: VersionNum::try_from(2).unwrap(),
            created: DateTime::parse_from_rfc3339("2019-08-05T15:57:53Z").unwrap().into(),
            user_name: Some("Peter".to_string()),
            user_address: Some("peter@example.com".to_string()),
            message: Some("2".to_string())
        }
    });

    Ok(())
}

#[test]
fn list_single_object_from_glob() -> Result<()> {
    let repo_root = create_repo_root("multiple-objects");
    let repo = OcflRepo::new_fs_repo(&repo_root)?;

    let mut objects: Vec<ObjectVersionDetails> = repo.list_objects(Some("*1"))?.map(|o| o.unwrap()).collect();

    assert_eq!(1, objects.len());

    assert_eq!(objects.remove(0), ObjectVersionDetails {
        id: "o1".to_string(),
        object_root: repo_root.join("235").join("2da").join("728").join("2352da7280f1decc3acf1ba84eb945c9fc2b7b541094e1d0992dbffd1b6664cc")
            .to_string_lossy().to_string(),
        digest_algorithm: "sha512".to_string(),
        version_details: VersionDetails {
            version_num: VersionNum::try_from(1).unwrap(),
            created: DateTime::parse_from_rfc3339("2019-08-05T15:57:53Z").unwrap().into(),
            user_name: Some("Peter".to_string()),
            user_address: Some("peter@example.com".to_string()),
            message: Some("commit message".to_string())
        }
    });

    Ok(())
}

#[test]
fn list_empty_repo() -> Result<()> {
    let repo_root = create_repo_root("empty");
    let repo = OcflRepo::new_fs_repo(&repo_root)?;

    let objects: Vec<ObjectVersionDetails> = repo.list_objects(None)?.map(|o| o.unwrap()).collect();

    assert_eq!(0, objects.len());

    Ok(())
}

#[test]
fn list_repo_with_invalid_objects() -> Result<()> {
    let repo_root = create_repo_root("invalid");
    let repo = OcflRepo::new_fs_repo(&repo_root)?;

    let object_root = repo_root.join("925").join("0b9").join("912")
        .join("9250b9912ee91d6b46e23299459ecd6eb8154451d62558a3a0a708a77926ad04");

    let iter = repo.list_objects(None)?;

    for result in iter {
        match result {
            Ok(object) => {
                assert_eq!(object, ObjectVersionDetails {
                    id: "o2".to_string(),
                    object_root: object_root.to_string_lossy().to_string(),
                    digest_algorithm: "sha512".to_string(),
                    version_details: o2_v3_details()
                });
            }
            Err(e) => {
                let message = format!("{}", e);
                assert!(message.contains("Failed to parse inventory"));
            }
        }
    }

    Ok(())
}

#[test]
fn get_object_when_exists() -> Result<()> {
    let repo_root = create_repo_root("multiple-objects");
    let repo = OcflRepo::new_fs_repo(&repo_root)?;

    let object = repo.get_object("o2", None)?;

    let object_root = repo_root.join("925").join("0b9").join("912")
        .join("9250b9912ee91d6b46e23299459ecd6eb8154451d62558a3a0a708a77926ad04");

    assert_eq!(object, ObjectVersion {
        id: "o2".to_string(),
        object_root: object_root.to_string_lossy().to_string(),
        digest_algorithm: "sha512".to_string(),
        version_details: o2_v3_details(),
        state: hashmap!{
            "dir1/file3".to_string() => FileDetails {
                digest: Rc::new("6e027f3dc89e0bfd97e4c2ec6919a8fb793bdc7b5c513bea618f174beec32a66d2fc0ce19439751e2f01ae49f78c56dcfc7b49c167a751c823d09da8419a4331".to_string()),
                digest_algorithm: Rc::new("sha512".to_string()),
                content_path: "v3/content/dir1/file3".to_string(),
                storage_path: object_root.join("v3").join("content").join("dir1").join("file3")
                    .to_string_lossy().to_string(),
                last_update: Rc::new(o2_v3_details())
            },
            "dir1/dir2/file2".to_string() => FileDetails {
                digest: Rc::new("4cf0ff5673ec65d9900df95502ed92b2605fc602ca20b6901652c7561b302668026095813af6adb0e663bdcdbe1f276d18bf0de254992a78573ad6574e7ae1f6".to_string()),
                digest_algorithm: Rc::new("sha512".to_string()),
                content_path: "v1/content/dir1/dir2/file2".to_string(),
                storage_path: object_root.join("v1").join("content").join("dir1").join("dir2").join("file2")
                    .to_string_lossy().to_string(),
                last_update: Rc::new(o2_v1_details())
            }
        }
    });

    Ok(())
}

#[test]
fn get_object_version_when_exists() -> Result<()> {
    let repo_root = create_repo_root("multiple-objects");
    let repo = OcflRepo::new_fs_repo(&repo_root)?;

    let object = repo.get_object("o2", Some(&VersionNum::try_from(2)?))?;

    let object_root = repo_root.join("925").join("0b9").join("912")
        .join("9250b9912ee91d6b46e23299459ecd6eb8154451d62558a3a0a708a77926ad04");

    assert_eq!(object, ObjectVersion {
        id: "o2".to_string(),
        object_root: object_root.to_string_lossy().to_string(),
        digest_algorithm: "sha512".to_string(),
        version_details: o2_v2_details(),
        state: hashmap!{
            "dir1/file3".to_string() => FileDetails {
                digest: Rc::new("7b866cfcfe06bf2bcaea7086f2a059854afe8de12a6e21e4286bec4828d3da36bdef28599be8c9be49da3e45ede3ddbc049f99ee197e5244c33e294748b1a986".to_string()),
                digest_algorithm: Rc::new("sha512".to_string()),
                content_path: "v2/content/dir1/file3".to_string(),
                storage_path: object_root.join("v2").join("content").join("dir1").join("file3")
                    .to_string_lossy().to_string(),
                last_update: Rc::new(o2_v2_details())
            },
            "dir1/dir2/file2".to_string() => FileDetails {
                digest: Rc::new("4cf0ff5673ec65d9900df95502ed92b2605fc602ca20b6901652c7561b302668026095813af6adb0e663bdcdbe1f276d18bf0de254992a78573ad6574e7ae1f6".to_string()),
                digest_algorithm: Rc::new("sha512".to_string()),
                content_path: "v1/content/dir1/dir2/file2".to_string(),
                storage_path: object_root.join("v1").join("content").join("dir1").join("dir2").join("file2")
                    .to_string_lossy().to_string(),
                last_update: Rc::new(o2_v1_details())
            },
            "dir3/file1".to_string() => FileDetails {
                digest: Rc::new("96a26e7629b55187f9ba3edc4acc940495d582093b8a88cb1f0303cf3399fe6b1f5283d76dfd561fc401a0cdf878c5aad9f2d6e7e2d9ceee678757bb5d95c39e".to_string()),
                digest_algorithm: Rc::new("sha512".to_string()),
                content_path: "v1/content/file1".to_string(),
                storage_path: object_root.join("v1").join("content").join("file1")
                    .to_string_lossy().to_string(),
                last_update: Rc::new(o2_v2_details())
            }
        }
    });

    Ok(())
}

#[test]
#[should_panic(expected = "Not found: Object o4")]
fn error_when_object_not_exists() {
    let repo_root = create_repo_root("multiple-objects");
    let repo = OcflRepo::new_fs_repo(&repo_root).unwrap();
    repo.get_object("o4", None).unwrap();
}

#[test]
#[should_panic(expected = "Not found: Object o2 version v4")]
fn error_when_version_not_exists() {
    let repo_root = create_repo_root("multiple-objects");
    let repo = OcflRepo::new_fs_repo(&repo_root).unwrap();
    repo.get_object("o2", Some(&VersionNum::try_from(4).unwrap())).unwrap();
}

#[test]
#[should_panic(expected = "Not found: Object o3")]
fn error_when_get_invalid_object() {
    let repo_root = create_repo_root("invalid");
    let repo = OcflRepo::new_fs_repo(&repo_root).unwrap();
    repo.get_object("o3", None).unwrap();
}

#[test]
fn list_versions_when_multiple() -> Result<()> {
    let repo_root = create_repo_root("multiple-objects");
    let repo = OcflRepo::new_fs_repo(&repo_root)?;

    let mut versions = repo.list_object_versions("o2")?;

    assert_eq!(3, versions.len());

    assert_eq!(versions.remove(0), o2_v1_details());
    assert_eq!(versions.remove(0), o2_v2_details());
    assert_eq!(versions.remove(0), o2_v3_details());

    Ok(())
}

#[test]
fn list_file_versions_when_multiple() -> Result<()> {
    let repo_root = create_repo_root("multiple-objects");
    let repo = OcflRepo::new_fs_repo(&repo_root)?;

    let mut versions = repo.list_file_versions("o2", "dir3/file1")?;

    assert_eq!(2, versions.len());

    assert_eq!(versions.remove(0), o2_v2_details());
    assert_eq!(versions.remove(0), o2_v3_details());

    Ok(())
}

#[test]
#[should_panic(expected = "Not found: Object o5")]
fn list_versions_not_exists() {
    let repo_root = create_repo_root("multiple-objects");
    let repo = OcflRepo::new_fs_repo(&repo_root).unwrap();
    repo.list_object_versions("o5").unwrap();
}

#[test]
#[should_panic(expected = "Not found: Path bogus.txt not found in object o2")]
fn list_file_versions_not_exists() {
    let repo_root = create_repo_root("multiple-objects");
    let repo = OcflRepo::new_fs_repo(&repo_root).unwrap();
    repo.list_file_versions("o2", "bogus.txt").unwrap();
}

#[test]
fn diff_when_left_and_right_specified() -> Result<()> {
    let repo_root = create_repo_root("multiple-objects");
    let repo = OcflRepo::new_fs_repo(&repo_root)?;

    let mut diff = repo.diff("o2", Some(&VersionNum::try_from(1).unwrap()), &VersionNum::try_from(3).unwrap())?;

    sort_diffs(&mut diff);

    assert_eq!(2, diff.len());

    assert_eq!(diff.remove(0), Diff {
        diff_type: DiffType::Added,
        path: "dir1/file3".to_string()
    });
    assert_eq!(diff.remove(0), Diff {
        diff_type: DiffType::Deleted,
        path: "file1".to_string()
    });

    Ok(())
}

#[test]
fn diff_with_previous_when_left_not_specified() -> Result<()> {
    let repo_root = create_repo_root("multiple-objects");
    let repo = OcflRepo::new_fs_repo(&repo_root)?;

    let mut diff = repo.diff("o2", None, &VersionNum::try_from(3).unwrap())?;

    sort_diffs(&mut diff);

    assert_eq!(2, diff.len());

    assert_eq!(diff.remove(0), Diff {
        diff_type: DiffType::Modified,
        path: "dir1/file3".to_string()
    });
    assert_eq!(diff.remove(0), Diff {
        diff_type: DiffType::Deleted,
        path: "dir3/file1".to_string()
    });

    Ok(())
}

#[test]
fn diff_first_version_all_adds() -> Result<()> {
    let repo_root = create_repo_root("multiple-objects");
    let repo = OcflRepo::new_fs_repo(&repo_root)?;

    let mut diff = repo.diff("o2", None, &VersionNum::try_from(1).unwrap())?;

    sort_diffs(&mut diff);

    assert_eq!(2, diff.len());

    assert_eq!(diff.remove(0), Diff {
        diff_type: DiffType::Added,
        path: "dir1/dir2/file2".to_string()
    });
    assert_eq!(diff.remove(0), Diff {
        diff_type: DiffType::Added,
        path: "file1".to_string()
    });

    Ok(())
}

#[test]
fn diff_same_version_no_diff() -> Result<()> {
    let repo_root = create_repo_root("multiple-objects");
    let repo = OcflRepo::new_fs_repo(&repo_root)?;

    let diff = repo.diff("o2", Some(&VersionNum::try_from(2).unwrap()), &VersionNum::try_from(2).unwrap())?;

    assert_eq!(0, diff.len());

    Ok(())
}

#[test]
#[should_panic(expected = "Not found: Object o6")]
fn diff_object_not_exists() {
    let repo_root = create_repo_root("multiple-objects");
    let repo = OcflRepo::new_fs_repo(&repo_root).unwrap();
    repo.diff("o6", None, &VersionNum::try_from(2).unwrap()).unwrap();
}

#[test]
#[should_panic(expected = "Not found: Object o1 version v2")]
fn diff_version_not_exists() {
    let repo_root = create_repo_root("multiple-objects");
    let repo = OcflRepo::new_fs_repo(&repo_root).unwrap();
    repo.diff("o1", None, &VersionNum::try_from(2).unwrap()).unwrap();
}

fn o2_v1_details() -> VersionDetails {
    VersionDetails {
        version_num: VersionNum::try_from(1).unwrap(),
        created: DateTime::parse_from_rfc3339("2019-08-05T15:57:53Z").unwrap().into(),
        user_name: Some("Peter".to_string()),
        user_address: Some("peter@example.com".to_string()),
        message: Some("commit message".to_string())
    }
}

fn o2_v2_details() -> VersionDetails {
    VersionDetails {
        version_num: VersionNum::try_from(2).unwrap(),
        created: DateTime::parse_from_rfc3339("2019-08-05T16:59:56Z").unwrap().into(),
        user_name: Some("Peter".to_string()),
        user_address: Some("peter@example.com".to_string()),
        message: Some("2".to_string())
    }
}

fn o2_v3_details() -> VersionDetails {
    VersionDetails {
        version_num: VersionNum::try_from(3).unwrap(),
        created: DateTime::parse_from_rfc3339("2019-08-07T12:37:43Z").unwrap().into(),
        user_name: Some("Peter".to_string()),
        user_address: Some("peter@example.com".to_string()),
        message: Some("3".to_string())
    }
}

fn create_repo_root(name: &str) -> PathBuf {
    let mut path = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
    path.push("resources");
    path.push("test");
    path.push("repos");
    path.push(name);
    path
}

fn sort_obj_details(objects: &mut Vec<ObjectVersionDetails>) {
    objects.sort_unstable_by(|a, b| {
        a.id.cmp(&b.id)
    });
}

fn sort_diffs(diffs: &mut Vec<Diff>) {
    diffs.sort_unstable_by(|a, b| {
        a.path.cmp(&b.path)
    })
}