Skip to content

Commit f508ef7

Browse files
committed
Merge #1179: Extract new package: Tracker API client
aa7ffdf refactor: [#1159] API client. Extract Origin type (Jose Celano) e4b9875 refactor: [#1159] use new tracker api client package in tests (Jose Celano) a1ded65 feat: [#1159] extract new package tracker api client (Jose Celano) Pull request description: Extract new package: Tracker API client. I've also extracted a new type `Origin` for the API base URL in the connection info. For example: `htpp://tracker.com:1212/`. ACKs for top commit: josecelano: ACK aa7ffdf Tree-SHA512: a079411fe0928d719f61a827e87ee7a323dc4609031dd3872a6d308df81c8c2cc01b951f92288bb670a3a75977c153679d73f81aa2e4802c82909e97bb20bb63
2 parents f6aca40 + aa7ffdf commit f508ef7

21 files changed

+358
-66
lines changed

.github/workflows/deployment.yaml

+1
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,7 @@ jobs:
5858
cargo publish -p bittorrent-http-protocol
5959
cargo publish -p bittorrent-tracker-client
6060
cargo publish -p torrust-tracker
61+
cargo publish -p torrust-tracker-api-client
6162
cargo publish -p torrust-tracker-client
6263
cargo publish -p torrust-tracker-clock
6364
cargo publish -p torrust-tracker-configuration

Cargo.lock

+13
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

+3-1
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ rust-version.workspace = true
1616
version.workspace = true
1717

1818
[lib]
19-
name = "torrust_tracker_lib"
19+
name = "torrust_tracker_lib"
2020

2121
[workspace.package]
2222
authors = ["Nautilus Cyberneering <info@nautilus-cyberneering.de>, Mick van Dijke <mick@dutchbits.nl>"]
@@ -97,6 +97,7 @@ ignored = ["crossbeam-skiplist", "dashmap", "figment", "parking_lot", "serde_byt
9797
[dev-dependencies]
9898
local-ip-address = "0"
9999
mockall = "0"
100+
torrust-tracker-api-client = { version = "3.0.0-develop", path = "packages/tracker-api-client" }
100101
torrust-tracker-test-helpers = { version = "3.0.0-develop", path = "packages/test-helpers" }
101102

102103
[workspace]
@@ -108,6 +109,7 @@ members = [
108109
"packages/primitives",
109110
"packages/test-helpers",
110111
"packages/torrent-repository",
112+
"packages/tracker-api-client",
111113
"packages/tracker-client",
112114
]
113115

+23
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
[package]
2+
description = "A library to interact with the Torrust Tracker REST API."
3+
keywords = ["bittorrent", "client", "tracker"]
4+
license = "LGPL-3.0"
5+
name = "torrust-tracker-api-client"
6+
readme = "README.md"
7+
8+
authors.workspace = true
9+
documentation.workspace = true
10+
edition.workspace = true
11+
homepage.workspace = true
12+
publish.workspace = true
13+
repository.workspace = true
14+
rust-version.workspace = true
15+
version.workspace = true
16+
17+
[dependencies]
18+
hyper = "1"
19+
reqwest = { version = "0", features = ["json"] }
20+
serde = { version = "1", features = ["derive"] }
21+
thiserror = "2"
22+
url = { version = "2", features = ["serde"] }
23+
uuid = { version = "1", features = ["v4"] }

packages/tracker-api-client/README.md

+23
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
# Torrust Tracker API Client
2+
3+
A library to interact with the Torrust Tracker REST API.
4+
5+
## License
6+
7+
**Copyright (c) 2024 The Torrust Developers.**
8+
9+
This program is free software: you can redistribute it and/or modify it under the terms of the [GNU Lesser General Public License][LGPL_3_0] as published by the [Free Software Foundation][FSF], version 3.
10+
11+
This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the [GNU Lesser General Public License][LGPL_3_0] for more details.
12+
13+
You should have received a copy of the *GNU Lesser General Public License* along with this program. If not, see <https://www.gnu.org/licenses/>.
14+
15+
Some files include explicit copyright notices and/or license notices.
16+
17+
### Legacy Exception
18+
19+
For prosperity, versions of Torrust BitTorrent Tracker Client that are older than five years are automatically granted the [MIT-0][MIT_0] license in addition to the existing [LGPL-3.0-only][LGPL_3_0] license.
20+
21+
[LGPL_3_0]: ./LICENSE
22+
[MIT_0]: ./docs/licenses/LICENSE-MIT_0
23+
[FSF]: https://www.fsf.org/
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
MIT No Attribution
2+
3+
Permission is hereby granted, free of charge, to any person obtaining a copy of this
4+
software and associated documentation files (the "Software"), to deal in the Software
5+
without restriction, including without limitation the rights to use, copy, modify,
6+
merge, publish, distribute, sublicense, and/or sell copies of the Software, and to
7+
permit persons to whom the Software is furnished to do so.
8+
9+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
10+
INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
11+
PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
12+
HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
13+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
14+
SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
pub type ReqwestQuery = Vec<ReqwestQueryParam>;
2+
pub type ReqwestQueryParam = (String, String);
3+
4+
/// URL Query component
5+
#[derive(Default, Debug)]
6+
pub struct Query {
7+
params: Vec<QueryParam>,
8+
}
9+
10+
impl Query {
11+
#[must_use]
12+
pub fn empty() -> Self {
13+
Self { params: vec![] }
14+
}
15+
16+
#[must_use]
17+
pub fn params(params: Vec<QueryParam>) -> Self {
18+
Self { params }
19+
}
20+
21+
pub fn add_param(&mut self, param: QueryParam) {
22+
self.params.push(param);
23+
}
24+
}
25+
26+
impl From<Query> for ReqwestQuery {
27+
fn from(url_search_params: Query) -> Self {
28+
url_search_params
29+
.params
30+
.iter()
31+
.map(|param| ReqwestQueryParam::from((*param).clone()))
32+
.collect()
33+
}
34+
}
35+
36+
/// URL query param
37+
#[derive(Clone, Debug)]
38+
pub struct QueryParam {
39+
name: String,
40+
value: String,
41+
}
42+
43+
impl QueryParam {
44+
#[must_use]
45+
pub fn new(name: &str, value: &str) -> Self {
46+
Self {
47+
name: name.to_string(),
48+
value: value.to_string(),
49+
}
50+
}
51+
}
52+
53+
impl From<QueryParam> for ReqwestQueryParam {
54+
fn from(param: QueryParam) -> Self {
55+
(param.name, param.value)
56+
}
57+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
pub mod http;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,154 @@
1+
use std::str::FromStr;
2+
3+
use thiserror::Error;
4+
use url::Url;
5+
6+
#[derive(Clone)]
7+
pub struct ConnectionInfo {
8+
pub origin: Origin,
9+
pub api_token: Option<String>,
10+
}
11+
12+
impl ConnectionInfo {
13+
#[must_use]
14+
pub fn authenticated(origin: Origin, api_token: &str) -> Self {
15+
Self {
16+
origin,
17+
api_token: Some(api_token.to_string()),
18+
}
19+
}
20+
21+
#[must_use]
22+
pub fn anonymous(origin: Origin) -> Self {
23+
Self { origin, api_token: None }
24+
}
25+
}
26+
27+
/// Represents the origin of a HTTP request.
28+
///
29+
/// The format of the origin is a URL, but only the scheme, host, and port are used.
30+
///
31+
/// Pattern: `scheme://host:port/`
32+
#[derive(Debug, Clone)]
33+
pub struct Origin {
34+
url: Url,
35+
}
36+
37+
#[derive(Debug, Error)]
38+
pub enum OriginError {
39+
#[error("Invalid URL: {0}")]
40+
InvalidUrl(#[from] url::ParseError),
41+
42+
#[error("URL is missing scheme or host")]
43+
InvalidOrigin,
44+
45+
#[error("Invalid URL scheme, only http and https are supported")]
46+
InvalidScheme,
47+
}
48+
49+
impl FromStr for Origin {
50+
type Err = OriginError;
51+
52+
fn from_str(s: &str) -> Result<Self, Self::Err> {
53+
let mut url = Url::parse(s).map_err(OriginError::InvalidUrl)?;
54+
55+
// Ensure the URL has a scheme and host
56+
if url.scheme().is_empty() || url.host().is_none() {
57+
return Err(OriginError::InvalidOrigin);
58+
}
59+
60+
if url.scheme() != "http" && url.scheme() != "https" {
61+
return Err(OriginError::InvalidScheme);
62+
}
63+
64+
// Retain only the origin components
65+
url.set_path("/");
66+
url.set_query(None);
67+
url.set_fragment(None);
68+
69+
Ok(Origin { url })
70+
}
71+
}
72+
73+
impl std::fmt::Display for Origin {
74+
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
75+
write!(f, "{}", self.url)
76+
}
77+
}
78+
79+
impl Origin {
80+
/// # Errors
81+
///
82+
/// Will return an error if the string is not a valid URL containing a
83+
/// scheme and host.
84+
pub fn new(s: &str) -> Result<Self, OriginError> {
85+
s.parse()
86+
}
87+
88+
#[must_use]
89+
pub fn url(&self) -> &Url {
90+
&self.url
91+
}
92+
}
93+
94+
#[cfg(test)]
95+
mod tests {
96+
mod origin {
97+
use crate::connection_info::Origin;
98+
99+
#[test]
100+
fn should_be_parsed_from_a_string_representing_a_url() {
101+
let origin = Origin::new("https://example.com:8080/path?query#fragment").unwrap();
102+
103+
assert_eq!(origin.to_string(), "https://example.com:8080/");
104+
}
105+
106+
mod when_parsing_from_url_string {
107+
use crate::connection_info::Origin;
108+
109+
#[test]
110+
fn should_ignore_default_ports() {
111+
let origin = Origin::new("http://example.com:80").unwrap(); // DevSkim: ignore DS137138
112+
assert_eq!(origin.to_string(), "http://example.com/"); // DevSkim: ignore DS137138
113+
114+
let origin = Origin::new("https://example.com:443").unwrap();
115+
assert_eq!(origin.to_string(), "https://example.com/");
116+
}
117+
118+
#[test]
119+
fn should_add_the_slash_after_the_host() {
120+
let origin = Origin::new("https://example.com:1212").unwrap();
121+
122+
assert_eq!(origin.to_string(), "https://example.com:1212/");
123+
}
124+
125+
#[test]
126+
fn should_remove_extra_path_and_query_parameters() {
127+
let origin = Origin::new("https://example.com:1212/path/to/resource?query=1#fragment").unwrap();
128+
129+
assert_eq!(origin.to_string(), "https://example.com:1212/");
130+
}
131+
132+
#[test]
133+
fn should_fail_when_the_scheme_is_missing() {
134+
let result = Origin::new("example.com");
135+
136+
assert!(result.is_err());
137+
}
138+
139+
#[test]
140+
fn should_fail_when_the_scheme_is_not_supported() {
141+
let result = Origin::new("udp://example.com");
142+
143+
assert!(result.is_err());
144+
}
145+
146+
#[test]
147+
fn should_fail_when_the_host_is_missing() {
148+
let result = Origin::new("http://");
149+
150+
assert!(result.is_err());
151+
}
152+
}
153+
}
154+
}
+3
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
pub mod common;
2+
pub mod connection_info;
3+
pub mod v1;

0 commit comments

Comments
 (0)