use anyhow::{Context, Result}; use reqwest::{Client, StatusCode, Url}; use serde::Deserialize; use crate::domain::{CardInfoRequest, OriginalCardName, OriginalCardNameFetcher}; pub struct ScryfallOriginalCardNameFetcher { client: Client, url: Url, } #[derive(Deserialize)] struct CardResponse { name: String, } impl ScryfallOriginalCardNameFetcher { pub fn new(client: Client, url: Url) -> Self { Self { client, url } } } impl OriginalCardNameFetcher for ScryfallOriginalCardNameFetcher { async fn fetch_original_card_name( &self, request: &CardInfoRequest, ) -> Result> { let url = match request { CardInfoRequest::ByPrintedName { name } => { let mut url = self .url .join("/cards/named") .context("failed to form scryfall named url")?; url.query_pairs_mut().append_pair("fuzzy", name); url } CardInfoRequest::BySetAndNumber { set, number } => self .url .join(&format!("/cards/{set}/{number}")) .context("failed to form scryfall set+number url")?, }; let res = self .client .get(url) .header("user-agent", "mtg-price-bot/1.0") .header("accept", "application/json") .send() .await .context("failed to get card info from scryfall")?; if res.status() == StatusCode::NOT_FOUND { return Ok(None); } let response = res .json::() .await .context("failed to parse scryfall response")?; Ok(Some(OriginalCardName(response.name))) } } #[cfg(test)] mod tests { use super::*; use serde_json::json; #[tokio::test] async fn test_search_card_by_printed_name() { let mut server = mockito::Server::new_async().await; let mock = server .mock("GET", "/cards/named") .match_query(mockito::Matcher::UrlEncoded( "fuzzy".into(), "Тефери, герой доминарии".into(), )) .with_status(200) .with_header("content-type", "application/json") .with_body( json!({ "name": "Teferi, Hero of Dominaria" }) .to_string(), ) .create_async() .await; let url = Url::parse(server.url().as_str()).unwrap(); let name = ScryfallOriginalCardNameFetcher::new(Client::new(), url) .fetch_original_card_name(&CardInfoRequest::ByPrintedName { name: "Тефери, герой доминарии".into(), }) .await .unwrap(); assert_eq!(name.unwrap().0, "Teferi, Hero of Dominaria"); mock.assert_async().await; } #[tokio::test] async fn test_search_card_by_set_and_number() { let mut server = mockito::Server::new_async().await; let mock = server .mock("GET", "/cards/GRN/123") .with_status(200) .with_header("content-type", "application/json") .with_body( json!({ "name": "Teferi, Hero of Dominaria" }) .to_string(), ) .create_async() .await; let url = Url::parse(server.url().as_str()).unwrap(); let name = ScryfallOriginalCardNameFetcher::new(Client::new(), url) .fetch_original_card_name(&CardInfoRequest::BySetAndNumber { set: "GRN".into(), number: "123".into(), }) .await .unwrap(); assert_eq!(name.unwrap().0, "Teferi, Hero of Dominaria"); mock.assert_async().await; } #[tokio::test] async fn test_search_card_not_found() { let mut server = mockito::Server::new_async().await; let mock = server .mock("GET", "/cards/named") .match_query(mockito::Matcher::UrlEncoded( "fuzzy".into(), "Тефери, герой доминарии".into(), )) .with_status(404) .create_async() .await; let url = Url::parse(server.url().as_str()).unwrap(); let result = ScryfallOriginalCardNameFetcher::new(Client::new(), url) .fetch_original_card_name(&CardInfoRequest::ByPrintedName { name: "Тефери, герой доминарии".into(), }) .await .unwrap(); assert!(result.is_none()); mock.assert_async().await; } }