1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
//! Module for handling unresolved URLs returned by the scryfall api
//!
//! Some fields of the scryfall api have URLs referring to queries that can be
//! run to obtain more information. This module abstracts the work of fetching
//! that data.
use std::convert::TryFrom;
use std::marker::PhantomData;

use httpstatus::StatusCode;
use serde::de::DeserializeOwned;
use serde::{Deserialize, Serialize};
use url::Url;

use crate::error::Error;
use crate::list::{List, ListIter};

/// An unresolved URI returned by the Scryfall API, or generated by this crate.
///
/// The `fetch` method handles requesting the resource from the API endpoint,
/// and deserializing it into a `T` object. If the type parameter is
/// [`List`][crate::list::List]`<_>`, then additional methods `fetch_iter`
/// and `fetch_all` are available, giving access to objects from all pages
/// of the collection.
#[derive(Serialize, Deserialize, Clone, Eq, PartialEq, Hash, Debug)]
#[serde(transparent)]
pub struct Uri<T> {
    url: Url,
    _marker: PhantomData<fn() -> T>,
}

impl<T> TryFrom<&str> for Uri<T> {
    type Error = crate::error::Error;

    fn try_from(url: &str) -> Result<Self, Self::Error> {
        Ok(Uri::from(Url::parse(url)?))
    }
}

impl<T> From<Url> for Uri<T> {
    fn from(url: Url) -> Self {
        Uri {
            url,
            _marker: PhantomData,
        }
    }
}

impl<T: DeserializeOwned> Uri<T> {
    /// Fetches a resource from the Scryfall API and deserializes it into a type
    /// `T`.
    ///
    /// # Example
    /// ```rust
    /// # use std::convert::TryFrom;
    /// #
    /// # use scryfall::card::Card;
    /// # use scryfall::uri::Uri;
    /// # tokio_test::block_on(async {
    /// let uri =
    ///     Uri::<Card>::try_from("https://api.scryfall.com/cards/named?exact=Lightning+Bolt").unwrap();
    /// let bolt = uri.fetch().await.unwrap();
    /// assert_eq!(bolt.mana_cost, Some("{R}".to_string()));
    /// # })
    /// ```
    pub async fn fetch(&self) -> crate::Result<T> {
        match self.fetch_raw().await {
            Ok(response) => match response.status().as_u16() {
                200..=299 => Ok(response.json().await?),
                status => Err(Error::HttpError(StatusCode::from(status))),
            },
            Err(e) => Err(e),
        }
    }

    pub(crate) async fn fetch_raw(&self) -> crate::Result<reqwest::Response> {
        match reqwest::get(self.url.clone()).await {
            Ok(response) => match response.status().as_u16() {
                400..=599 => Err(Error::ScryfallError(response.json().await?)),
                _ => Ok(response),
            },
            Err(e) => Err(Error::ReqwestError(e.into(), self.url.to_string())),
        }
    }
}

impl<T: DeserializeOwned + Send + Sync + Unpin> Uri<List<T>> {
    /// Lazily iterate over items from all pages of a list. Following pages are
    /// requested once the previous page has been exhausted.
    ///
    /// # Example
    /// ```rust
    /// # use std::convert::TryFrom;
    /// #
    /// # use scryfall::Card;
    /// # use scryfall::list::List;
    /// # use scryfall::uri::Uri;
    /// use futures::stream::StreamExt;
    /// use futures::future;
    /// # tokio_test::block_on(async {
    /// let uri = Uri::<List<Card>>::try_from("https://api.scryfall.com/cards/search?q=zurgo").unwrap();
    /// assert!(
    ///     uri.fetch_iter()
    ///         .await
    ///         .unwrap()
    ///         .into_stream()
    ///         .map(Result::unwrap)
    ///         .filter(|c| future::ready(c.name.contains("Bellstriker")))
    ///         .collect::<Vec<_>>()
    ///         .await
    ///         .len()
    ///          > 0
    /// );
    /// # })
    /// ```
    ///
    /// ```rust
    /// # use std::convert::TryFrom;
    /// #
    /// # use scryfall::Card;
    /// # use scryfall::list::List;
    /// # use scryfall::uri::Uri;
    /// use futures::stream::StreamExt;
    /// use futures::future;
    /// # tokio_test::block_on(async {
    /// let uri = Uri::<List<Card>>::try_from("https://api.scryfall.com/cards/search?q=zurgo").unwrap();
    /// assert!(
    ///     uri.fetch_iter()
    ///         .await
    ///         .unwrap()
    ///         .into_stream_buffered(10)
    ///         .map(Result::unwrap)
    ///         .filter(|c| future::ready(c.name.contains("Bellstriker")))
    ///         .collect::<Vec<_>>()
    ///         .await
    ///         .len()
    ///          > 0
    /// );
    /// # })
    /// ```
    pub async fn fetch_iter(&self) -> crate::Result<ListIter<T>> {
        Ok(self.fetch().await?.into_list_iter())
    }

    /// Eagerly fetch items from all pages of a list. If any of the pages fail
    /// to load, returns an error.
    ///
    /// # Example
    /// ```rust
    /// # use std::convert::TryFrom;
    /// #
    /// # use scryfall::Card;
    /// # use scryfall::list::List;
    /// # use scryfall::uri::Uri;
    /// # tokio_test::block_on(async {
    /// let uri =
    ///     Uri::<List<Card>>::try_from("https://api.scryfall.com/cards/search?q=e:ddu&unique=prints")
    ///         .unwrap();
    /// assert_eq!(uri.fetch_all().await.unwrap().len(), 76);
    /// # })
    /// ```
    pub async fn fetch_all(&self) -> crate::Result<Vec<T>> {
        let mut items = vec![];
        let mut next_page = Some(self.fetch().await?);
        while let Some(page) = next_page {
            items.extend(page.data.into_iter());
            next_page = match page.next_page {
                Some(uri) => Some(uri.fetch().await?),
                None => None,
            };
        }
        Ok(items)
    }
}