Loading acorn-cli/src/commands/gather/mod.rs +1 −1 Original line number Diff line number Diff line Loading @@ -31,7 +31,7 @@ pub async fn run( // let langs = gitlab::languages().await?; // dbg!(langs); // gitlab_merge_request_note_example().await; // gitlab_example().await; gitlab_example().await; // orcid_example().await; // ror_example().await; Ok(()) Loading acorn-lib/assets/constants/application.json +1 −1 Original line number Diff line number Diff line Loading @@ -34,7 +34,7 @@ { "name": "tree", "method": "get", "template": "{{ base }}/{{ path }}/git/trees/{{ branch }}?recursive=1" "template": "{{ base }}/repos/{{ path }}/git/trees/{{ branch }}{{ query }}" } ] }, Loading acorn-lib/src/io/api/github.rs +18 −24 Original line number Diff line number Diff line //! Module for interacting with GitHub API //! use crate::io::api::{Endpoint, RemoteResource, Resource, TreeEntryType}; use crate::io::api::{Endpoint, RemoteResource, TreeEntryType}; use crate::io::ApiResult; use crate::param; use color_eyre::eyre::eyre; Loading Loading @@ -75,20 +75,15 @@ impl TreeEntry { self.entry_type.eq(&TreeEntryType::Blob) } } /// Construct an [`Endpoint`] for any GitHub API host with the repository tree resource registered pub(crate) fn endpoint_for(host: impl Into<String>) -> Endpoint { let tree = Resource::init() .name("tree") .method("get") .template("{{ base }}/repos{{ path }}/git/trees/{{ branch }}{{ query }}") .build(); Endpoint::at(host).resources(vec![tree]).build() } /// Fetch repository tree blob paths for a GitHub repository and branch pub(crate) async fn tree_paths(endpoint: &Endpoint, path: impl Into<String>, branch: impl Into<String>) -> ApiResult<Vec<String>> { pub(crate) async fn tree_paths(host: impl Into<String>, path: impl Into<String>, branch: impl Into<String>) -> ApiResult<Vec<String>> { let endpoint = match Endpoint::from_template("github").map(|e| e.with_domain(host)) { | Ok(endpoint) => endpoint, | Err(why) => return Err(why), }; let path = path.into(); let branch = branch.into(); let recursive_response = match fetch_tree(endpoint, path.as_str(), branch.as_str(), true).await { let recursive_response = match fetch_tree(&endpoint, path.as_str(), branch.as_str(), true).await { | Ok(data) => data, | Err(why) => return Err(why), }; Loading @@ -96,14 +91,14 @@ pub(crate) async fn tree_paths(endpoint: &Endpoint, path: impl Into<String>, bra let (paths, _) = collect_tree(recursive_response.tree, ""); return Ok(paths); } let root_response = match fetch_tree(endpoint, path.as_str(), branch.as_str(), false).await { let root_response = match fetch_tree(&endpoint, path.as_str(), branch.as_str(), false).await { | Ok(data) if !data.truncated => data, | Ok(_) => return Err(eyre!("Failed to fetch complete GitHub tree for {path}")), | Err(why) => return Err(why), }; let (mut all_paths, mut pending) = collect_tree(root_response.tree, ""); while let Some((sha, prefix)) = pending.pop() { let subtree_response = match fetch_tree(endpoint, path.as_str(), sha.as_str(), false).await { let subtree_response = match fetch_tree(&endpoint, path.as_str(), sha.as_str(), false).await { | Ok(data) if !data.truncated => data, | Ok(_) => return Err(eyre!("Failed to fetch complete GitHub tree for {path} at subtree {prefix}")), | Err(why) => return Err(why), Loading @@ -114,13 +109,6 @@ pub(crate) async fn tree_paths(endpoint: &Endpoint, path: impl Into<String>, bra } Ok(all_paths) } fn path_for(prefix: &str, path: &str) -> String { if prefix.is_empty() { path.to_string() } else { format!("{prefix}/{path}") } } fn collect_tree(entries: Vec<TreeEntry>, prefix: &str) -> (Vec<String>, Vec<(String, String)>) { entries.into_iter().fold((vec![], vec![]), |(mut paths, mut pending), entry| { let is_blob = entry.is_blob(); Loading @@ -146,16 +134,22 @@ async fn fetch_tree(endpoint: &Endpoint, path: &str, branch: &str, recursive: bo let response = endpoint.invoke("tree", Some(params)).await; endpoint.handle::<TreeResponse>(response) } fn path_for(prefix: &str, path: &str) -> String { if prefix.is_empty() { path.to_string() } else { format!("{prefix}/{path}") } } #[cfg(test)] mod tests { use super::*; #[test] fn test_endpoint_for() { let endpoint = endpoint_for("api.github.com"); fn test_template_endpoint_for() { let endpoint = Endpoint::from_template("github").map(|e| e.with_domain("api.github.com")).unwrap(); assert_eq!(endpoint.base(), "https://api.github.com"); let endpoint = endpoint_for("api.github.com"); assert!(endpoint.resources.iter().any(|r| r.name == "tree")); } #[test] Loading acorn-lib/src/io/api/gitlab.rs +58 −59 Original line number Diff line number Diff line Loading @@ -2,9 +2,7 @@ //! // TODO: Add custom field list and query pair type // TODO: Finish modeling necessary API endpoints use crate::io::api::{ DatabasePersistence, EmptyField, Endpoint, RemoteResource, Resource, ResponseContent, TreeEntryType, ValueValidator, INCLUDED_ENDPOINTS, }; use crate::io::api::{DatabasePersistence, EmptyField, Endpoint, RemoteResource, ResponseContent, TreeEntryType, ValueValidator, INCLUDED_ENDPOINTS}; use crate::io::config::{RunnerStatus, RunnerType}; use crate::io::database::schema::{ProgrammingLanguageRow, Table}; use crate::io::database::{Database, Operations}; Loading Loading @@ -377,6 +375,9 @@ pub struct Options { pub internal_identifier: Option<String>, /// Request body payload pub body: Option<String>, /// GitLab domain (defaults to gitlab.com) #[builder(default = String::from("gitlab.com"))] pub domain: String, } /// GitLab API response for runner details /// ### Example JSON response Loading Loading @@ -575,6 +576,7 @@ impl Options { /// - `CI_JOB_TOKEN` -> `token` (defaults to empty string when unset) /// - `CI_PROJECT_ID` -> `identifier` /// - `CI_MERGE_REQUEST_IID` -> `internal_identifier` /// - `CI_SERVER_HOST` -> `domain` (defaults to gitlab.com when unset) /// /// See <https://docs.gitlab.com/ci/variables/predefined_variables> for more information on available GitLab CI environment variables pub fn from_env() -> Self { Loading @@ -583,6 +585,7 @@ impl Options { token: var("CI_JOB_TOKEN").unwrap_or_default(), identifier: var("CI_PROJECT_ID").ok(), internal_identifier: var("CI_MERGE_REQUEST_IID").ok(), domain: var("CI_SERVER_HOST").unwrap_or_else(|_| "gitlab.com".to_string()), body: None, } } Loading @@ -600,6 +603,13 @@ impl Options { ..self } } /// Return a copy of options with GitLab domain set pub fn with_domain(self, domain: impl Into<String>) -> Self { Self { domain: domain.into(), ..self } } } impl From<ProgrammingLanguageMetadata> for ProgrammingLanguageRow { fn from(value: ProgrammingLanguageMetadata) -> Self { Loading Loading @@ -753,11 +763,12 @@ impl ValueValidator for PaginationField { } /// Get descendant groups of a group by identifier pub async fn groups(options: Options) -> ApiResult<GroupsResponse> { let name = "GitLab"; let action = "groups"; match INCLUDED_ENDPOINTS.find_by_name(name) { | Some(endpoint) => { let Options { token, identifier, .. } = options; let Options { token, identifier, domain, .. } = options; match Endpoint::from_template("gitlab").map(|e| e.with_domain(&domain)) { | Ok(endpoint) => { let params = vec![ param!(Header, "PRIVATE-TOKEN", &token), param!(TemplateValue, "identifier", &identifier.unwrap_or_default()), Loading @@ -767,7 +778,7 @@ pub async fn groups(options: Options) -> ApiResult<GroupsResponse> { let response = endpoint.invoke_with::<PaginationField, EmptyField>(action, Some(params)).await; endpoint.handle::<GroupsResponse>(response) } | None => Err(eyre!("{name} API endpoint not found")), | Err(why) => Err(why), } } /// Download programming language metadata from GitLab linguist source file Loading Loading @@ -802,16 +813,16 @@ pub async fn languages() -> ApiResult<ProgrammingLanguagesResponse> { /// /// See <https://docs.gitlab.com/api/notes/#create-a-merge-request-note> for more information on this API endpoint and required parameters pub async fn merge_request_note(options: Options) -> ApiResult<MergeRequestNoteResponse> { let name = "GitLab"; let action = "merge-request-note"; match INCLUDED_ENDPOINTS.find_by_name(name) { | Some(endpoint) => { let Options { token, identifier, internal_identifier, body, domain, } = options; match Endpoint::from_template("gitlab").map(|e| e.with_domain(&domain)) { | Ok(endpoint) => { let body = body.unwrap_or_default(); let params = vec![ param!(Header, "PRIVATE-TOKEN", &token), Loading @@ -822,16 +833,17 @@ pub async fn merge_request_note(options: Options) -> ApiResult<MergeRequestNoteR let response = endpoint.invoke_with::<PaginationField, EmptyField>(action, Some(params)).await; endpoint.handle::<MergeRequestNoteResponse>(response) } | None => Err(eyre!("{name} API endpoint not found")), | Err(why) => Err(why), } } /// Get runner details by identifier pub async fn runner(options: Options) -> ApiResult<RunnerDetails> { let name = "GitLab"; let action = "runners"; match INCLUDED_ENDPOINTS.find_by_name(name) { | Some(endpoint) => { let Options { token, identifier, .. } = options; let Options { token, identifier, domain, .. } = options; match Endpoint::from_template("gitlab").map(|e| e.with_domain(&domain)) { | Ok(endpoint) => { let params = vec![ param!(Header, "PRIVATE-TOKEN", &token), param!(TemplateValue, "identifier", &identifier.unwrap_or_default()), Loading @@ -839,38 +851,32 @@ pub async fn runner(options: Options) -> ApiResult<RunnerDetails> { let response = endpoint.invoke(action, Some(params)).await; endpoint.handle::<RunnerDetails>(response) } | None => Err(eyre!("{name} API endpoint not found")), | Err(why) => Err(why), } } /// Get runners visible to user associated with given token pub async fn runners(options: Options) -> ApiResult<RunnersResponse> { let name = "GitLab"; let action = "runners"; match INCLUDED_ENDPOINTS.find_by_name(name) { | Some(endpoint) => { let params = vec![param!(Header, "PRIVATE-TOKEN", &options.token)]; let Options { token, domain, .. } = options; match Endpoint::from_template("gitlab").map(|e| e.with_domain(&domain)) { | Ok(endpoint) => { let params = vec![param!(Header, "PRIVATE-TOKEN", &token)]; let response = endpoint.invoke_with::<PaginationField, EmptyField>(action, Some(params)).await; endpoint.handle::<RunnersResponse>(response) } | None => Err(eyre!("{name} API endpoint not found")), } | Err(why) => Err(why), } /// Construct an [`Endpoint`] for any GitLab host with the repository tree resource registered pub(crate) fn endpoint_for(host: impl Into<String>) -> Endpoint { let tree = Resource::init() .name("tree") .method("get") .template("{{ base }}/projects/{{ identifier }}/repository/tree{{ query }}") .build(); Endpoint::at(host).root("api/v4").resources(vec![tree]).build() } /// Fetch one page of repository tree blob paths for a GitLab project pub(crate) async fn tree_paths( endpoint: &Endpoint, host: impl Into<String>, identifier: impl Into<String>, directory: impl Into<String>, page: u32, ) -> ApiResult<Vec<String>> { match Endpoint::from_template("gitlab").map(|e| e.with_domain(host.into())) { | Ok(endpoint) => { let params = vec![ param!(TemplateValue, "identifier", &identifier.into()), param!(KeyValuePair, "per_page", "100"), Loading @@ -883,22 +889,15 @@ pub(crate) async fn tree_paths( .handle::<TreeResponse>(response) .map(|entries| entries.into_iter().filter(|e| e.is_blob()).map(|e| e.path()).collect()) } | Err(why) => Err(why), } } #[cfg(test)] mod tests { use super::*; use crate::io::api::IntoBody; use core::iter::FromIterator; #[test] fn test_endpoint_for_base_url() { let endpoint = endpoint_for("code.ornl.gov"); assert_eq!(endpoint.base(), "https://code.ornl.gov/api/v4"); } #[test] fn test_endpoint_for_has_tree_resource() { let endpoint = endpoint_for("code.ornl.gov"); assert!(endpoint.resources.iter().any(|r| r.name == "tree")); } #[test] fn test_add_comment_payload_uses_body_field() { let payload = vec![param!(Body, "body", "This is a test comment!!!")].into_body(); Loading acorn-lib/src/io/api/mod.rs +20 −0 Original line number Diff line number Diff line Loading @@ -250,7 +250,27 @@ impl Endpoint { let root = root.as_ref().map_or(String::new(), |root| format!("/{root}")); format!("{scheme}://{domain}{port}{root}") } /// Create a new endpoint with a custom domain while preserving all other properties pub fn with_domain(&self, domain: impl Into<String>) -> Self { Self { domain: domain.into(), ..self.clone() } } /// Find an endpoint template by name from [`INCLUDED_ENDPOINTS`] /// /// # Example /// ```ignore /// let endpoint = Endpoint::from_template("gitlab")?.with_domain("my-gitlab.example.com"); /// ``` pub fn from_template(name: impl Into<String>) -> ApiResult<Self> { let endpoint_name = name.into(); INCLUDED_ENDPOINTS .find_by_name(&endpoint_name) .ok_or_else(|| eyre!("Endpoint template '{endpoint_name}' not found in application configuration")) } } impl Searchable<Endpoint> for Vec<Endpoint> { fn find_by_name(&self, value: impl Into<String>) -> Option<Endpoint> { let name = value.into(); Loading Loading
acorn-cli/src/commands/gather/mod.rs +1 −1 Original line number Diff line number Diff line Loading @@ -31,7 +31,7 @@ pub async fn run( // let langs = gitlab::languages().await?; // dbg!(langs); // gitlab_merge_request_note_example().await; // gitlab_example().await; gitlab_example().await; // orcid_example().await; // ror_example().await; Ok(()) Loading
acorn-lib/assets/constants/application.json +1 −1 Original line number Diff line number Diff line Loading @@ -34,7 +34,7 @@ { "name": "tree", "method": "get", "template": "{{ base }}/{{ path }}/git/trees/{{ branch }}?recursive=1" "template": "{{ base }}/repos/{{ path }}/git/trees/{{ branch }}{{ query }}" } ] }, Loading
acorn-lib/src/io/api/github.rs +18 −24 Original line number Diff line number Diff line //! Module for interacting with GitHub API //! use crate::io::api::{Endpoint, RemoteResource, Resource, TreeEntryType}; use crate::io::api::{Endpoint, RemoteResource, TreeEntryType}; use crate::io::ApiResult; use crate::param; use color_eyre::eyre::eyre; Loading Loading @@ -75,20 +75,15 @@ impl TreeEntry { self.entry_type.eq(&TreeEntryType::Blob) } } /// Construct an [`Endpoint`] for any GitHub API host with the repository tree resource registered pub(crate) fn endpoint_for(host: impl Into<String>) -> Endpoint { let tree = Resource::init() .name("tree") .method("get") .template("{{ base }}/repos{{ path }}/git/trees/{{ branch }}{{ query }}") .build(); Endpoint::at(host).resources(vec![tree]).build() } /// Fetch repository tree blob paths for a GitHub repository and branch pub(crate) async fn tree_paths(endpoint: &Endpoint, path: impl Into<String>, branch: impl Into<String>) -> ApiResult<Vec<String>> { pub(crate) async fn tree_paths(host: impl Into<String>, path: impl Into<String>, branch: impl Into<String>) -> ApiResult<Vec<String>> { let endpoint = match Endpoint::from_template("github").map(|e| e.with_domain(host)) { | Ok(endpoint) => endpoint, | Err(why) => return Err(why), }; let path = path.into(); let branch = branch.into(); let recursive_response = match fetch_tree(endpoint, path.as_str(), branch.as_str(), true).await { let recursive_response = match fetch_tree(&endpoint, path.as_str(), branch.as_str(), true).await { | Ok(data) => data, | Err(why) => return Err(why), }; Loading @@ -96,14 +91,14 @@ pub(crate) async fn tree_paths(endpoint: &Endpoint, path: impl Into<String>, bra let (paths, _) = collect_tree(recursive_response.tree, ""); return Ok(paths); } let root_response = match fetch_tree(endpoint, path.as_str(), branch.as_str(), false).await { let root_response = match fetch_tree(&endpoint, path.as_str(), branch.as_str(), false).await { | Ok(data) if !data.truncated => data, | Ok(_) => return Err(eyre!("Failed to fetch complete GitHub tree for {path}")), | Err(why) => return Err(why), }; let (mut all_paths, mut pending) = collect_tree(root_response.tree, ""); while let Some((sha, prefix)) = pending.pop() { let subtree_response = match fetch_tree(endpoint, path.as_str(), sha.as_str(), false).await { let subtree_response = match fetch_tree(&endpoint, path.as_str(), sha.as_str(), false).await { | Ok(data) if !data.truncated => data, | Ok(_) => return Err(eyre!("Failed to fetch complete GitHub tree for {path} at subtree {prefix}")), | Err(why) => return Err(why), Loading @@ -114,13 +109,6 @@ pub(crate) async fn tree_paths(endpoint: &Endpoint, path: impl Into<String>, bra } Ok(all_paths) } fn path_for(prefix: &str, path: &str) -> String { if prefix.is_empty() { path.to_string() } else { format!("{prefix}/{path}") } } fn collect_tree(entries: Vec<TreeEntry>, prefix: &str) -> (Vec<String>, Vec<(String, String)>) { entries.into_iter().fold((vec![], vec![]), |(mut paths, mut pending), entry| { let is_blob = entry.is_blob(); Loading @@ -146,16 +134,22 @@ async fn fetch_tree(endpoint: &Endpoint, path: &str, branch: &str, recursive: bo let response = endpoint.invoke("tree", Some(params)).await; endpoint.handle::<TreeResponse>(response) } fn path_for(prefix: &str, path: &str) -> String { if prefix.is_empty() { path.to_string() } else { format!("{prefix}/{path}") } } #[cfg(test)] mod tests { use super::*; #[test] fn test_endpoint_for() { let endpoint = endpoint_for("api.github.com"); fn test_template_endpoint_for() { let endpoint = Endpoint::from_template("github").map(|e| e.with_domain("api.github.com")).unwrap(); assert_eq!(endpoint.base(), "https://api.github.com"); let endpoint = endpoint_for("api.github.com"); assert!(endpoint.resources.iter().any(|r| r.name == "tree")); } #[test] Loading
acorn-lib/src/io/api/gitlab.rs +58 −59 Original line number Diff line number Diff line Loading @@ -2,9 +2,7 @@ //! // TODO: Add custom field list and query pair type // TODO: Finish modeling necessary API endpoints use crate::io::api::{ DatabasePersistence, EmptyField, Endpoint, RemoteResource, Resource, ResponseContent, TreeEntryType, ValueValidator, INCLUDED_ENDPOINTS, }; use crate::io::api::{DatabasePersistence, EmptyField, Endpoint, RemoteResource, ResponseContent, TreeEntryType, ValueValidator, INCLUDED_ENDPOINTS}; use crate::io::config::{RunnerStatus, RunnerType}; use crate::io::database::schema::{ProgrammingLanguageRow, Table}; use crate::io::database::{Database, Operations}; Loading Loading @@ -377,6 +375,9 @@ pub struct Options { pub internal_identifier: Option<String>, /// Request body payload pub body: Option<String>, /// GitLab domain (defaults to gitlab.com) #[builder(default = String::from("gitlab.com"))] pub domain: String, } /// GitLab API response for runner details /// ### Example JSON response Loading Loading @@ -575,6 +576,7 @@ impl Options { /// - `CI_JOB_TOKEN` -> `token` (defaults to empty string when unset) /// - `CI_PROJECT_ID` -> `identifier` /// - `CI_MERGE_REQUEST_IID` -> `internal_identifier` /// - `CI_SERVER_HOST` -> `domain` (defaults to gitlab.com when unset) /// /// See <https://docs.gitlab.com/ci/variables/predefined_variables> for more information on available GitLab CI environment variables pub fn from_env() -> Self { Loading @@ -583,6 +585,7 @@ impl Options { token: var("CI_JOB_TOKEN").unwrap_or_default(), identifier: var("CI_PROJECT_ID").ok(), internal_identifier: var("CI_MERGE_REQUEST_IID").ok(), domain: var("CI_SERVER_HOST").unwrap_or_else(|_| "gitlab.com".to_string()), body: None, } } Loading @@ -600,6 +603,13 @@ impl Options { ..self } } /// Return a copy of options with GitLab domain set pub fn with_domain(self, domain: impl Into<String>) -> Self { Self { domain: domain.into(), ..self } } } impl From<ProgrammingLanguageMetadata> for ProgrammingLanguageRow { fn from(value: ProgrammingLanguageMetadata) -> Self { Loading Loading @@ -753,11 +763,12 @@ impl ValueValidator for PaginationField { } /// Get descendant groups of a group by identifier pub async fn groups(options: Options) -> ApiResult<GroupsResponse> { let name = "GitLab"; let action = "groups"; match INCLUDED_ENDPOINTS.find_by_name(name) { | Some(endpoint) => { let Options { token, identifier, .. } = options; let Options { token, identifier, domain, .. } = options; match Endpoint::from_template("gitlab").map(|e| e.with_domain(&domain)) { | Ok(endpoint) => { let params = vec![ param!(Header, "PRIVATE-TOKEN", &token), param!(TemplateValue, "identifier", &identifier.unwrap_or_default()), Loading @@ -767,7 +778,7 @@ pub async fn groups(options: Options) -> ApiResult<GroupsResponse> { let response = endpoint.invoke_with::<PaginationField, EmptyField>(action, Some(params)).await; endpoint.handle::<GroupsResponse>(response) } | None => Err(eyre!("{name} API endpoint not found")), | Err(why) => Err(why), } } /// Download programming language metadata from GitLab linguist source file Loading Loading @@ -802,16 +813,16 @@ pub async fn languages() -> ApiResult<ProgrammingLanguagesResponse> { /// /// See <https://docs.gitlab.com/api/notes/#create-a-merge-request-note> for more information on this API endpoint and required parameters pub async fn merge_request_note(options: Options) -> ApiResult<MergeRequestNoteResponse> { let name = "GitLab"; let action = "merge-request-note"; match INCLUDED_ENDPOINTS.find_by_name(name) { | Some(endpoint) => { let Options { token, identifier, internal_identifier, body, domain, } = options; match Endpoint::from_template("gitlab").map(|e| e.with_domain(&domain)) { | Ok(endpoint) => { let body = body.unwrap_or_default(); let params = vec![ param!(Header, "PRIVATE-TOKEN", &token), Loading @@ -822,16 +833,17 @@ pub async fn merge_request_note(options: Options) -> ApiResult<MergeRequestNoteR let response = endpoint.invoke_with::<PaginationField, EmptyField>(action, Some(params)).await; endpoint.handle::<MergeRequestNoteResponse>(response) } | None => Err(eyre!("{name} API endpoint not found")), | Err(why) => Err(why), } } /// Get runner details by identifier pub async fn runner(options: Options) -> ApiResult<RunnerDetails> { let name = "GitLab"; let action = "runners"; match INCLUDED_ENDPOINTS.find_by_name(name) { | Some(endpoint) => { let Options { token, identifier, .. } = options; let Options { token, identifier, domain, .. } = options; match Endpoint::from_template("gitlab").map(|e| e.with_domain(&domain)) { | Ok(endpoint) => { let params = vec![ param!(Header, "PRIVATE-TOKEN", &token), param!(TemplateValue, "identifier", &identifier.unwrap_or_default()), Loading @@ -839,38 +851,32 @@ pub async fn runner(options: Options) -> ApiResult<RunnerDetails> { let response = endpoint.invoke(action, Some(params)).await; endpoint.handle::<RunnerDetails>(response) } | None => Err(eyre!("{name} API endpoint not found")), | Err(why) => Err(why), } } /// Get runners visible to user associated with given token pub async fn runners(options: Options) -> ApiResult<RunnersResponse> { let name = "GitLab"; let action = "runners"; match INCLUDED_ENDPOINTS.find_by_name(name) { | Some(endpoint) => { let params = vec![param!(Header, "PRIVATE-TOKEN", &options.token)]; let Options { token, domain, .. } = options; match Endpoint::from_template("gitlab").map(|e| e.with_domain(&domain)) { | Ok(endpoint) => { let params = vec![param!(Header, "PRIVATE-TOKEN", &token)]; let response = endpoint.invoke_with::<PaginationField, EmptyField>(action, Some(params)).await; endpoint.handle::<RunnersResponse>(response) } | None => Err(eyre!("{name} API endpoint not found")), } | Err(why) => Err(why), } /// Construct an [`Endpoint`] for any GitLab host with the repository tree resource registered pub(crate) fn endpoint_for(host: impl Into<String>) -> Endpoint { let tree = Resource::init() .name("tree") .method("get") .template("{{ base }}/projects/{{ identifier }}/repository/tree{{ query }}") .build(); Endpoint::at(host).root("api/v4").resources(vec![tree]).build() } /// Fetch one page of repository tree blob paths for a GitLab project pub(crate) async fn tree_paths( endpoint: &Endpoint, host: impl Into<String>, identifier: impl Into<String>, directory: impl Into<String>, page: u32, ) -> ApiResult<Vec<String>> { match Endpoint::from_template("gitlab").map(|e| e.with_domain(host.into())) { | Ok(endpoint) => { let params = vec![ param!(TemplateValue, "identifier", &identifier.into()), param!(KeyValuePair, "per_page", "100"), Loading @@ -883,22 +889,15 @@ pub(crate) async fn tree_paths( .handle::<TreeResponse>(response) .map(|entries| entries.into_iter().filter(|e| e.is_blob()).map(|e| e.path()).collect()) } | Err(why) => Err(why), } } #[cfg(test)] mod tests { use super::*; use crate::io::api::IntoBody; use core::iter::FromIterator; #[test] fn test_endpoint_for_base_url() { let endpoint = endpoint_for("code.ornl.gov"); assert_eq!(endpoint.base(), "https://code.ornl.gov/api/v4"); } #[test] fn test_endpoint_for_has_tree_resource() { let endpoint = endpoint_for("code.ornl.gov"); assert!(endpoint.resources.iter().any(|r| r.name == "tree")); } #[test] fn test_add_comment_payload_uses_body_field() { let payload = vec![param!(Body, "body", "This is a test comment!!!")].into_body(); Loading
acorn-lib/src/io/api/mod.rs +20 −0 Original line number Diff line number Diff line Loading @@ -250,7 +250,27 @@ impl Endpoint { let root = root.as_ref().map_or(String::new(), |root| format!("/{root}")); format!("{scheme}://{domain}{port}{root}") } /// Create a new endpoint with a custom domain while preserving all other properties pub fn with_domain(&self, domain: impl Into<String>) -> Self { Self { domain: domain.into(), ..self.clone() } } /// Find an endpoint template by name from [`INCLUDED_ENDPOINTS`] /// /// # Example /// ```ignore /// let endpoint = Endpoint::from_template("gitlab")?.with_domain("my-gitlab.example.com"); /// ``` pub fn from_template(name: impl Into<String>) -> ApiResult<Self> { let endpoint_name = name.into(); INCLUDED_ENDPOINTS .find_by_name(&endpoint_name) .ok_or_else(|| eyre!("Endpoint template '{endpoint_name}' not found in application configuration")) } } impl Searchable<Endpoint> for Vec<Endpoint> { fn find_by_name(&self, value: impl Into<String>) -> Option<Endpoint> { let name = value.into(); Loading