omnitrack 0.3.0

Universal issue-tracker provider contracts and clients (Linear, Jira, ...) for Rust, in one crate.
Documentation
use omnitrack::{
    IssueId, LabelId, MilestoneId, ProjectId, StatusCategory, TeamId, UserId, comment, issue,
    issue_filter,
};

#[test]
fn issue_builder_constructs_with_required_and_optional_fields() {
    let built = issue()
        .id("ISS-1")
        .title("Wire up sync")
        .status("open")
        .category(StatusCategory::Started)
        .project("PRJ-1")
        .milestone("MIL-1")
        .assignee("USR-1")
        .priority(2)
        .updated_at("2026-05-25T00:00:00Z")
        .build();

    assert_eq!(built.id().as_str(), "ISS-1");
    assert_eq!(built.title(), "Wire up sync");
    assert_eq!(built.status(), "open");
    assert_eq!(built.category(), Some(StatusCategory::Started));
    assert_eq!(built.project().map(ProjectId::as_str), Some("PRJ-1"));
    assert_eq!(built.milestone().map(MilestoneId::as_str), Some("MIL-1"));
    assert_eq!(built.assignee().map(UserId::as_str), Some("USR-1"));
    assert_eq!(built.priority(), Some(2));
    assert_eq!(built.updated_at(), "2026-05-25T00:00:00Z");
}

#[test]
fn issue_builder_defaults_optionals_to_none() {
    let built = issue().id("ISS-2").title("Minimal").status("todo").build();

    assert_eq!(built.category(), None);
    assert!(built.project().is_none());
    assert!(built.milestone().is_none());
    assert!(built.assignee().is_none());
    assert_eq!(built.priority(), None);
    assert_eq!(built.updated_at(), "");
}

#[test]
fn builder_accepts_raw_strings_or_newtypes() {
    let from_strings = issue()
        .id("ISS-1")
        .title("t")
        .status("s")
        .project("PRJ-1")
        .build();
    let from_newtypes = issue()
        .id(IssueId::make("ISS-1"))
        .title("t")
        .status("s")
        .project(ProjectId::make("PRJ-1"))
        .build();

    assert_eq!(from_strings.id().as_str(), from_newtypes.id().as_str());
    assert_eq!(
        from_strings.project().map(ProjectId::as_str),
        from_newtypes.project().map(ProjectId::as_str)
    );
}

#[test]
fn issue_builder_captures_enriched_fields() {
    let built = issue()
        .id("ISS-9")
        .title("Rich")
        .status("In Progress")
        .identifier("ENG-9")
        .description("body text")
        .url("https://linear.app/x/issue/ENG-9")
        .created_at("2026-05-25T00:00:00Z")
        .updated_at("2026-05-25T01:00:00Z")
        .author("USR-2")
        .team("TEAM-1")
        .labels(["L-1", "L-2"])
        .label("L-3")
        .build();

    assert_eq!(built.identifier(), Some("ENG-9"));
    assert_eq!(built.description(), Some("body text"));
    assert_eq!(built.url(), Some("https://linear.app/x/issue/ENG-9"));
    assert_eq!(built.created_at(), Some("2026-05-25T00:00:00Z"));
    assert_eq!(built.updated_at(), "2026-05-25T01:00:00Z");
    assert_eq!(built.author().map(UserId::as_str), Some("USR-2"));
    assert_eq!(built.team().map(TeamId::as_str), Some("TEAM-1"));
    assert_eq!(
        built
            .labels()
            .iter()
            .map(LabelId::as_str)
            .collect::<Vec<_>>(),
        vec!["L-1", "L-2", "L-3"]
    );
}

#[test]
fn comment_builder_constructs() {
    let c = comment()
        .id("CMT-1")
        .body("looks good")
        .author("USR-1")
        .created_at("2026-05-25T00:00:00Z")
        .build();

    assert_eq!(c.id().as_str(), "CMT-1");
    assert_eq!(c.body(), "looks good");
    assert_eq!(c.author().map(UserId::as_str), Some("USR-1"));
    assert_eq!(c.created_at(), Some("2026-05-25T00:00:00Z"));
}

#[test]
fn issue_filter_builder() {
    let filter = issue_filter()
        .team("TEAM-1")
        .project("PRJ-1")
        .category(StatusCategory::Started)
        .build();

    assert!(!filter.is_empty());
    assert_eq!(filter.team().map(TeamId::as_str), Some("TEAM-1"));
    assert_eq!(filter.project().map(ProjectId::as_str), Some("PRJ-1"));
    assert_eq!(filter.category(), Some(StatusCategory::Started));
    assert!(issue_filter().build().is_empty());
}