gr/
api_traits.rs

1use std::{fmt::Display, str::FromStr};
2
3use serde::Deserialize;
4
5use crate::{
6    cli::browse::BrowseOptions,
7    cmds::{
8        cicd::{
9            Job, JobListBodyArgs, LintResponse, Pipeline, PipelineBodyArgs, Runner,
10            RunnerListBodyArgs, RunnerMetadata, RunnerPostDataCliArgs, RunnerRegistrationResponse,
11            YamlBytes,
12        },
13        docker::{DockerListBodyArgs, ImageMetadata, RegistryRepository, RepositoryTag},
14        gist::{Gist, GistListBodyArgs},
15        merge_request::{
16            Comment, CommentMergeRequestBodyArgs, CommentMergeRequestListBodyArgs,
17            MergeRequestBodyArgs, MergeRequestListBodyArgs, MergeRequestResponse,
18        },
19        project::{Member, Project, ProjectListBodyArgs, Tag},
20        release::{Release, ReleaseAssetListBodyArgs, ReleaseAssetMetadata, ReleaseBodyArgs},
21        trending::TrendingProject,
22        user::UserCliArgs,
23    },
24    io::CmdInfo,
25    Result,
26};
27
28pub trait MergeRequest {
29    fn open(&self, args: MergeRequestBodyArgs) -> Result<MergeRequestResponse>;
30    fn list(&self, args: MergeRequestListBodyArgs) -> Result<Vec<MergeRequestResponse>>;
31    fn merge(&self, id: i64) -> Result<MergeRequestResponse>;
32    fn get(&self, id: i64) -> Result<MergeRequestResponse>;
33    fn close(&self, id: i64) -> Result<MergeRequestResponse>;
34    fn approve(&self, id: i64) -> Result<MergeRequestResponse>;
35    /// Queries the remote API to get the number of pages available for a given
36    /// resource based on list arguments.
37    fn num_pages(&self, args: MergeRequestListBodyArgs) -> Result<Option<u32>>;
38    fn num_resources(&self, args: MergeRequestListBodyArgs) -> Result<Option<NumberDeltaErr>>;
39}
40
41pub trait RemoteProject {
42    /// Get the project data from the remote API. Implementers will need to pass
43    /// either an `id` or a `path`. The `path` should be in the format
44    /// `OWNER/PROJECT_NAME`
45    fn get_project_data(&self, id: Option<i64>, path: Option<&str>) -> Result<CmdInfo>;
46    fn get_project_members(&self) -> Result<CmdInfo>;
47    /// User requests to open a browser using the remote url. It can open the
48    /// merge/pull requests, pipeline, issues, etc.
49    fn get_url(&self, option: BrowseOptions) -> String;
50    fn list(&self, args: ProjectListBodyArgs) -> Result<Vec<Project>>;
51    fn num_pages(&self, args: ProjectListBodyArgs) -> Result<Option<u32>>;
52    fn num_resources(&self, args: ProjectListBodyArgs) -> Result<Option<NumberDeltaErr>>;
53}
54
55pub trait RemoteTag: RemoteProject {
56    fn list(&self, args: ProjectListBodyArgs) -> Result<Vec<Tag>>;
57}
58
59pub trait ProjectMember: RemoteProject {
60    fn list(&self, args: ProjectListBodyArgs) -> Result<Vec<Member>>;
61}
62
63pub trait Cicd {
64    fn list(&self, args: PipelineBodyArgs) -> Result<Vec<Pipeline>>;
65    fn get_pipeline(&self, id: i64) -> Result<Pipeline>;
66    fn num_pages(&self) -> Result<Option<u32>>;
67    fn num_resources(&self) -> Result<Option<NumberDeltaErr>>;
68    /// Lints ci/cd pipeline file contents. In gitlab this is the .gitlab-ci.yml
69    /// file. Checks that the file is valid and has no syntax errors.
70    fn lint(&self, body: YamlBytes) -> Result<LintResponse>;
71}
72
73pub trait CicdRunner {
74    fn list(&self, args: RunnerListBodyArgs) -> Result<Vec<Runner>>;
75    fn get(&self, id: i64) -> Result<RunnerMetadata>;
76    fn create(&self, args: RunnerPostDataCliArgs) -> Result<RunnerRegistrationResponse>;
77    fn num_pages(&self, args: RunnerListBodyArgs) -> Result<Option<u32>>;
78    fn num_resources(&self, args: RunnerListBodyArgs) -> Result<Option<NumberDeltaErr>>;
79}
80
81pub trait CicdJob {
82    fn list(&self, args: JobListBodyArgs) -> Result<Vec<Job>>;
83    fn num_pages(&self, args: JobListBodyArgs) -> Result<Option<u32>>;
84    fn num_resources(&self, args: JobListBodyArgs) -> Result<Option<NumberDeltaErr>>;
85}
86
87pub trait Deploy {
88    fn list(&self, args: ReleaseBodyArgs) -> Result<Vec<Release>>;
89    fn num_pages(&self) -> Result<Option<u32>>;
90    fn num_resources(&self) -> Result<Option<NumberDeltaErr>>;
91}
92
93pub trait DeployAsset {
94    fn list(&self, args: ReleaseAssetListBodyArgs) -> Result<Vec<ReleaseAssetMetadata>>;
95    fn num_pages(&self, args: ReleaseAssetListBodyArgs) -> Result<Option<u32>>;
96    fn num_resources(&self, args: ReleaseAssetListBodyArgs) -> Result<Option<NumberDeltaErr>>;
97}
98
99pub trait UserInfo {
100    /// Get the user's information from the remote API.
101    fn get_auth_user(&self) -> Result<Member>;
102    fn get(&self, args: &UserCliArgs) -> Result<Member>;
103}
104
105pub trait CodeGist {
106    fn list(&self, args: GistListBodyArgs) -> Result<Vec<Gist>>;
107    fn num_pages(&self) -> Result<Option<u32>>;
108    fn num_resources(&self) -> Result<Option<NumberDeltaErr>>;
109}
110
111pub trait Timestamp {
112    fn created_at(&self) -> String;
113}
114
115pub trait ContainerRegistry {
116    fn list_repositories(&self, args: DockerListBodyArgs) -> Result<Vec<RegistryRepository>>;
117    fn list_repository_tags(&self, args: DockerListBodyArgs) -> Result<Vec<RepositoryTag>>;
118    fn num_pages_repository_tags(&self, repository_id: i64) -> Result<Option<u32>>;
119    fn num_resources_repository_tags(&self, repository_id: i64) -> Result<Option<NumberDeltaErr>>;
120    fn num_pages_repositories(&self) -> Result<Option<u32>>;
121    fn num_resources_repositories(&self) -> Result<Option<NumberDeltaErr>>;
122    fn get_image_metadata(&self, repository_id: i64, tag: &str) -> Result<ImageMetadata>;
123}
124
125pub trait CommentMergeRequest {
126    fn create(&self, args: CommentMergeRequestBodyArgs) -> Result<()>;
127    fn list(&self, args: CommentMergeRequestListBodyArgs) -> Result<Vec<Comment>>;
128    fn num_pages(&self, args: CommentMergeRequestListBodyArgs) -> Result<Option<u32>>;
129    fn num_resources(
130        &self,
131        args: CommentMergeRequestListBodyArgs,
132    ) -> Result<Option<NumberDeltaErr>>;
133}
134
135pub trait TrendingProjectURL {
136    fn list(&self, language: String) -> Result<Vec<TrendingProject>>;
137}
138
139/// Represents a type carrying a result and a delta error. This is the case when
140/// querying the number of resources such as releases, pipelines, etc...
141/// available. REST APIs don't carry a count, so that is computed by the total
142/// number of pages available (last page in link header) and the number of
143/// resources per page.
144pub struct NumberDeltaErr {
145    /// Possible number of resources = num_pages * resources_per_page
146    pub num: u32,
147    /// Resources per_page
148    pub delta: u32,
149}
150
151impl NumberDeltaErr {
152    pub fn new(num: u32, delta: u32) -> Self {
153        Self { num, delta }
154    }
155
156    fn compute_interval(&self) -> (u32, u32) {
157        if self.num < self.delta {
158            return (1, self.delta);
159        }
160        (self.num - self.delta + 1, self.num)
161    }
162}
163
164impl Display for NumberDeltaErr {
165    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
166        let (start, end) = self.compute_interval();
167        write!(f, "({start}, {end})")
168    }
169}
170
171/// Types of API resources attached to a request. The request will carry this
172/// information so we can decide if we need to use the cache or not based on
173/// global configuration.
174/// This is for read requests only, so that would be list merge_requests, list
175/// pipelines, get one merge request information, etc...
176#[derive(Clone, Debug, PartialEq, Hash, Eq)]
177pub enum ApiOperation {
178    MergeRequest,
179    Pipeline,
180    // Project members, project data such as default upstream branch, project
181    // id, etc...any metadata related to the project.
182    Project,
183    ContainerRegistry,
184    Release,
185    // Get request to a single URL page. Ex. The trending repositories in github.com
186    SinglePage,
187    // Gists
188    Gist,
189    RepositoryTag,
190}
191
192impl Display for ApiOperation {
193    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
194        match self {
195            ApiOperation::MergeRequest => write!(f, "merge_request"),
196            ApiOperation::Pipeline => write!(f, "pipeline"),
197            ApiOperation::Project => write!(f, "project"),
198            ApiOperation::ContainerRegistry => write!(f, "container_registry"),
199            ApiOperation::Release => write!(f, "release"),
200            ApiOperation::SinglePage => write!(f, "single_page"),
201            ApiOperation::Gist => write!(f, "gist"),
202            ApiOperation::RepositoryTag => write!(f, "repository_tag"),
203        }
204    }
205}
206
207impl<'de> Deserialize<'de> for ApiOperation {
208    fn deserialize<D>(deserializer: D) -> std::result::Result<Self, D::Error>
209    where
210        D: serde::Deserializer<'de>,
211    {
212        let s = String::deserialize(deserializer)?;
213        ApiOperation::from_str(&s).map_err(serde::de::Error::custom)
214    }
215}
216
217impl FromStr for ApiOperation {
218    type Err = String;
219
220    fn from_str(s: &str) -> std::result::Result<ApiOperation, std::string::String> {
221        match s.to_lowercase().as_str() {
222            "merge_request" => Ok(ApiOperation::MergeRequest),
223            "pipeline" => Ok(ApiOperation::Pipeline),
224            "project" => Ok(ApiOperation::Project),
225            "container_registry" => Ok(ApiOperation::ContainerRegistry),
226            "release" => Ok(ApiOperation::Release),
227            "single_page" => Ok(ApiOperation::SinglePage),
228            "gist" => Ok(ApiOperation::Gist),
229            "repository_tag" => Ok(ApiOperation::RepositoryTag),
230            _ => Err(format!("Unknown ApiOperation: {s}")),
231        }
232    }
233}
234
235pub struct ApiOperationIterator {
236    current: Option<ApiOperation>,
237}
238
239impl ApiOperationIterator {
240    fn new() -> Self {
241        ApiOperationIterator { current: None }
242    }
243}
244
245impl Iterator for ApiOperationIterator {
246    type Item = ApiOperation;
247
248    fn next(&mut self) -> Option<Self::Item> {
249        let next = match self.current {
250            None => Some(ApiOperation::MergeRequest),
251            Some(ApiOperation::MergeRequest) => Some(ApiOperation::Pipeline),
252            Some(ApiOperation::Pipeline) => Some(ApiOperation::Project),
253            Some(ApiOperation::Project) => Some(ApiOperation::ContainerRegistry),
254            Some(ApiOperation::ContainerRegistry) => Some(ApiOperation::Release),
255            Some(ApiOperation::Release) => Some(ApiOperation::SinglePage),
256            Some(ApiOperation::SinglePage) => Some(ApiOperation::Gist),
257            Some(ApiOperation::Gist) => Some(ApiOperation::RepositoryTag),
258            Some(ApiOperation::RepositoryTag) => None,
259        };
260        self.current = next.clone();
261        next
262    }
263}
264
265impl ApiOperation {
266    pub fn iter() -> ApiOperationIterator {
267        ApiOperationIterator::new()
268    }
269}
270
271#[cfg(test)]
272mod tests {
273    use super::*;
274
275    #[test]
276    fn test_api_operation_display() {
277        assert_eq!(format!("{}", ApiOperation::MergeRequest), "merge_request");
278        assert_eq!(format!("{}", ApiOperation::Pipeline), "pipeline");
279        assert_eq!(format!("{}", ApiOperation::Project), "project");
280        assert_eq!(
281            format!("{}", ApiOperation::ContainerRegistry),
282            "container_registry"
283        );
284        assert_eq!(format!("{}", ApiOperation::Release), "release");
285        assert_eq!(format!("{}", ApiOperation::SinglePage), "single_page");
286    }
287
288    #[test]
289    fn test_delta_err_display() {
290        let delta_err = NumberDeltaErr::new(40, 20);
291        assert_eq!("(21, 40)", delta_err.to_string());
292    }
293
294    #[test]
295    fn test_num_less_than_delta_begins_at_one_up_to_delta() {
296        let delta_err = NumberDeltaErr::new(25, 30);
297        assert_eq!("(1, 30)", delta_err.to_string());
298    }
299
300    #[test]
301    fn test_api_operation_iterator() {
302        let operations: Vec<ApiOperation> = ApiOperation::iter().collect();
303        assert_eq!(operations.len(), 8);
304        assert_eq!(operations[0], ApiOperation::MergeRequest);
305        assert_eq!(operations[7], ApiOperation::RepositoryTag);
306    }
307}