From 9aec267e0a7ef24787a354106476b7b41f1f9b8a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julien=20Fastr=C3=A9?= Date: Mon, 20 May 2024 19:16:21 +0200 Subject: [PATCH 1/3] WIP on integrate gitea --- src/config.rs | 7 ++++++ src/debug.rs | 10 ++++++++ src/error.rs | 19 +++++++++++++++ src/gitea/client.rs | 54 +++++++++++++++++++++++++++++++++++++++++ src/gitea/issue.rs | 22 +++++++++++++++++ src/gitea/mod.rs | 3 +++ src/gitea/repository.rs | 9 +++++++ src/main.rs | 1 + 8 files changed, 125 insertions(+) create mode 100644 src/gitea/client.rs create mode 100644 src/gitea/issue.rs create mode 100644 src/gitea/mod.rs create mode 100644 src/gitea/repository.rs diff --git a/src/config.rs b/src/config.rs index 468a551..9cd9fdd 100644 --- a/src/config.rs +++ b/src/config.rs @@ -3,6 +3,7 @@ use serde::Deserialize; #[derive(Deserialize, Debug)] pub(crate) struct Config { pub gitlab: Vec, + pub gitea: Vec, pub openproject: OpenProjectConfig, } @@ -12,6 +13,12 @@ pub(crate) struct GitlabConfig { pub domain: String, } +#[derive(Deserialize, Debug)] +pub(crate) struct GiteaConfig { + pub token: String, + pub domain: String, +} + #[derive(Deserialize, Debug)] pub(crate) struct OpenProjectConfig { pub token: String, diff --git a/src/debug.rs b/src/debug.rs index 68362b1..6a9f8b2 100644 --- a/src/debug.rs +++ b/src/debug.rs @@ -1,5 +1,7 @@ +use url::Url; use crate::config::Config; use crate::error::GeneralError; +use crate::gitea::issue::Issue; use crate::openproject::client::Client; use crate::openproject::root::RootClient; use crate::openproject::user::GetMe; @@ -7,6 +9,14 @@ use crate::openproject::user::GetMe; pub(crate) async fn debug(config: Config) -> Result<(), GeneralError> { println!("test"); + + let gitea_client = crate::gitea::client::Client::from_config(config.gitea.first().unwrap()); + let issue: Issue = gitea_client.get(Url::parse("https://gitea.champs-libres.be/api/v1/repos/julienfastre/test/issues/6").unwrap()).await?; + + println!("issue: {:?}", issue); + + return Ok(()); + let open_project_client = Client::from_config(&config.openproject); println!("base_url: {}", open_project_client.base_url); println!("base_url: will get root"); diff --git a/src/error.rs b/src/error.rs index c39d1aa..fdf9bc0 100644 --- a/src/error.rs +++ b/src/error.rs @@ -1,4 +1,23 @@ +use reqwest::header::InvalidHeaderValue; +use reqwest::Error; + #[derive(Debug)] pub struct GeneralError { pub(crate) description: String, } + +impl From for GeneralError { + fn from(value: InvalidHeaderValue) -> Self { + GeneralError { + description: "Unable to convert the token into header value".to_string(), + } + } +} + +impl From for GeneralError { + fn from(value: Error) -> Self { + GeneralError { + description: format!("Unable to perform a request: {}", value.to_string()), + } + } +} diff --git a/src/gitea/client.rs b/src/gitea/client.rs new file mode 100644 index 0000000..a3abd12 --- /dev/null +++ b/src/gitea/client.rs @@ -0,0 +1,54 @@ +use crate::error::GeneralError; +use reqwest::header::{HeaderMap, AUTHORIZATION, ACCEPT}; +use reqwest::{ClientBuilder, StatusCode}; +use serde::de::DeserializeOwned; +use url::Url; +use crate::config::GiteaConfig; + +#[derive(Debug)] +pub struct Client { + token: String, + base_uri: String, +} + +impl Client { + + pub fn from_config(config: &GiteaConfig) -> Self { + Self::new(&config.token, &config.domain) + } + + pub fn new(token: &String, domain: &String) -> Self { + Client { + token: token.clone(), + base_uri: format!("https://{}", domain.clone()), + } + } + + pub async fn get(&self, url: Url) -> Result { + let mut headers = HeaderMap::new(); + headers.append(AUTHORIZATION, self.token.parse()?); + headers.append(ACCEPT, "application/json".parse()?); + + let client = ClientBuilder::new() + .default_headers(headers) + .build() + .unwrap(); + + let response = client.get(url.clone()).send().await?; + + match response.status() { + StatusCode::OK => { + let result: T = response.json().await?; + + return Ok(result); + } + _ => Err(GeneralError { + description: format!( + "Could not call GET on {:?}, error code {}", + url, + response.status() + ), + }), + } + } +} diff --git a/src/gitea/issue.rs b/src/gitea/issue.rs new file mode 100644 index 0000000..ad7fc57 --- /dev/null +++ b/src/gitea/issue.rs @@ -0,0 +1,22 @@ +use crate::gitea::client::Client; +use serde::Deserialize; +use crate::gitea::repository::Repository; + +#[derive(Debug, Deserialize)] +pub struct Issue { + id: u64, + number: u64, + title: String, + body: String, + repository: Repository +} + +pub trait IssueClient { + fn get_issue(owner_or_organisation: &String, repo: &String, number: &u64) -> Option; +} + +impl IssueClient for Client { + fn get_issue(owner_or_organisation: &String, repo: &String, number: &u64) -> Option { + todo!() + } +} diff --git a/src/gitea/mod.rs b/src/gitea/mod.rs new file mode 100644 index 0000000..c47c4ac --- /dev/null +++ b/src/gitea/mod.rs @@ -0,0 +1,3 @@ +pub mod client; +pub mod issue; +pub mod repository; diff --git a/src/gitea/repository.rs b/src/gitea/repository.rs new file mode 100644 index 0000000..0e0ba74 --- /dev/null +++ b/src/gitea/repository.rs @@ -0,0 +1,9 @@ +use serde::Deserialize; + +#[derive(Debug, Deserialize)] +pub struct Repository { + id: u64, + name: String, + owner: String, + full_name: String, +} diff --git a/src/main.rs b/src/main.rs index 19789f7..fc7ff5f 100644 --- a/src/main.rs +++ b/src/main.rs @@ -7,6 +7,7 @@ mod cli; mod config; mod debug; mod error; +mod gitea; mod gitlab; mod openproject; mod planning; -- 2.45.2 From 9ef98e5044467acac0a864f92defb6d8c5a1b382 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julien=20Fastr=C3=A9?= Date: Thu, 24 Oct 2024 23:14:14 +0200 Subject: [PATCH 2/3] Refactor the issue2work action - separation the logic to handle issues from gitlab into the gitlab mod - create a kind of visitor pattern to check between the different providers --- src/debug.rs | 10 ++- src/gitea/client.rs | 8 +-- src/gitea/issue.rs | 4 +- src/gitlab/action.rs | 139 +++++++++++++++++++++++++++++++++++++ src/gitlab/client.rs | 67 +++++++++++------- src/gitlab/mod.rs | 1 + src/planning/issue2work.rs | 139 ++++++------------------------------- src/planning/mod.rs | 13 ++++ 8 files changed, 228 insertions(+), 153 deletions(-) create mode 100644 src/gitlab/action.rs diff --git a/src/debug.rs b/src/debug.rs index 6a9f8b2..790abea 100644 --- a/src/debug.rs +++ b/src/debug.rs @@ -1,17 +1,21 @@ -use url::Url; use crate::config::Config; use crate::error::GeneralError; use crate::gitea::issue::Issue; use crate::openproject::client::Client; use crate::openproject::root::RootClient; use crate::openproject::user::GetMe; +use url::Url; pub(crate) async fn debug(config: Config) -> Result<(), GeneralError> { println!("test"); - let gitea_client = crate::gitea::client::Client::from_config(config.gitea.first().unwrap()); - let issue: Issue = gitea_client.get(Url::parse("https://gitea.champs-libres.be/api/v1/repos/julienfastre/test/issues/6").unwrap()).await?; + let issue: Issue = gitea_client + .get( + Url::parse("https://gitea.champs-libres.be/api/v1/repos/julienfastre/test/issues/6") + .unwrap(), + ) + .await?; println!("issue: {:?}", issue); diff --git a/src/gitea/client.rs b/src/gitea/client.rs index a3abd12..cfce202 100644 --- a/src/gitea/client.rs +++ b/src/gitea/client.rs @@ -1,9 +1,10 @@ +use crate::config::{Config, GiteaConfig, GitlabConfig}; use crate::error::GeneralError; -use reqwest::header::{HeaderMap, AUTHORIZATION, ACCEPT}; +use gitlab::AsyncGitlab; +use reqwest::header::{HeaderMap, ACCEPT, AUTHORIZATION}; use reqwest::{ClientBuilder, StatusCode}; use serde::de::DeserializeOwned; use url::Url; -use crate::config::GiteaConfig; #[derive(Debug)] pub struct Client { @@ -12,7 +13,6 @@ pub struct Client { } impl Client { - pub fn from_config(config: &GiteaConfig) -> Self { Self::new(&config.token, &config.domain) } @@ -40,7 +40,7 @@ impl Client { StatusCode::OK => { let result: T = response.json().await?; - return Ok(result); + Ok(result) } _ => Err(GeneralError { description: format!( diff --git a/src/gitea/issue.rs b/src/gitea/issue.rs index ad7fc57..416365c 100644 --- a/src/gitea/issue.rs +++ b/src/gitea/issue.rs @@ -1,6 +1,6 @@ use crate::gitea::client::Client; -use serde::Deserialize; use crate::gitea::repository::Repository; +use serde::Deserialize; #[derive(Debug, Deserialize)] pub struct Issue { @@ -8,7 +8,7 @@ pub struct Issue { number: u64, title: String, body: String, - repository: Repository + repository: Repository, } pub trait IssueClient { diff --git a/src/gitlab/action.rs b/src/gitlab/action.rs new file mode 100644 index 0000000..1b9822f --- /dev/null +++ b/src/gitlab/action.rs @@ -0,0 +1,139 @@ +use crate::cli::Issue2Work; +use crate::config::Config; +use crate::error::GeneralError; +use crate::gitlab::client::{client_for_url, has_client_for_url}; +use crate::gitlab::issue::IssueBundle; +use crate::openproject::client::Client; +use crate::openproject::user::{GetMe, User}; +use crate::openproject::work::WorkPackageWriterAssignee; +use crate::planning::Issue2WorkActionTrait; +use gitlab::api::{issues, projects, AsyncQuery}; +use gitlab::{Issue, Project}; +use url::Url; + +#[derive(Debug)] +struct IssueInfo { + project: String, + iid: u64, +} + +/// details on how to create a work package from various informations +#[derive(Debug)] +struct Issue2WorkPackageDTO { + pub issue: IssueBundle, + pub assign_to: Option, +} + +pub(crate) struct GitlabAction {} + +impl Issue2WorkActionTrait for GitlabAction { + async fn run(&self, url: &Url, config: &Config, args: &Issue2Work) -> Result<(), GeneralError> { + let client = client_for_url(&url, &config).await?; + let data = extract_issue_info(&url).unwrap(); + + let endpoint = issues::ProjectIssues::builder() + .iid(data.iid) + .project(String::from(data.project)) + .build() + .unwrap(); + + let issues: Vec = endpoint.query_async(&client).await.unwrap(); + let issue = issues.first().unwrap(); + + let project_endpoint = projects::Project::builder() + .project(issue.project_id.value()) + .build() + .unwrap(); + + let project: Project = project_endpoint.query_async(&client).await.unwrap(); + + let issue_bundle = IssueBundle::new(&issue, &project); + + let open_project_client = Client::from_config(&config.openproject); + + let dto = Issue2WorkPackageDTO { + issue: issue_bundle, + assign_to: match args.assign_to_me { + true => { + let u = open_project_client.me().await?; + Some(u) + } + false => None, + }, + }; + + let work_package = open_project_client + .create_work_package(&(&dto).into(), &args.project_id) + .await?; + + println!( + "new work package created: {:?}, edit at {}/projects/{}/work_packages/{}", + work_package.subject, config.openproject.base_url, args.project_id, work_package.id + ); + + Ok(()) + } + + fn supports(&self, url: &Url, config: &Config, args: &Issue2Work) -> bool { + has_client_for_url(&url, &config) + } +} + +fn extract_issue_info(url: &Url) -> Option { + let parts = url + .path_segments() + .expect("Could not parse path segment of given url"); + let mut project_url: Vec = Vec::with_capacity(3); + let mut iid: Option = None; + + let mut project_found = false; + + for el in parts { + if el == "-" { + project_found = true; + continue; + } + if el == "issues" { + continue; + } + if !project_found { + project_url.push(String::from(el)); + } else { + // must be the id + iid = Some(String::from(el)); + break; + } + } + + Some(IssueInfo { + project: project_url.join("/"), + iid: iid + .expect("iid of the issue not found") + .parse() + .expect("could not transform issue id to u64"), + }) +} + +impl From<&Issue2WorkPackageDTO> for crate::openproject::work::WorkPackageWriter { + fn from(value: &Issue2WorkPackageDTO) -> Self { + crate::openproject::work::WorkPackageWriter { + subject: format!( + "{} ({}/{})", + value.issue.issue.title, + value.issue.project.name_with_namespace, + value.issue.issue.iid + ), + work_type: "TASK".into(), + description: crate::openproject::work::DescriptionWriter { + format: "markdown".into(), + raw: format!("From gitlab: {}", value.issue.issue.web_url), + }, + assignee: WorkPackageWriterAssignee { + href: match &value.assign_to { + None => None, + Some(w) => Some(w.clone().d_links.d_self.href), + }, + }, + } + } +} diff --git a/src/gitlab/client.rs b/src/gitlab/client.rs index d84979b..11a3fd0 100644 --- a/src/gitlab/client.rs +++ b/src/gitlab/client.rs @@ -1,35 +1,48 @@ -use crate::config::Config; +use crate::config::{Config, GitlabConfig}; use crate::error::GeneralError; -use gitlab::AsyncGitlab; -use gitlab::GitlabBuilder; +use gitlab::{AsyncGitlab, GitlabBuilder}; use url::Url; -pub trait ClientProviderTrait { - async fn client_for_url(url: &Url, config: &Config) -> Result; -} - pub struct ClientProvider {} -impl ClientProviderTrait for ClientProvider { - async fn client_for_url(url: &Url, config: &Config) -> Result { - for c in &config.gitlab { - if url.domain() == Some(c.domain.as_str()) { - let client = GitlabBuilder::new("gitlab.com", c.token.clone()) - .build_async() - .await; +impl ClientProvider {} - return match client { - Ok(new_client) => Ok(new_client), - Err(e) => { - let new_error = e.into(); - Err(new_error) - } - }; - } - } - - Err(GeneralError { - description: format!("No client available for this domain: {:?}", url.domain()), - }) +fn is_client_for_url(url: &Url, config: &GitlabConfig) -> bool { + if url.domain() == Some(config.domain.as_str()) { + return true; } + + false +} + +pub async fn client_for_url(url: &Url, config: &Config) -> Result { + for c in &config.gitlab { + if is_client_for_url(url, c) { + let client = GitlabBuilder::new("gitlab.com", c.token.clone()) + .build_async() + .await; + + return match client { + Ok(new_client) => Ok(new_client), + Err(e) => { + let new_error = e.into(); + Err(new_error) + } + }; + } + } + + Err(GeneralError { + description: format!("No client available for this domain: {:?}", url.domain()), + }) +} + +pub fn has_client_for_url(url: &Url, config: &Config) -> bool { + for c in &config.gitlab { + if (is_client_for_url(url, c)) { + return true; + } + } + + false } diff --git a/src/gitlab/mod.rs b/src/gitlab/mod.rs index e278a81..b26b243 100644 --- a/src/gitlab/mod.rs +++ b/src/gitlab/mod.rs @@ -1,3 +1,4 @@ +pub mod action; pub mod client; pub mod issue; diff --git a/src/planning/issue2work.rs b/src/planning/issue2work.rs index 7c19cc5..695728c 100644 --- a/src/planning/issue2work.rs +++ b/src/planning/issue2work.rs @@ -1,132 +1,37 @@ use crate::cli::Issue2Work; use crate::config::Config; use crate::error::GeneralError; -use crate::gitlab::client::{ClientProvider, ClientProviderTrait}; -use crate::gitlab::issue::IssueBundle; -use crate::openproject::client::Client; -use crate::openproject::user::{GetMe, User}; -use crate::openproject::work::WorkPackageWriterAssignee; -use gitlab::api::{issues, projects, AsyncQuery}; -use gitlab::{Issue, Project}; +use crate::gitlab::action::GitlabAction; +use crate::planning::Issue2WorkActionTrait; use url::Url; -#[derive(Debug)] -struct IssueInfo { - project: String, - iid: u64, +struct App { + gitlab_issue2work_action: GitlabAction, } -/// details on how to create a work package from various informations -#[derive(Debug)] -struct Issue2WorkPackageDTO { - pub issue: IssueBundle, - pub assign_to: Option, -} - -impl From<&Issue2WorkPackageDTO> for crate::openproject::work::WorkPackageWriter { - fn from(value: &Issue2WorkPackageDTO) -> Self { - crate::openproject::work::WorkPackageWriter { - subject: format!( - "{} ({}/{})", - value.issue.issue.title, - value.issue.project.name_with_namespace, - value.issue.issue.iid - ), - work_type: "TASK".into(), - description: crate::openproject::work::DescriptionWriter { - format: "markdown".into(), - raw: format!("From gitlab: {}", value.issue.issue.web_url), - }, - assignee: WorkPackageWriterAssignee { - href: match &value.assign_to { - None => None, - Some(w) => Some(w.clone().d_links.d_self.href), - }, - }, - } - } +trait Issue2WorkPackageAction { + fn issue2work_package_action( + &self, + config: &Config, + args: &Issue2Work, + ) -> Result<(), GeneralError>; } pub(crate) async fn issue2work(config: Config, args: &Issue2Work) -> Result<(), GeneralError> { let url = Url::parse(&*args.issue_url).expect("issue_url is not valid"); - let data = extract_issue_info(&url).unwrap(); - - let client = ClientProvider::client_for_url(&url, &config).await?; - - let endpoint = issues::ProjectIssues::builder() - .iid(data.iid) - .project(String::from(data.project)) - .build() - .unwrap(); - - let issues: Vec = endpoint.query_async(&client).await.unwrap(); - let issue = issues.first().unwrap(); - - let project_endpoint = projects::Project::builder() - .project(issue.project_id.value()) - .build() - .unwrap(); - - let project: Project = project_endpoint.query_async(&client).await.unwrap(); - - let issue_bundle = IssueBundle::new(&issue, &project); - - let open_project_client = Client::from_config(&config.openproject); - - let dto = Issue2WorkPackageDTO { - issue: issue_bundle, - assign_to: match args.assign_to_me { - true => { - let u = open_project_client.me().await?; - Some(u) - } - false => None, - }, + let app = App { + gitlab_issue2work_action: GitlabAction {}, }; - let work_package = open_project_client - .create_work_package(&(&dto).into(), &args.project_id) - .await?; - - println!( - "new work package created: {:?}, edit at {}/projects/{}/work_packages/{}", - work_package.subject, config.openproject.base_url, args.project_id, work_package.id - ); - - Ok(()) -} - -fn extract_issue_info(url: &Url) -> Option { - let parts = url - .path_segments() - .expect("Could not parse path segment of given url"); - let mut project_url: Vec = Vec::with_capacity(3); - let mut iid: Option = None; - - let mut project_found = false; - - for el in parts { - if el == "-" { - project_found = true; - continue; - } - if el == "issues" { - continue; - } - if !project_found { - project_url.push(String::from(el)); - } else { - // must be the id - iid = Some(String::from(el)); - break; - } + if (app + .gitlab_issue2work_action + .supports(&url, &config, args) + ) + { + app.gitlab_issue2work_action.run(&url, &config, args).await + } else { + Err(GeneralError { + description: format!("This action is not supported for this url: {}", url), + }) } - - Some(IssueInfo { - project: project_url.join("/"), - iid: iid - .expect("iid of the issue not found") - .parse() - .expect("could not transform issue id to u64"), - }) } diff --git a/src/planning/mod.rs b/src/planning/mod.rs index dd9b3f9..23561e9 100644 --- a/src/planning/mod.rs +++ b/src/planning/mod.rs @@ -1 +1,14 @@ +use crate::cli::Issue2Work; +use crate::config::Config; +use crate::error::GeneralError; +use crate::gitlab::client::ClientProvider; +use gitlab::{AsyncGitlab, GitlabBuilder}; +use url::Url; + pub(crate) mod issue2work; + +pub trait Issue2WorkActionTrait { + async fn run(&self, url: &Url, config: &Config, args: &Issue2Work) -> Result<(), GeneralError>; + + fn supports(&self, url: &Url, config: &Config, args: &Issue2Work) -> bool; +} -- 2.45.2 From 2b37c5b18cfab48a07ddb0c5cb68728e195e5a19 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julien=20Fastr=C3=A9?= Date: Fri, 25 Oct 2024 00:29:34 +0200 Subject: [PATCH 3/3] Add Gitea Action support for issue-to-work conversion Implemented Gitea actions to support converting Gitea issues into work packages in OpenProject. Modified various modules to include necessary structs, functions, and tests to handle Gitea issues, ensure URL validation, and adapt work package creation based on Gitea issues. --- src/gitea/action.rs | 63 ++++++++++++++++++++++++++++++++++++++ src/gitea/client.rs | 46 +++++++++++++++++++++++++++- src/gitea/issue.rs | 44 +++++++++++++++++++++++--- src/gitea/mod.rs | 1 + src/gitea/repository.rs | 2 +- src/gitlab/action.rs | 2 +- src/gitlab/client.rs | 2 +- src/openproject/user.rs | 3 ++ src/openproject/work.rs | 12 ++++++++ src/planning/issue2work.rs | 19 ++++-------- 10 files changed, 172 insertions(+), 22 deletions(-) create mode 100644 src/gitea/action.rs diff --git a/src/gitea/action.rs b/src/gitea/action.rs new file mode 100644 index 0000000..c93a473 --- /dev/null +++ b/src/gitea/action.rs @@ -0,0 +1,63 @@ +use url::Url; +use crate::cli::Issue2Work; +use crate::config::Config; +use crate::error::GeneralError; +use crate::gitea::issue::{issue_html_url_to_api, Issue}; +use crate::gitea::client::has_client_for_url; +use crate::openproject::user::{GetMe, User}; +use crate::openproject::work::{WorkPackage, WorkPackageWriter, WorkPackageWriterAssignee}; +use crate::planning::Issue2WorkActionTrait; + +pub(crate) struct GiteaAction {} + +impl Issue2WorkActionTrait for GiteaAction { + async fn run(&self, url: &Url, config: &Config, args: &Issue2Work) -> Result<(), GeneralError> { + let gitea_client = crate::gitea::client::Client::from_config(config.gitea.first().unwrap()); + let issue: Issue = gitea_client + .get(issue_html_url_to_api(url)?) + .await?; + let open_project_client = crate::openproject::client::Client::from_config(&config.openproject); + + let work_package = create_work_package_from_issue( + &issue, + match args.assign_to_me { + true => { + let u = open_project_client.me().await?; + Some(u) + } + false => None, + } + ); + + let work_package = open_project_client + .create_work_package(&work_package, &args.project_id) + .await?; + + println!( + "new work package created: {:?}, edit at {}/projects/{}/work_packages/{}", + work_package.subject, config.openproject.base_url, args.project_id, work_package.id + ); + + Ok(()) + } + + fn supports(&self, url: &Url, config: &Config, _args: &Issue2Work) -> bool { + has_client_for_url(&url, &config) + } +} + +fn create_work_package_from_issue(issue: &Issue, assignee: Option) -> WorkPackageWriter { + WorkPackageWriter { + subject: format!( + "{} ({})", + issue.title, + issue.repository.full_name + ), + work_type: "TASK".into(), + description: crate::openproject::work::DescriptionWriter { + format: "markdown".into(), + raw: format!("From Gitea issue: {} \n\n{}", issue.html_url, issue.body), + }, + assignee: assignee.into() + } +} \ No newline at end of file diff --git a/src/gitea/client.rs b/src/gitea/client.rs index cfce202..65dc5f9 100644 --- a/src/gitea/client.rs +++ b/src/gitea/client.rs @@ -1,6 +1,5 @@ use crate::config::{Config, GiteaConfig, GitlabConfig}; use crate::error::GeneralError; -use gitlab::AsyncGitlab; use reqwest::header::{HeaderMap, ACCEPT, AUTHORIZATION}; use reqwest::{ClientBuilder, StatusCode}; use serde::de::DeserializeOwned; @@ -12,6 +11,35 @@ pub struct Client { base_uri: String, } +fn is_client_for_url(url: &Url, config: &GiteaConfig) -> bool { + if url.domain() == Some(config.domain.as_str()) { + return true; + } + false +} + +pub(crate) fn has_client_for_url(url: &Url, config: &Config) -> bool { + for c in &config.gitea { + if is_client_for_url(url, c) { + return true; + } + } + + false +} + +fn client_for_url(url: &Url, config: &Config) -> Result { + for c in &config.gitea { + if is_client_for_url(url, c) { + return Ok(Client::from_config(&c)); + } + } + + Err(GeneralError { + description: format!("No gitea client found for url {}", url), + }) +} + impl Client { pub fn from_config(config: &GiteaConfig) -> Self { Self::new(&config.token, &config.domain) @@ -52,3 +80,19 @@ impl Client { } } } + +#[cfg(test)] +mod test { + use super::*; + + #[test] + fn test_is_client_for_url() { + let config = GiteaConfig { + domain: "gitea.champs-libres.be".into(), + token: "".into(), + }; + + assert_eq!(is_client_for_url(&Url::parse("https://gitea.champs-libres.be/something/somewhere").unwrap(), &config), true); + assert_eq!(is_client_for_url(&Url::parse("https://somewhere.else/something/somewhere").unwrap(), &config), false); + } +} diff --git a/src/gitea/issue.rs b/src/gitea/issue.rs index 416365c..e087049 100644 --- a/src/gitea/issue.rs +++ b/src/gitea/issue.rs @@ -1,14 +1,17 @@ use crate::gitea::client::Client; use crate::gitea::repository::Repository; use serde::Deserialize; +use url::Url; +use crate::error::GeneralError; #[derive(Debug, Deserialize)] pub struct Issue { - id: u64, + pub id: u64, number: u64, - title: String, - body: String, - repository: Repository, + pub title: String, + pub body: String, + pub repository: Repository, + pub html_url: String, } pub trait IssueClient { @@ -16,7 +19,38 @@ pub trait IssueClient { } impl IssueClient for Client { - fn get_issue(owner_or_organisation: &String, repo: &String, number: &u64) -> Option { + fn get_issue(_owner_or_organisation: &String, _repo: &String, number: &u64) -> Option { todo!() } } + +pub fn issue_html_url_to_api(url: &Url) -> Result { + let mut parts = url.path_segments().unwrap(); + let domain = parts.next().unwrap(); + let repo = parts.next().unwrap(); + let issue = parts.next().unwrap(); + let iid = parts.next().unwrap(); + + if (!issue.eq("issues")) { + return Err(GeneralError { + description: format!("Issue url is not valid: {}", url), + }); + } + + let url = Url::parse(format!("{}://{}/api/v1/repos/{}/{}/issues/{}", url.scheme(), url.host().unwrap(), domain, repo, iid).as_str()).unwrap(); + + Ok(url) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_issue_html_url_to_api() { + let url = Url::parse("https://gitea.champs-libres.be/champs-libres/test/issues/1").unwrap(); + let result = issue_html_url_to_api(&url).unwrap(); + + assert_eq!(result.as_str(), "https://gitea.champs-libres.be/api/v1/repos/champs-libres/test/issues/1"); + } +} diff --git a/src/gitea/mod.rs b/src/gitea/mod.rs index c47c4ac..32c6c76 100644 --- a/src/gitea/mod.rs +++ b/src/gitea/mod.rs @@ -1,3 +1,4 @@ pub mod client; pub mod issue; pub mod repository; +pub(crate) mod action; diff --git a/src/gitea/repository.rs b/src/gitea/repository.rs index 0e0ba74..80d50db 100644 --- a/src/gitea/repository.rs +++ b/src/gitea/repository.rs @@ -5,5 +5,5 @@ pub struct Repository { id: u64, name: String, owner: String, - full_name: String, + pub full_name: String, } diff --git a/src/gitlab/action.rs b/src/gitlab/action.rs index 1b9822f..ef3c4d7 100644 --- a/src/gitlab/action.rs +++ b/src/gitlab/action.rs @@ -74,7 +74,7 @@ impl Issue2WorkActionTrait for GitlabAction { Ok(()) } - fn supports(&self, url: &Url, config: &Config, args: &Issue2Work) -> bool { + fn supports(&self, url: &Url, config: &Config, _args: &Issue2Work) -> bool { has_client_for_url(&url, &config) } } diff --git a/src/gitlab/client.rs b/src/gitlab/client.rs index 11a3fd0..4b3d2a6 100644 --- a/src/gitlab/client.rs +++ b/src/gitlab/client.rs @@ -39,7 +39,7 @@ pub async fn client_for_url(url: &Url, config: &Config) -> Result bool { for c in &config.gitlab { - if (is_client_for_url(url, c)) { + if is_client_for_url(url, c) { return true; } } diff --git a/src/openproject/user.rs b/src/openproject/user.rs index 3e29a3f..54965a1 100644 --- a/src/openproject/user.rs +++ b/src/openproject/user.rs @@ -11,8 +11,11 @@ pub struct UserLink { #[derive(Deserialize, Debug, Clone)] pub struct User { #[serde(rename = "_type")] + #[allow(unused_variables)] pub d_type: String, + #[allow(unused_variables)] pub id: u64, + #[allow(unused_variables)] pub name: String, #[serde(rename = "_links")] pub d_links: UserLink, diff --git a/src/openproject/work.rs b/src/openproject/work.rs index d9aca46..5520698 100644 --- a/src/openproject/work.rs +++ b/src/openproject/work.rs @@ -1,4 +1,5 @@ use serde::{Deserialize, Serialize}; +use crate::openproject::user::User; #[derive(Serialize, Debug)] pub struct WorkPackageWriterAssignee { @@ -24,3 +25,14 @@ pub struct WorkPackage { pub id: u64, pub subject: String, } + +impl From> for WorkPackageWriterAssignee { + fn from(value: Option) -> Self { + WorkPackageWriterAssignee { + href: match value { + None => None, + Some(w) => Some(w.clone().d_links.d_self.href), + }, + } + } +} diff --git a/src/planning/issue2work.rs b/src/planning/issue2work.rs index 695728c..2e1189b 100644 --- a/src/planning/issue2work.rs +++ b/src/planning/issue2work.rs @@ -1,34 +1,27 @@ use crate::cli::Issue2Work; use crate::config::Config; use crate::error::GeneralError; +use crate::gitea::action::GiteaAction; use crate::gitlab::action::GitlabAction; use crate::planning::Issue2WorkActionTrait; use url::Url; struct App { gitlab_issue2work_action: GitlabAction, -} - -trait Issue2WorkPackageAction { - fn issue2work_package_action( - &self, - config: &Config, - args: &Issue2Work, - ) -> Result<(), GeneralError>; + gitea_issue2work_action: GiteaAction, } pub(crate) async fn issue2work(config: Config, args: &Issue2Work) -> Result<(), GeneralError> { let url = Url::parse(&*args.issue_url).expect("issue_url is not valid"); let app = App { gitlab_issue2work_action: GitlabAction {}, + gitea_issue2work_action: GiteaAction {}, }; - if (app - .gitlab_issue2work_action - .supports(&url, &config, args) - ) - { + if app.gitlab_issue2work_action.supports(&url, &config, args) { app.gitlab_issue2work_action.run(&url, &config, args).await + } else if app.gitea_issue2work_action.supports(&url, &config, args) { + app.gitea_issue2work_action.run(&url, &config, args).await } else { Err(GeneralError { description: format!("This action is not supported for this url: {}", url), -- 2.45.2