torrust_tracker/servers/http/v1/requests/
scrape.rs1use std::panic::Location;
5
6use thiserror::Error;
7use torrust_tracker_located_error::{Located, LocatedError};
8use torrust_tracker_primitives::info_hash::{self, InfoHash};
9
10use crate::servers::http::percent_encoding::percent_decode_info_hash;
11use crate::servers::http::v1::query::Query;
12use crate::servers::http::v1::responses;
13
14const INFO_HASH: &str = "info_hash";
16
17#[derive(Debug, PartialEq)]
18pub struct Scrape {
19 pub info_hashes: Vec<InfoHash>,
20}
21
22#[derive(Error, Debug)]
23pub enum ParseScrapeQueryError {
24 #[error("missing query params for scrape request in {location}")]
25 MissingParams { location: &'static Location<'static> },
26 #[error("missing param {param_name} in {location}")]
27 MissingParam {
28 location: &'static Location<'static>,
29 param_name: String,
30 },
31 #[error("invalid param value {param_value} for {param_name} in {source}")]
32 InvalidInfoHashParam {
33 param_name: String,
34 param_value: String,
35 source: LocatedError<'static, info_hash::ConversionError>,
36 },
37}
38
39impl From<ParseScrapeQueryError> for responses::error::Error {
40 fn from(err: ParseScrapeQueryError) -> Self {
41 responses::error::Error {
42 failure_reason: format!("Cannot parse query params for scrape request: {err}"),
43 }
44 }
45}
46
47impl TryFrom<Query> for Scrape {
48 type Error = ParseScrapeQueryError;
49
50 fn try_from(query: Query) -> Result<Self, Self::Error> {
51 Ok(Self {
52 info_hashes: extract_info_hashes(&query)?,
53 })
54 }
55}
56
57fn extract_info_hashes(query: &Query) -> Result<Vec<InfoHash>, ParseScrapeQueryError> {
58 match query.get_param_vec(INFO_HASH) {
59 Some(raw_params) => {
60 let mut info_hashes = vec![];
61
62 for raw_param in raw_params {
63 let info_hash =
64 percent_decode_info_hash(&raw_param).map_err(|err| ParseScrapeQueryError::InvalidInfoHashParam {
65 param_name: INFO_HASH.to_owned(),
66 param_value: raw_param.clone(),
67 source: Located(err).into(),
68 })?;
69
70 info_hashes.push(info_hash);
71 }
72
73 Ok(info_hashes)
74 }
75 None => Err(ParseScrapeQueryError::MissingParam {
76 location: Location::caller(),
77 param_name: INFO_HASH.to_owned(),
78 }),
79 }
80}
81
82#[cfg(test)]
83mod tests {
84
85 mod scrape_request {
86
87 use torrust_tracker_primitives::info_hash::InfoHash;
88
89 use crate::servers::http::v1::query::Query;
90 use crate::servers::http::v1::requests::scrape::{Scrape, INFO_HASH};
91
92 #[test]
93 fn should_be_instantiated_from_the_url_query_with_only_one_infohash() {
94 let raw_query = Query::from(vec![(INFO_HASH, "%3B%24U%04%CF%5F%11%BB%DB%E1%20%1C%EAjk%F4Z%EE%1B%C0")]).to_string();
95
96 let query = raw_query.parse::<Query>().unwrap();
97
98 let scrape_request = Scrape::try_from(query).unwrap();
99
100 assert_eq!(
101 scrape_request,
102 Scrape {
103 info_hashes: vec!["3b245504cf5f11bbdbe1201cea6a6bf45aee1bc0".parse::<InfoHash>().unwrap()],
104 }
105 );
106 }
107
108 mod when_it_is_instantiated_from_the_url_query_params {
109
110 use crate::servers::http::v1::query::Query;
111 use crate::servers::http::v1::requests::scrape::{Scrape, INFO_HASH};
112
113 #[test]
114 fn it_should_fail_if_the_query_does_not_include_the_info_hash_param() {
115 let raw_query_without_info_hash = "another_param=NOT_RELEVANT";
116
117 assert!(Scrape::try_from(raw_query_without_info_hash.parse::<Query>().unwrap()).is_err());
118 }
119
120 #[test]
121 fn it_should_fail_if_the_info_hash_param_is_invalid() {
122 let raw_query = Query::from(vec![(INFO_HASH, "INVALID_INFO_HASH_VALUE")]).to_string();
123
124 assert!(Scrape::try_from(raw_query.parse::<Query>().unwrap()).is_err());
125 }
126 }
127 }
128}