247 lines
7.0 KiB
Rust
247 lines
7.0 KiB
Rust
use crate::models;
|
|
|
|
pub const DEFAULT_PAGE_SIZE: i32 = 30;
|
|
|
|
#[derive(Clone, Debug, Eq, PartialEq)]
|
|
pub struct RepositoryId {
|
|
pub owner: String,
|
|
pub repository: String,
|
|
}
|
|
|
|
impl RepositoryId {
|
|
pub fn new(owner: impl Into<String>, repository: impl Into<String>) -> crate::Result<Self> {
|
|
let result = Self {
|
|
owner: owner.into(),
|
|
repository: repository.into(),
|
|
};
|
|
if result.owner.is_empty()
|
|
|| result.repository.is_empty()
|
|
|| result.owner.contains('/')
|
|
|| result.repository.contains('/')
|
|
{
|
|
return Err(crate::Error::InvalidInput(
|
|
"repository must be OWNER/REPOSITORY".into(),
|
|
));
|
|
}
|
|
Ok(result)
|
|
}
|
|
|
|
pub fn parse(value: &str) -> crate::Result<Self> {
|
|
let (owner, repository) = value.split_once('/').ok_or_else(|| {
|
|
crate::Error::InvalidInput("repository must be OWNER/REPOSITORY".into())
|
|
})?;
|
|
Self::new(owner, repository)
|
|
}
|
|
}
|
|
|
|
#[derive(Clone, Debug)]
|
|
pub struct Page<T> {
|
|
pub items: Vec<T>,
|
|
pub has_more: bool,
|
|
}
|
|
|
|
impl<T> Page<T> {
|
|
pub fn from_items(items: Vec<T>, limit: i32) -> Self {
|
|
Self {
|
|
has_more: limit > 0 && items.len() == limit as usize,
|
|
items,
|
|
}
|
|
}
|
|
}
|
|
|
|
#[derive(Clone, Debug)]
|
|
pub struct IssueQuery {
|
|
pub state: String,
|
|
pub labels: Option<String>,
|
|
pub keyword: Option<String>,
|
|
pub kind: String,
|
|
pub milestones: Option<String>,
|
|
pub from: Option<String>,
|
|
pub until: Option<String>,
|
|
pub author: Option<String>,
|
|
pub assignee: Option<String>,
|
|
pub mentions: Option<String>,
|
|
pub page: i32,
|
|
pub limit: i32,
|
|
}
|
|
|
|
impl Default for IssueQuery {
|
|
fn default() -> Self {
|
|
Self {
|
|
state: "open".into(),
|
|
labels: None,
|
|
keyword: None,
|
|
kind: "issues".into(),
|
|
milestones: None,
|
|
from: None,
|
|
until: None,
|
|
author: None,
|
|
assignee: None,
|
|
mentions: None,
|
|
page: 1,
|
|
limit: DEFAULT_PAGE_SIZE,
|
|
}
|
|
}
|
|
}
|
|
|
|
#[derive(Clone, Debug)]
|
|
pub struct CreateIssue {
|
|
pub option: models::CreateIssueOption,
|
|
pub label_names: Vec<String>,
|
|
pub milestone_name: Option<String>,
|
|
}
|
|
|
|
#[derive(Clone, Debug)]
|
|
pub struct EditIssue {
|
|
pub option: models::EditIssueOption,
|
|
pub replace_labels: Option<Vec<i64>>,
|
|
pub add_labels: Vec<String>,
|
|
pub remove_labels: Vec<String>,
|
|
pub add_assignees: Vec<String>,
|
|
pub milestone_name: Option<String>,
|
|
}
|
|
|
|
#[derive(Clone, Debug)]
|
|
pub struct IssueDraft {
|
|
pub title: String,
|
|
pub body: String,
|
|
pub label_ids: Vec<i64>,
|
|
pub milestone_id: Option<i64>,
|
|
pub due_date: Option<String>,
|
|
pub closed: bool,
|
|
}
|
|
|
|
#[derive(Clone, Debug)]
|
|
pub struct IssueDetails {
|
|
pub issue: models::Issue,
|
|
pub comments: Vec<models::Comment>,
|
|
pub viewer_id: Option<i64>,
|
|
pub has_more: bool,
|
|
}
|
|
|
|
#[derive(Clone, Debug)]
|
|
pub struct IssueEditorData {
|
|
pub issue: Option<models::Issue>,
|
|
pub labels: Vec<models::Label>,
|
|
pub milestones: Vec<models::Milestone>,
|
|
}
|
|
|
|
#[derive(Clone, Debug)]
|
|
pub struct MilestoneDraft {
|
|
pub title: String,
|
|
pub description: String,
|
|
pub due_on: Option<String>,
|
|
pub state: String,
|
|
}
|
|
|
|
#[derive(Clone, Debug)]
|
|
pub struct MilestoneDetails {
|
|
pub milestone: models::Milestone,
|
|
pub issues: Vec<models::Issue>,
|
|
pub pulls: Vec<models::Issue>,
|
|
pub has_more: bool,
|
|
}
|
|
|
|
#[derive(Clone, Debug)]
|
|
pub struct PullDetails {
|
|
pub pull: models::PullRequest,
|
|
pub comments: Vec<models::Comment>,
|
|
pub files: Vec<models::ChangedFile>,
|
|
pub has_more: bool,
|
|
}
|
|
|
|
#[derive(Clone, Debug)]
|
|
pub struct HistoryCommit {
|
|
pub commit: models::Commit,
|
|
pub top_lanes: Vec<usize>,
|
|
pub bottom_lanes: Vec<usize>,
|
|
pub node_lane: Option<usize>,
|
|
pub top_connections: Vec<usize>,
|
|
pub bottom_connections: Vec<usize>,
|
|
pub refs: Vec<String>,
|
|
pub branch_starts: Vec<String>,
|
|
}
|
|
|
|
#[derive(Clone, Debug)]
|
|
pub struct HomeData {
|
|
pub activities: Vec<models::Activity>,
|
|
pub heatmap: Vec<models::UserHeatmapData>,
|
|
pub next_page: Option<i32>,
|
|
}
|
|
|
|
pub fn api_date(timestamp: i64) -> String {
|
|
let (year, month, day) = civil_from_days(timestamp.div_euclid(86_400));
|
|
format!("{year:04}-{month:02}-{day:02}T00:00:00Z")
|
|
}
|
|
|
|
pub fn api_timestamp(timestamp: i64) -> String {
|
|
let days = timestamp.div_euclid(86_400);
|
|
let seconds = timestamp.rem_euclid(86_400);
|
|
let (year, month, day) = civil_from_days(days);
|
|
let hour = seconds / 3_600;
|
|
let minute = seconds % 3_600 / 60;
|
|
let second = seconds % 60;
|
|
format!("{year:04}-{month:02}-{day:02}T{hour:02}:{minute:02}:{second:02}Z")
|
|
}
|
|
|
|
pub fn parse_api_date(value: &str) -> Option<i64> {
|
|
let date = value.get(..10)?;
|
|
let mut parts = date.split('-');
|
|
let year = parts.next()?.parse().ok()?;
|
|
let month = parts.next()?.parse().ok()?;
|
|
let day = parts.next()?.parse().ok()?;
|
|
if parts.next().is_some() || !(1..=12).contains(&month) || !(1..=31).contains(&day) {
|
|
return None;
|
|
}
|
|
let days = days_from_civil(year, month, day);
|
|
(civil_from_days(days) == (year, month, day)).then_some(days * 86_400)
|
|
}
|
|
|
|
pub fn civil_from_days(days: i64) -> (i64, i64, i64) {
|
|
let days = days + 719_468;
|
|
let era = days.div_euclid(146_097);
|
|
let day_of_era = days - era * 146_097;
|
|
let year_of_era =
|
|
(day_of_era - day_of_era / 1_460 + day_of_era / 36_524 - day_of_era / 146_096) / 365;
|
|
let mut year = year_of_era + era * 400;
|
|
let day_of_year = day_of_era - (365 * year_of_era + year_of_era / 4 - year_of_era / 100);
|
|
let month_prime = (5 * day_of_year + 2) / 153;
|
|
let day = day_of_year - (153 * month_prime + 2) / 5 + 1;
|
|
let month = month_prime + if month_prime < 10 { 3 } else { -9 };
|
|
year += i64::from(month <= 2);
|
|
(year, month, day)
|
|
}
|
|
|
|
pub fn days_from_civil(mut year: i64, month: i64, day: i64) -> i64 {
|
|
year -= i64::from(month <= 2);
|
|
let era = year.div_euclid(400);
|
|
let year_of_era = year - era * 400;
|
|
let month_prime = month + if month > 2 { -3 } else { 9 };
|
|
let day_of_year = (153 * month_prime + 2) / 5 + day - 1;
|
|
let day_of_era = year_of_era * 365 + year_of_era / 4 - year_of_era / 100 + day_of_year;
|
|
era * 146_097 + day_of_era - 719_468
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
#[test]
|
|
fn validates_repositories_pages_and_dates() {
|
|
assert_eq!(
|
|
RepositoryId::parse("alice/project").unwrap(),
|
|
RepositoryId {
|
|
owner: "alice".into(),
|
|
repository: "project".into(),
|
|
}
|
|
);
|
|
assert!(RepositoryId::parse("project").is_err());
|
|
assert!(Page::from_items(vec![(); 30], 30).has_more);
|
|
assert!(!Page::from_items(vec![(); 29], 30).has_more);
|
|
let leap_day = parse_api_date("2024-02-29T12:34:56Z").unwrap();
|
|
assert_eq!(api_date(leap_day), "2024-02-29T00:00:00Z");
|
|
assert_eq!(api_timestamp(leap_day + 45_296), "2024-02-29T12:34:56Z");
|
|
assert_eq!(parse_api_date("2023-02-29T00:00:00Z"), None);
|
|
}
|
|
}
|