deployment_changelog/api/
bitbucket.rs

1//! The `deployment_changelog::api::bitbucket` module provides a high-level API for interacting with
2//! the Bitbucket REST API, making it easy to retrieve information related to commits, pull requests,
3//! and issues.
4//!
5//! This module provides the `BitbucketClient` struct for interacting with the Bitbucket API, as well
6//! as various structs for representing Bitbucket objects, such as `BitbucketCommit`, `BitbucketPullRequest`,
7//! and `BitbucketPullRequestIssue`.
8//!
9//! # Examples
10//!
11//! Creating a new `BitbucketClient` with a base URL and fetching commits between two revisions:
12//!
13//! ```rust
14//! use deployment_changelog::api::bitbucket::BitbucketClient;
15//!
16//! let bitbucket_client = BitbucketClient::new("https://api.bitbucket.org")
17//!     .unwrap();
18//!
19//! let mut commits = bitbucket_client.compare_commits("MY_PROJECT", "MY_REPO", "abcdef123456", "fedcba654321");
20//!
21//! let all_commits = commits.all().await.unwrap();
22//!
23//! for commit in all_commits {
24//!     println!("{}", commit);
25//! }
26//! ```
27//!
28//! Fetching pull requests for a specific commit:
29//!
30//! ```rust
31//! use deployment_changelog::api::bitbucket::BitbucketClient;
32//!
33//! let bitbucket_client = BitbucketClient::new("https://api.bitbucket.org")
34//!     .unwrap();
35//!
36//! let mut pull_requests = bitbucket_client.get_pull_requests("MY_PROJECT", "MY_REPO", "abcdef123456");
37//!
38//! let all_pull_requests = pull_requests.all().await.unwrap();
39//!
40//! for pr in all_pull_requests {
41//!     println!("{}", pr);
42//! }
43//! ```
44//!
45//! Fetching issues associated with a pull request:
46//!
47//! ```rust
48//! use deployment_changelog::api::bitbucket::BitbucketClient;
49//!
50//! let bitbucket_client = BitbucketClient::new("https://api.bitbucket.org")
51//!     .unwrap();
52//!
53//! let issues = bitbucket_client.get_pull_request_issues("MY_PROJECT", "MY_REPO", 42).await.unwrap();
54//!
55//! for issue in issues {
56//!     println!("{}", issue);
57//! }
58//! ```
59use std::{fmt::Display, collections::HashMap, marker::PhantomData};
60
61use serde::{Deserialize, Serialize, de::DeserializeOwned};
62use serde_with::chrono::{DateTime, Local};
63use serde_with::TimestampMilliSeconds;
64use serde_with::formats::Flexible;
65use anyhow::Result;
66
67use super::rest::{RestClient, Paginated};
68
69enum BitbucketEndpoints {
70    CompareCommits,
71    PullRequestsForCommit,
72    IssuesForPullRequest
73}
74
75impl BitbucketEndpoints {
76    fn url(&self) -> &'static str {
77        match self {
78            BitbucketEndpoints::CompareCommits => "rest/api/latest/projects/{projectKey}/repos/{repositorySlug}/compare/commits?from={from}&to={to}",
79            BitbucketEndpoints::PullRequestsForCommit => "rest/api/latest/projects/{projectKey}/repos/{repositorySlug}/commits/{commitId}/pull-requests",
80            BitbucketEndpoints::IssuesForPullRequest => "/rest/jira/latest/projects/{projectKey}/repos/{repositorySlug}/pull-requests/{pullRequestId}/issues"
81        }
82    }
83}
84
85enum BitbucketOptions {
86    PageStart
87}
88
89impl BitbucketOptions {
90    fn option(&self) -> &'static str {
91        match self {
92            BitbucketOptions::PageStart => "start"
93        }
94    }
95}
96
97/// The `BitbucketPage` struct represents a single page of results returned by the Bitbucket API.
98///
99/// It is generic over the type `T` and contains a vector of values, pagination information such as the
100/// current page size, whether this is the last page, the current page start index, the result limit,
101/// and the index for the next page, if available.
102///
103/// You usually don't need to interact with `BitbucketPage` directly, as the `BitbucketPaginated`
104/// iterator takes care of the pagination for you when fetching multiple pages of results.
105///
106/// # Example
107///
108/// Suppose you are fetching commits using the `BitbucketClient::compare_commits()` method. The
109/// response from the Bitbucket API will be represented as a `BitbucketPage<BitbucketCommit>`.
110///
111/// To get the vector of `BitbucketCommit` objects from the page, you can access the `values` field:
112///
113/// ```rust
114/// use deployment_changelog::api::bitbucket::{BitbucketClient, BitbucketPage};
115///
116/// // Suppose you fetched a BitbucketPage<BitbucketCommit> named 'commit_page'
117/// let commits: Vec<BitbucketCommit> = commit_page.values;
118///
119/// for commit in commits {
120///     println!("{}", commit);
121/// }
122/// ```
123#[derive(Serialize, Deserialize, Debug)]
124#[serde(rename_all = "camelCase")]
125pub struct BitbucketPage<T> {
126    pub values: Vec<T>,
127    pub size: u32,
128    pub is_last_page: bool,
129    pub start: u32,
130    pub limit: u32,
131    pub next_page_start: Option<u32>
132}
133
134impl<T: Serialize> Display for BitbucketPage<T> {
135    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
136        match serde_json::to_string_pretty(&self) {
137            Ok(json) => write!(f, "{json}"),
138            Err(error) => panic!("Error serializing Bitbucket commit page: {error}")
139        }
140    }
141}
142
143/// The `BitbucketPaginated` struct represents an iterator for paginated results returned by the
144/// Bitbucket API.
145///
146/// It is generic over the type `T`, and is used in conjunction with [`Paginated`](crate::api::rest::Paginated) trait.
147/// It abstracts the pagination logic, allowing you to easily fetch multiple pages of results without
148/// worrying about pagination details.
149///
150/// You usually don't need to create a `BitbucketPaginated` object manually, as the methods from `BitbucketClient`
151/// will return a `BitbucketPaginated` instance when necessary.
152///
153/// # Example
154///
155/// Suppose you want to fetch all commits between two commit hashes using the `BitbucketClient::compare_commits()` method.
156/// It returns a `BitbucketPaginated<BitbucketCommit>` iterator, which you can use to fetch all pages of results:
157///
158/// ```rust
159/// use deployment_changelog::api::bitbucket::{BitbucketClient, BitbucketPaginated};
160/// use deployment_changelog::api::rest::Paginated;
161///
162/// // Suppose you have a BitbucketClient named 'client'
163/// let project_key = "PROJECT";
164/// let repo_slug = "my-repo";
165/// let start_commit = "abcdef";
166/// let end_commit = "123456";
167///
168/// let mut commits_iter = client.compare_commits(project_key, repo_slug, start_commit, end_commit);
169/// let all_commits = commits_iter.all().await.unwrap();
170///
171/// for commit in all_commits {
172///     println!("{}", commit);
173/// }
174/// ```
175pub struct BitbucketPaginated<'a, T> {
176    client: &'a BitbucketClient,
177    url: String,
178    query: HashMap<String, String>,
179    next_page_start: Option<u32>,
180    is_last_page: bool,
181    phantom: PhantomData<T>
182}
183
184impl<'a, T> BitbucketPaginated<'a, T> {
185    /// Creates a new `BitbucketPaginated` instance with the specified client, URL, and query options.
186    ///
187    /// # Example
188    ///
189    /// ```
190    /// use deployment_changelog::api::bitbucket::{BitbucketClient, BitbucketPaginated};
191    ///
192    /// let bitbucket_base_url = "https://your-bitbucket-instance.com/";
193    /// let client = BitbucketClient::new(bitbucket_base_url).unwrap();
194    /// let url = "some/endpoint";
195    /// let paginated = BitbucketPaginated::new(&client, url.to_string(), None);
196    /// ```
197    fn new(client: &'a BitbucketClient, url: String, query: Option<&HashMap<String, String>>) -> Self {
198        let query_options = match query {
199            Some(query_opts) => query_opts.clone(),
200            None => HashMap::with_capacity(1)
201        };
202
203        BitbucketPaginated {
204            client,
205            url,
206            query: query_options,
207            next_page_start: Some(0),
208            is_last_page: false,
209            phantom: PhantomData
210        }
211    }
212}
213
214#[async_trait::async_trait]
215impl<T: DeserializeOwned + Send> Paginated<T> for BitbucketPaginated<'_, T> {
216    /// Fetches the next page of items of type `T` from the API and returns them as a vector.
217    ///
218    /// # Example
219    ///
220    /// ```
221    /// use deployment_changelog::api::bitbucket::{BitbucketClient, BitbucketCommit, BitbucketPaginated};
222    ///
223    /// async fn fetch_next_page_of_commits() {
224    ///     let bitbucket_base_url = "https://your-bitbucket-instance.com/";
225    ///     let client = BitbucketClient::new(bitbucket_base_url).unwrap();
226    ///     let url = "some/endpoint";
227    ///     let mut paginated = BitbucketPaginated::<BitbucketCommit>::new(&client, url.to_string(), None);
228    ///
229    ///     let commits = paginated.next().await.unwrap();
230    ///     println!("Fetched {} commits", commits.len());
231    /// }
232    /// ```
233    async fn next(&mut self) -> Result<Vec<T>> {
234        if let Some(next_page_start) = self.next_page_start {
235            self.query.insert(
236                BitbucketOptions::PageStart.option().to_string(),
237                next_page_start.to_string()
238            );
239        };
240
241        let page = self.client.client.get::<BitbucketPage<T>>(&self.url, Some(&self.query)).await?;
242
243        self.next_page_start = page.next_page_start;
244        self.is_last_page = page.is_last_page;
245
246        Ok(page.values)
247    }
248
249    /// Returns whether the last page of items has been fetched.
250    ///
251    /// # Example
252    ///
253    /// ```
254    /// use deployment_changelog::api::bitbucket::{BitbucketClient, BitbucketCommit, BitbucketPaginated};
255    ///
256    /// async fn iterate_over_all_commits() {
257    ///     let bitbucket_base_url = "https://your-bitbucket-instance.com/";
258    ///     let client = BitbucketClient::new(bitbucket_base_url).unwrap();
259    ///     let url = "some/endpoint";
260    ///     let mut paginated = BitbucketPaginated::<BitbucketCommit>::new(&client, url.to_string(), None);
261    ///
262    ///     while !paginated.is_last() {
263    ///         let commits = paginated.next().await.unwrap();
264    ///         println!("Fetched {} commits", commits.len());
265    ///     }
266    /// }
267    /// ```
268    fn is_last(&self) -> bool {
269        self.is_last_page
270    }
271}
272
273/// The `BitbucketCommit` struct represents a single commit returned by the Bitbucket API.
274///
275/// It contains information about the commit, such as its ID, display ID, author, committer, and message.
276///
277/// This struct is usually used as a result of API calls made through the `BitbucketClient`.
278///
279/// # Example
280///
281/// Suppose you want to fetch all commits between two commit hashes using the `BitbucketClient::compare_commits()` method.
282/// You'll receive a `BitbucketPaginated<BitbucketCommit>` iterator, which you can use to fetch all pages of commits:
283///
284/// ```rust
285/// use deployment_changelog::api::bitbucket::{BitbucketClient, BitbucketPaginated};
286/// use deployment_changelog::api::rest::Paginated;
287///
288/// // Suppose you have a BitbucketClient named 'client'
289/// let project_key = "PROJECT";
290/// let repo_slug = "my-repo";
291/// let start_commit = "abcdef";
292/// let end_commit = "123456";
293///
294/// let mut commits_iter = client.compare_commits(project_key, repo_slug, start_commit, end_commit);
295/// let all_commits = commits_iter.all().await.unwrap();
296///
297/// for commit in all_commits {
298///     println!("Commit ID: {}", commit.id);
299///     println!("Author: {}", commit.author.display_name);
300///     println!("Message: {}", commit.message);
301/// }
302/// ```
303#[derive(Serialize, Deserialize, Debug, PartialEq, Eq, Hash)]
304#[serde(rename_all = "camelCase")]
305pub struct BitbucketCommit {
306    pub id: String,
307    pub display_id: String,
308    pub author: BitbucketAuthor,
309    pub committer: BitbucketAuthor,
310    pub message: String
311}
312
313impl Display for BitbucketCommit {
314    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
315        match serde_json::to_string_pretty(&self) {
316            Ok(json) => write!(f, "{json}"),
317            Err(error) => panic!("Error serializing Bitbucket commit: {error}")
318        }
319    }
320}
321
322/// The `BitbucketAuthor` struct represents an author or committer of a commit returned by the Bitbucket API.
323///
324/// It contains information about the author, such as their name, email address, and display name.
325///
326/// This struct is usually used as a part of the `BitbucketCommit` struct when working with the `BitbucketClient`.
327///
328/// # Example
329///
330/// Suppose you want to fetch all commits between two commit hashes using the `BitbucketClient::compare_commits()` method.
331/// You'll receive a `BitbucketPaginated<BitbucketCommit>` iterator, which you can use to fetch all pages of commits:
332///
333/// ```rust
334/// use deployment_changelog::api::bitbucket::{BitbucketClient, BitbucketPaginated};
335/// use deployment_changelog::api::rest::Paginated;
336///
337/// // Suppose you have a BitbucketClient named 'client'
338/// let project_key = "PROJECT";
339/// let repo_slug = "my-repo";
340/// let start_commit = "abcdef";
341/// let end_commit = "123456";
342///
343/// let mut commits_iter = client.compare_commits(project_key, repo_slug, start_commit, end_commit);
344/// let all_commits = commits_iter.all().await.unwrap();
345///
346/// for commit in all_commits {
347///     let author = &commit.author;
348///     println!("Author name: {}", author.name);
349///     println!("Author email: {}", author.email_address);
350///     println!("Author display name: {}", author.display_name);
351/// }
352/// ```
353#[derive(Serialize, Deserialize, Debug, PartialEq, Eq, Hash)]
354#[serde(rename_all = "camelCase")]
355pub struct BitbucketAuthor {
356    pub name: String,
357    pub email_address: String,
358    pub display_name: String
359}
360
361impl Display for BitbucketAuthor {
362    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
363        match serde_json::to_string_pretty(&self) {
364            Ok(json) => write!(f, "{json}"),
365            Err(error) => panic!("Error serializing Bitbucket author: {error}")
366        }
367    }
368}
369
370/// The `BitbucketPullRequest` struct represents a pull request returned by the Bitbucket API.
371///
372/// It contains information about the pull request, such as the ID, title, description, open status, author, and creation and update dates.
373///
374/// This struct is usually used when working with the `BitbucketClient` to fetch pull requests associated with a commit.
375///
376/// # Example
377///
378/// Suppose you want to fetch all pull requests associated with a commit hash using the `BitbucketClient::get_pull_requests()` method.
379/// You'll receive a `BitbucketPaginated<BitbucketPullRequest>` iterator, which you can use to fetch all pages of pull requests:
380///
381/// ```rust
382/// use deployment_changelog::api::bitbucket::{BitbucketClient, BitbucketPaginated};
383/// use deployment_changelog::api::rest::Paginated;
384///
385/// // Suppose you have a BitbucketClient named 'client'
386/// let project_key = "PROJECT";
387/// let repo_slug = "my-repo";
388/// let commit_hash = "abcdef";
389///
390/// let mut pr_iter = client.get_pull_requests(project_key, repo_slug, commit_hash);
391/// let all_pull_requests = pr_iter.all().await.unwrap();
392///
393/// for pr in all_pull_requests {
394///     println!("Pull request ID: {}", pr.id);
395///     println!("Title: {}", pr.title);
396///     println!("Description: {}", pr.description);
397///     println!("Open: {}", pr.open);
398///     println!("Created: {}", pr.created_date);
399///     println!("Updated: {}", pr.updated_date);
400/// }
401/// ```
402#[serde_with::serde_as]
403#[derive(Serialize, Deserialize, Debug, PartialEq, Eq, Hash)]
404#[serde(rename_all = "camelCase")]
405pub struct BitbucketPullRequest {
406    pub id: u64,
407    pub title: String,
408    pub description: String,
409    pub open: bool,
410    pub author: BitbucketPullRequestAuthor,
411
412    #[serde_as(as = "TimestampMilliSeconds<String, Flexible>")]
413    pub created_date: DateTime<Local>,
414
415    #[serde_as(as = "TimestampMilliSeconds<String, Flexible>")]
416    pub updated_date: DateTime<Local>
417}
418
419impl Display for BitbucketPullRequest {
420    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
421        match serde_json::to_string_pretty(&self) {
422            Ok(json) => write!(f, "{json}"),
423            Err(error) => panic!("Error serializing Bitbucket pull request: {error}")
424        }
425    }
426}
427
428/// The `BitbucketPullRequestAuthor` struct represents the author of a pull request returned by the Bitbucket API.
429///
430/// It contains information about the author, such as the user and whether the pull request has been approved by the author.
431///
432/// This struct is usually used as part of the `BitbucketPullRequest` struct when working with the `BitbucketClient` to fetch pull requests associated with a commit.
433///
434/// # Example
435///
436/// Suppose you want to fetch all pull requests associated with a commit hash using the `BitbucketClient::get_pull_requests()` method.
437/// You'll receive a `BitbucketPaginated<BitbucketPullRequest>` iterator, which you can use to fetch all pages of pull requests:
438///
439/// ```rust
440/// use deployment_changelog::api::bitbucket::{BitbucketClient, BitbucketPaginated};
441/// use deployment_changelog::api::rest::Paginated;
442///
443/// // Suppose you have a BitbucketClient named 'client'
444/// let project_key = "PROJECT";
445/// let repo_slug = "my-repo";
446/// let commit_hash = "abcdef";
447///
448/// let mut pr_iter = client.get_pull_requests(project_key, repo_slug, commit_hash);
449/// let all_pull_requests = pr_iter.all().await.unwrap();
450///
451/// for pr in all_pull_requests {
452///     println!("Author display name: {}", pr.author.user.display_name);
453///     println!("Author email: {}", pr.author.user.email_address);
454///     println!("Author approval status: {}", pr.author.approved);
455/// }
456/// ```
457#[derive(Serialize, Deserialize, Debug, PartialEq, Eq, Hash)]
458#[serde(rename_all = "camelCase")]
459pub struct BitbucketPullRequestAuthor {
460    pub user: BitbucketAuthor,
461    pub approved: bool
462}
463
464impl Display for BitbucketPullRequestAuthor {
465    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
466        match serde_json::to_string_pretty(&self) {
467            Ok(json) => write!(f, "{json}"),
468            Err(error) => panic!("Error serializing Bitbucket pull request author: {error}")
469        }
470    }
471}
472
473/// The `BitbucketPullRequestIssue` struct represents an issue associated with a pull request returned by the Bitbucket API.
474///
475/// It contains information about the issue, such as the key and URL of the issue.
476///
477/// This struct is usually used when working with the `BitbucketClient` to fetch issues associated with a specific pull request.
478///
479/// # Example
480///
481/// Suppose you want to fetch all issues associated with a pull request using the `BitbucketClient::get_pull_request_issues()` method.
482/// You'll receive a `Result<Vec<BitbucketPullRequestIssue>>`, which you can use to access and process the associated issues:
483///
484/// ```rust
485/// use deployment_changelog::api::bitbucket::BitbucketClient;
486///
487/// // Suppose you have a BitbucketClient named 'client'
488/// let project_key = "PROJECT";
489/// let repo_slug = "my-repo";
490/// let pull_request_id = 42;
491///
492/// let issues_result = client.get_pull_request_issues(project_key, repo_slug, pull_request_id).await;
493///
494/// match issues_result {
495///     Ok(issues) => {
496///         for issue in issues {
497///             println!("Issue key: {}", issue.key);
498///             println!("Issue URL: {}", issue.url);
499///         }
500///     },
501///     Err(error) => {
502///         println!("Error fetching pull request issues: {:?}", error);
503///     }
504/// }
505/// ```
506#[derive(Serialize, Deserialize, Debug, PartialEq, Eq, Hash)]
507#[serde(rename_all = "camelCase")]
508pub struct BitbucketPullRequestIssue {
509    pub key: String,
510    pub url: String
511}
512
513impl Display for BitbucketPullRequestIssue {
514    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
515        match serde_json::to_string_pretty(&self) {
516            Ok(json) => write!(f, "{json}"),
517            Err(error) => panic!("Error serializing Bitbucket pull request issue: {error}")
518        }
519    }
520}
521
522/// The `BitbucketClient` struct is a high-level API client for working with the Bitbucket API.
523///
524/// It provides methods for common operations like comparing commits, fetching pull requests for a commit, and getting issues associated with a pull request.
525///
526/// Internally, it uses the `RestClient` struct for making API calls.
527///
528/// # Example
529///
530/// To create a new `BitbucketClient`, you can use the `new()` method and pass the base URL of your Bitbucket instance:
531///
532/// ```rust
533/// use deployment_changelog::api::bitbucket::BitbucketClient;
534///
535/// let base_url = "https://bitbucket.example.com";
536/// let client = BitbucketClient::new(base_url).unwrap();
537/// ```
538///
539/// Once you have a `BitbucketClient`, you can use it to interact with the Bitbucket API:
540///
541/// ```rust
542/// use deployment_changelog::api::bitbucket::{BitbucketClient, BitbucketCommit};
543///
544/// // Suppose you have a BitbucketClient named 'client'
545/// let project_key = "PROJECT";
546/// let repo_slug = "my-repo";
547/// let start_commit = "abcdef";
548/// let end_commit = "ghijkl";
549///
550/// let mut commits_paginated = client.compare_commits(project_key, repo_slug, start_commit, end_commit);
551///
552/// while let Some(commits_result) = commits_paginated.next().await {
553///     match commits_result {
554///         Ok(commits) => {
555///             for commit in commits {
556///                 println!("Commit ID: {}", commit.id);
557///                 println!("Commit message: {}", commit.message);
558///             }
559///         },
560///         Err(error) => {
561///             println!("Error fetching commits: {:?}", error);
562///         }
563///     }
564/// }
565/// ```
566/// BitbucketClient is a struct that provides methods for interacting with the Bitbucket API.
567///
568/// It wraps the RestClient struct and exposes methods for fetching commits, pull requests,
569/// and related issues.
570///
571/// # Example
572///
573/// ```
574/// let client = BitbucketClient::new("https://api.bitbucket.com").unwrap();
575/// ```
576#[derive(Debug)]
577pub struct BitbucketClient {
578    client: RestClient
579}
580
581impl BitbucketClient {
582    /// Creates a new BitbucketClient instance given the base URL.
583    ///
584    /// # Arguments
585    ///
586    /// * `base_url` - The base URL of the Bitbucket API.
587    ///
588    /// # Returns
589    ///
590    /// A Result containing a BitbucketClient instance or an error if the provided base URL is invalid.
591    pub fn new(base_url: &str) -> Result<Self> {
592        Ok(Self {
593            client: RestClient::new(base_url)?
594        })
595    }
596
597    /// Constructs a BitbucketClient instance from a pre-initialized RestClient.
598    ///
599    /// # Arguments
600    ///
601    /// * `client` - An instance of RestClient.
602    pub fn from_client(client: RestClient) -> Self {
603        Self {
604            client
605        }
606    }
607
608    /// Returns a `BitbucketPaginated<BitbucketCommit>` instance for fetching commits between
609    /// two commit IDs (start_commit and end_commit) in a specified Bitbucket project and repository.
610    ///
611    /// # Arguments
612    ///
613    /// * `project` - The project key in Bitbucket.
614    /// * `repo` - The repository slug in Bitbucket.
615    /// * `start_commit` - The commit ID to start the comparison from.
616    /// * `end_commit` - The commit ID to end the comparison at.
617    ///
618    /// # Returns
619    ///
620    /// A `BitbucketPaginated<BitbucketCommit>` instance.
621    pub fn compare_commits(&self, project: &str, repo: &str, start_commit: &str, end_commit: &str) -> BitbucketPaginated<BitbucketCommit> {
622        let compare_commits_path: String = BitbucketEndpoints::CompareCommits.url()
623            .replace("{projectKey}", project)
624            .replace("{repositorySlug}", repo)
625            .replace("{from}", start_commit)
626            .replace("{to}", end_commit);
627
628        BitbucketPaginated::new(&self, compare_commits_path, None)
629    }
630
631    /// Returns a `BitbucketPaginated<BitbucketPullRequest>` instance for fetching pull requests
632    /// associated with a specific commit in a Bitbucket project and repository.
633    ///
634    /// # Arguments
635    ///
636    /// * `project` - The project key in Bitbucket.
637    /// * `repo` - The repository slug in Bitbucket.
638    /// * `commit` - The commit ID to fetch the pull requests for.
639    ///
640    /// # Returns
641    ///
642    /// A `BitbucketPaginated<BitbucketPullRequest>` instance.
643    pub fn get_pull_requests(&self, project: &str, repo: &str, commit: &str) -> BitbucketPaginated<BitbucketPullRequest> {
644        let get_pull_requests_path: String = BitbucketEndpoints::PullRequestsForCommit.url()
645            .replace("{projectKey}", project)
646            .replace("{repositorySlug}", repo)
647            .replace("{commitId}", commit);
648
649        BitbucketPaginated::new(&self, get_pull_requests_path, None)
650    }
651
652    /// Fetches issues associated with a specific pull request in a Bitbucket project and repository.
653    ///
654    /// # Arguments
655    ///
656    /// * `project` - The project key in Bitbucket.
657    /// * `repo` - The repository slug in Bitbucket.
658    /// * `pull_request_id` - The ID of the pull request to fetch the issues for.
659    ///
660    /// # Returns
661    ///
662    /// A Result containing a Vec of BitbucketPullRequestIssue instances or an error if the request fails.
663    pub async fn get_pull_request_issues(&self, project: &str, repo: &str, pull_request_id: u64) -> Result<Vec<BitbucketPullRequestIssue>> {
664        let get_pull_request_issues_path: String = BitbucketEndpoints::IssuesForPullRequest.url()
665            .replace("{projectKey}", project)
666            .replace("{repositorySlug}", repo)
667            .replace("{pullRequestId}", &pull_request_id.to_string());
668
669        self.client.get::<Vec<BitbucketPullRequestIssue>>(&get_pull_request_issues_path, None).await
670    }
671}