prlens 0.1.1

One queue for all your PRs — aggregates GitHub and Bitbucket review requests into a single interactive view
Documentation
use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};

#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash)]
pub struct PrIdentifier {
    pub provider: String,
    pub owner: String,
    pub repo: String,
    pub number: u64,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct User {
    pub login: String,
    pub display_name: Option<String>,
    pub avatar_url: Option<String>,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Reviewer {
    pub user: User,
    pub state: ReviewerState,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum ReviewerState {
    Pending,
    Approved,
    ChangesRequested,
    Dismissed,
    Commented,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum PrState {
    Open,
    Closed,
    Merged,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum ReviewStatus {
    NeedsReview,
    Approved,
    ChangesRequested,
    Mixed,
    InReview,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum CiStatus {
    Pending,
    Running,
    Success,
    Failed,
    Cancelled,
}

/// The normalized PR record. All provider implementations map to this.
/// Provider-specific types MUST NOT appear in the display or CLI layer.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PullRequest {
    // Identity
    pub id: PrIdentifier,
    pub number: u64,
    pub title: String,
    pub url: String,

    // People
    pub author: User,
    pub reviewers: Vec<Reviewer>,

    // Repository context
    pub repo_full_name: String,   // "owner/repo"
    pub provider: String,         // "github" | "bitbucket" | "mock"

    // Branches
    pub head_branch: String,
    pub base_branch: String,

    // Status
    pub state: PrState,
    pub review_status: ReviewStatus,
    pub ci_status: Option<CiStatus>, // Not available from all list endpoints
    pub draft: bool,

    // Timing (UTC)
    pub created_at: DateTime<Utc>,
    pub updated_at: DateTime<Utc>,

    // Metadata
    pub labels: Vec<String>,
    pub comment_count: u32,
    pub additions: Option<u32>,   // May require separate API call
    pub deletions: Option<u32>,
}

#[cfg(test)]
mod tests {
    use super::*;
    use chrono::Utc;

    #[test]
    fn models_round_trip() {
        let pr = PullRequest {
            id: PrIdentifier {
                provider: "mock".to_string(),
                owner: "acme".to_string(),
                repo: "widget".to_string(),
                number: 42,
            },
            number: 42,
            title: "feat: round trip test".to_string(),
            url: "https://github.com/acme/widget/pull/42".to_string(),
            author: User {
                login: "alice".to_string(),
                display_name: None,
                avatar_url: None,
            },
            reviewers: vec![],
            repo_full_name: "acme/widget".to_string(),
            provider: "mock".to_string(),
            head_branch: "feat/test".to_string(),
            base_branch: "main".to_string(),
            state: PrState::Open,
            review_status: ReviewStatus::NeedsReview,
            ci_status: None,
            draft: false,
            created_at: Utc::now(),
            updated_at: Utc::now(),
            labels: vec![],
            comment_count: 0,
            additions: None,
            deletions: None,
        };

        let json = serde_json::to_string(&pr).expect("serialization failed");
        let deserialized: PullRequest = serde_json::from_str(&json).expect("deserialization failed");
        assert_eq!(deserialized.number, 42);
    }
}