Centralize Gitea domain workflows
This commit is contained in:
238
crates/gitea/src/milestones.rs
Normal file
238
crates/gitea/src/milestones.rs
Normal file
@@ -0,0 +1,238 @@
|
||||
use serde::Serialize;
|
||||
|
||||
use crate::{
|
||||
Client, Error, Method, Result,
|
||||
domain::{DEFAULT_PAGE_SIZE, IssueQuery, MilestoneDetails, MilestoneDraft, Page, RepositoryId},
|
||||
models,
|
||||
};
|
||||
use gitea_openapi::apis;
|
||||
|
||||
impl Client {
|
||||
pub async fn milestones_page(
|
||||
&self,
|
||||
repository: &RepositoryId,
|
||||
page: i32,
|
||||
limit: i32,
|
||||
) -> Result<Page<models::Milestone>> {
|
||||
if page < 1 || limit < 1 {
|
||||
return Err(Error::InvalidInput(
|
||||
"milestone page and limit must be positive".into(),
|
||||
));
|
||||
}
|
||||
apis::issue_api::issue_get_milestones_list(
|
||||
&self.configuration(),
|
||||
&repository.owner,
|
||||
&repository.repository,
|
||||
Some("all"),
|
||||
None,
|
||||
Some(page),
|
||||
Some(limit),
|
||||
)
|
||||
.await
|
||||
.map(|items| Page::from_items(items, limit))
|
||||
.map_err(Error::generated)
|
||||
}
|
||||
|
||||
pub async fn milestones(&self, repository: &RepositoryId) -> Result<Vec<models::Milestone>> {
|
||||
let mut milestones = Vec::new();
|
||||
for page in 1.. {
|
||||
let batch = self.milestones_page(repository, page, 100).await?;
|
||||
milestones.extend(batch.items);
|
||||
if !batch.has_more {
|
||||
break;
|
||||
}
|
||||
}
|
||||
Ok(milestones)
|
||||
}
|
||||
|
||||
pub async fn resolve_milestone_id(&self, repository: &RepositoryId, name: &str) -> Result<i64> {
|
||||
self.milestones(repository)
|
||||
.await?
|
||||
.into_iter()
|
||||
.find(|milestone| milestone.title.as_deref() == Some(name))
|
||||
.and_then(|milestone| milestone.id)
|
||||
.ok_or_else(|| Error::InvalidInput(format!("unknown milestone {name:?}")))
|
||||
}
|
||||
|
||||
pub async fn milestone(&self, repository: &RepositoryId, id: i64) -> Result<models::Milestone> {
|
||||
if id < 1 {
|
||||
return Err(Error::InvalidInput(
|
||||
"milestone id must be a positive integer".into(),
|
||||
));
|
||||
}
|
||||
apis::issue_api::issue_get_milestone(
|
||||
&self.configuration(),
|
||||
&repository.owner,
|
||||
&repository.repository,
|
||||
&id.to_string(),
|
||||
)
|
||||
.await
|
||||
.map_err(Error::generated)
|
||||
}
|
||||
|
||||
pub async fn milestone_details(
|
||||
&self,
|
||||
repository: &RepositoryId,
|
||||
id: i64,
|
||||
page: i32,
|
||||
) -> Result<MilestoneDetails> {
|
||||
let milestone = self.milestone(repository, id).await?;
|
||||
let base = IssueQuery {
|
||||
state: "all".into(),
|
||||
milestones: milestone.title.clone(),
|
||||
page,
|
||||
limit: DEFAULT_PAGE_SIZE,
|
||||
..Default::default()
|
||||
};
|
||||
let pulls_query = IssueQuery {
|
||||
kind: "pulls".into(),
|
||||
..base.clone()
|
||||
};
|
||||
let issues = self.issues(repository, &base);
|
||||
let pulls = self.issues(repository, &pulls_query);
|
||||
let (issues, pulls) = tokio::try_join!(issues, pulls)?;
|
||||
let has_more = issues.has_more || pulls.has_more;
|
||||
let mut pulls = pulls.items;
|
||||
for pull in &mut pulls {
|
||||
pull.repository.get_or_insert_with(|| {
|
||||
Box::new(models::RepositoryMeta {
|
||||
name: Some(repository.repository.clone()),
|
||||
owner: Some(repository.owner.clone()),
|
||||
..Default::default()
|
||||
})
|
||||
});
|
||||
}
|
||||
Ok(MilestoneDetails {
|
||||
milestone,
|
||||
has_more,
|
||||
issues: issues.items,
|
||||
pulls,
|
||||
})
|
||||
}
|
||||
|
||||
pub async fn create_milestone(
|
||||
&self,
|
||||
repository: &RepositoryId,
|
||||
option: models::CreateMilestoneOption,
|
||||
) -> Result<models::Milestone> {
|
||||
if option
|
||||
.title
|
||||
.as_deref()
|
||||
.unwrap_or_default()
|
||||
.trim()
|
||||
.is_empty()
|
||||
{
|
||||
return Err(Error::InvalidInput(
|
||||
"milestone title must not be empty".into(),
|
||||
));
|
||||
}
|
||||
apis::issue_api::issue_create_milestone(
|
||||
&self.configuration(),
|
||||
&repository.owner,
|
||||
&repository.repository,
|
||||
Some(option),
|
||||
)
|
||||
.await
|
||||
.map_err(Error::generated)
|
||||
}
|
||||
|
||||
pub async fn edit_milestone(
|
||||
&self,
|
||||
repository: &RepositoryId,
|
||||
id: &str,
|
||||
option: models::EditMilestoneOption,
|
||||
) -> Result<models::Milestone> {
|
||||
apis::issue_api::issue_edit_milestone(
|
||||
&self.configuration(),
|
||||
&repository.owner,
|
||||
&repository.repository,
|
||||
id,
|
||||
Some(option),
|
||||
)
|
||||
.await
|
||||
.map_err(Error::generated)
|
||||
}
|
||||
|
||||
pub async fn save_milestone(
|
||||
&self,
|
||||
repository: &RepositoryId,
|
||||
id: Option<i64>,
|
||||
draft: MilestoneDraft,
|
||||
) -> Result<models::Milestone> {
|
||||
if draft.title.trim().is_empty() {
|
||||
return Err(Error::InvalidInput(
|
||||
"milestone title must not be empty".into(),
|
||||
));
|
||||
}
|
||||
let endpoint = match id {
|
||||
Some(id) if id > 0 => format!(
|
||||
"repos/{}/{}/milestones/{id}",
|
||||
apis::urlencode(&repository.owner),
|
||||
apis::urlencode(&repository.repository)
|
||||
),
|
||||
Some(_) => {
|
||||
return Err(Error::InvalidInput(
|
||||
"milestone id must be a positive integer".into(),
|
||||
));
|
||||
}
|
||||
None => format!(
|
||||
"repos/{}/{}/milestones",
|
||||
apis::urlencode(&repository.owner),
|
||||
apis::urlencode(&repository.repository)
|
||||
),
|
||||
};
|
||||
let request = self
|
||||
.request(
|
||||
if id.is_some() {
|
||||
Method::PATCH
|
||||
} else {
|
||||
Method::POST
|
||||
},
|
||||
&endpoint,
|
||||
)?
|
||||
.json(&MilestoneRequest {
|
||||
title: draft.title,
|
||||
description: draft.description,
|
||||
due_on: draft.due_on,
|
||||
});
|
||||
self.execute(request)
|
||||
.await?
|
||||
.json()
|
||||
.await
|
||||
.map_err(Into::into)
|
||||
}
|
||||
|
||||
pub async fn delete_milestone(&self, repository: &RepositoryId, id: &str) -> Result<()> {
|
||||
apis::issue_api::issue_delete_milestone(
|
||||
&self.configuration(),
|
||||
&repository.owner,
|
||||
&repository.repository,
|
||||
id,
|
||||
)
|
||||
.await
|
||||
.map_err(Error::generated)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
struct MilestoneRequest {
|
||||
title: String,
|
||||
description: String,
|
||||
due_on: Option<String>,
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn milestone_request_preserves_due_date_clear() {
|
||||
let value = serde_json::to_value(MilestoneRequest {
|
||||
title: "Release".into(),
|
||||
description: String::new(),
|
||||
due_on: None,
|
||||
})
|
||||
.unwrap();
|
||||
assert!(value.get("due_on").unwrap().is_null());
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user