scryfall_sdk_rust/resources/
cards.rs

1//! Card resource definitions
2//!
3//! See [Scryfall api documentation](https://scryfall.com/docs/api/cards)
4
5use reqwest::Method;
6use serde::{Deserialize, Serialize};
7use time::Date;
8use uuid::Uuid;
9use url::Url;
10use strum_macros::Display;
11use CardCatalogResource::Autocomplete;
12use CardPageResource::Search;
13use CardResource::*;
14use CardCollectionResource::*;
15use crate::HttpResource;
16use crate::resources::card_symbols::ColorSymbol;
17use crate::resources::catalog::Catalog;
18use crate::resources::ResourceKind;
19
20// ---------------------------------------
21// --  HTTP resources  -------------------
22// ---------------------------------------
23
24/// Endpoints for `/cards/*` resource (single card)
25pub enum CardResource<'a> {
26    /// Binding for endpoint `GET /cards/:id`
27    ///
28    /// Get a single card by its Scryfall id
29    ById(&'a str),
30
31    /// Binding for endpoint `GET /cards/arena/:id`
32    ///
33    /// Get a single card by its Arena id
34    ByArenaId(&'a str),
35
36    /// Binding for endpoint `GET /cards/cardmarket/:id`
37    ///
38    /// Get a single card by its Cardmarket id
39    ByCardmarketId(&'a str),
40
41    /// Binding for endpoint `GET /cards/:code/:number`
42    ///
43    /// Get a single card by its code and collector number
44    ByCode(&'a str, &'a str),
45
46    /// Binding for endpoint `GET /cards/mtgo/:id`
47    ///
48    /// Get a single card by its MTGO id
49    ByMtgoId(&'a str),
50
51    /// Binding for endpoint `GET /cards/multiverse/:id`
52    ///
53    /// Get a single card by its Multiverse id
54    ByMultiverseId(&'a str),
55
56    /// Binding for endpoint `GET /cards/tcgplayer/:id`
57    ///
58    /// Get a single card by its Tcgplayer id
59    ByTcgplayerId(&'a str),
60
61    /// Binding for endpoint `GET /cards/named?exact={name}`
62    ///
63    /// Get a single card by its exact name
64    NamedExact(&'a str),
65
66    /// Binding for endpoint `GET /cards/named?fuzzy={name}`
67    ///
68    /// Get a single card by fuzzy searching its name.
69    /// If exact match is found it is returned instead
70    NamedFuzzy(&'a str),
71
72    /// Binding for endpoint `GET /cards/random`
73    ///
74    /// Get a single card at random.
75    /// An optional `q` query parameter can provided
76    /// in order to limit the pool of cards.
77    Random(Option<&'a str>),
78}
79
80/// Endpoints for `/cards/*` resource (page)
81pub enum CardPageResource {
82    /// Binding for endpoint `GET /cards/search`
83    ///
84    /// Searches for one ore more cards based on specific query parameters.
85    ///
86    /// The following query parameters are supported:
87    ///
88    /// | parameter             | usage                      |
89    /// |-----------------------|----------------------------|
90    /// | q                     | fulltext search query      |
91    /// | unique                | omitting similar cards     |
92    /// | order                 | field to sort cards by     |
93    /// | dir                   | sorting direction          |
94    /// | include_extras        | extra cards (e.g. tokens)  |
95    /// | include_multilingual  | multilingual card versions |
96    /// | include_variations    | rare card variants         |
97    /// | page                  | results page number        |
98    ///
99    /// More info on the above parameters [are provided here](https://scryfall.com/docs/api/cards/search)
100    ///
101    /// For the `q` parameters, Scryfall provides a very [powerful search syntax](https://scryfall.com/docs/syntax)
102    /// which you could use to fine-tune your search queries.
103    /// Currently only raw string for `q` is supported. Future versions will provide
104    /// a fluent/rust way of defining the query according to the above syntax.
105    ///
106    /// You can use the `with_q` function to initialise the search params
107    /// by only providing the search string and leaving the rest of the query parameters empty.
108    ///
109    /// # Example
110    /// ```
111    /// use scryfall_sdk_rust::{CardPageResource, HttpResource};
112    /// use scryfall_sdk_rust::resources::cards::SearchQueryParams;
113    ///
114    /// let search_card = CardPageResource::Search(
115    ///     SearchQueryParams::with_q("name")
116    /// );
117    ///
118    /// assert_eq!("cards/search?q=name", search_card.path())
119    /// ```
120    Search(SearchQueryParams)
121}
122
123/// Endpoints for `/cards/*` resource (catalogs)
124pub enum CardCatalogResource<'a> {
125    /// Binding for endpoint `GET /cards/autocomplete`
126    ///
127    /// Returns a catalog of cards containing up to 20 card names,
128    /// used for autocompletion functionalities.
129    ///
130    /// See more info for this in the
131    /// [official Scryfall documentation](https://scryfall.com/docs/api/cards/autocomplete).
132    Autocomplete(&'a str),
133}
134
135/// Endpoints for `/cards/collection` resource
136pub enum CardCollectionResource {
137    /// Binding for endpoint `POST /cards/collection`
138    /// 
139    /// Accepts a JSON array of card identifiers, 
140    /// and returns a List object with the collection of requested cards.
141    /// 
142    /// Available identifiers are the following:
143    /// 
144    /// | identifier(s)          | usage                                                  |
145    /// |------------------------|--------------------------------------------------------|
146    /// | id                     | find a card by Scryfall id                             |
147    /// | mtgo_id                | find a card by MTGO id                                 |
148    /// | multiverse_id          | find a card by Multiverse id                           |
149    /// | oracle_id              | find a card by Oracle id                               |
150    /// | illustration_id        | find a card by illustration id (preferred scans)       |
151    /// | name                   | find a card by name (newest)                           |
152    /// | set + name             | find a card by combination of set and card name        |
153    /// | set + collector_number | find a card by combination of set and collector number |
154    ///
155    /// See more info for this in the 
156    /// [official Scryfall documentation](https://scryfall.com/docs/api/cards/collection).
157    WithIdentifiers(CardIdentifiers),
158}
159
160impl<'a> HttpResource<Card> for CardResource<'a> {
161    fn path(&self) -> String {
162        format!("cards/{}", match self {
163            ById(id) => id.to_string(),
164
165            ByArenaId(id) => format!(
166                "arena/{id}"
167            ),
168            ByCardmarketId(id) => format!(
169                "cardmarket/{id}"
170            ),
171            ByCode(code, number) => format!(
172                "{code}/{number}"
173            ),
174            ByMtgoId(id) => format!(
175                "mtgo/{id}"
176            ),
177            ByMultiverseId(id) => format!(
178                "multiverse/{id}"
179            ),
180            ByTcgplayerId(id) => format!(
181                "tcgplayer/{id}"
182            ),
183            NamedExact(name) => format!(
184                "named?exact={name}"
185            ),
186            NamedFuzzy(name) => format!(
187                "named?fuzzy={name}"
188            ),
189            Random(query) => format!(
190                "random{}", query
191                    .map(|q| format!("?q={q}"))
192                    .unwrap_or("".into())
193            ),
194        })
195    }
196}
197
198impl HttpResource<CardPage> for CardPageResource {
199    fn path(&self) -> String {
200        format!("cards/search{}", match self {
201            Search(params) => params.as_query_str()
202        })
203    }
204}
205
206impl<'a> HttpResource<Catalog> for CardCatalogResource<'a> {
207    fn path(&self) -> String {
208        format!("cards/{}", match self {
209            Autocomplete(q) => format!("autocomplete?q={q}")
210        })
211    }
212}
213
214impl HttpResource<CardCollection> for CardCollectionResource {
215    fn method(&self) -> Method {
216        match self {
217            WithIdentifiers(_) => Method::POST,
218        }
219    }
220
221    fn path(&self) -> String { 
222        match self {
223            WithIdentifiers(_) => "cards/collection".into()
224        }
225    }
226
227    fn json(&self) -> Option<String> {
228        match self {
229            WithIdentifiers(r) => serde_json::to_string(r).ok(),
230        }
231    }
232}
233
234// ---------------------------------------
235// --  Model definitions  ----------------
236// ---------------------------------------
237
238/// Basic struct representing a card
239#[derive(Debug, Serialize, Deserialize, PartialEq)]
240pub struct Card {
241    pub all_parts: Option<Vec<RelatedCard>>,
242    pub arena_id: Option<i32>,
243    pub artist: Option<String>,
244    pub artist_ids: Vec<Uuid>,
245    pub booster: bool,
246    pub border_color: String,
247    pub card_back_id: Option<Uuid>,
248    pub card_faces: Option<Vec<CardFace>>,
249    pub cardmarket_id: Option<i32>,
250    pub cmc: f64,
251    pub collector_number: String,
252    pub color_identity: Vec<ColorSymbol>,
253    pub color_indicator: Option<Vec<ColorSymbol>>,
254    pub colors: Option<Vec<ColorSymbol>>,
255    pub content_warning: Option<bool>,
256    pub digital: bool,
257    pub edhrec_rank: Option<i64>,
258    pub finishes: Vec<CardFinish>,
259    pub flavor_name: Option<String>,
260    pub flavor_text: Option<String>,
261    pub foil: bool,
262    pub frame: String,
263    pub full_art: bool,
264    pub games: Vec<GameKind>,
265    pub hand_modifier: Option<String>,
266    pub highres_image: bool,
267    pub id: Uuid,
268    pub illustration_id: Option<Uuid>,
269    pub image_status: ImageStatus,
270    pub image_uris: Option<ImageUris>,
271    pub keywords: Vec<String>,
272    #[serde(rename = "object")]
273    pub kind: ResourceKind,
274    pub lang: String,
275    pub layout: Layout,
276    pub legalities: Legalities,
277    pub life_modifier: Option<String>,
278    pub loyalty: Option<String>,
279    pub mana_cost: Option<String>,
280    pub mtgo_id: Option<i32>,
281    pub mtgo_foil_id: Option<i32>,
282    pub multiverse_ids: Option<Vec<i32>>,
283    pub name: String,
284    pub nonfoil: bool,
285    pub oracle_id: Uuid,
286    pub oracle_text: Option<String>,
287    pub oversized: bool,
288    pub penny_rank: Option<i64>,
289    pub power: Option<String>,
290    pub prices: Prices,
291    pub printed_name: Option<String>,
292    pub printed_text: Option<String>,
293    pub printed_type_line: Option<String>,
294    pub prints_search_uri: Url,
295    pub produced_mana: Option<Vec<ColorSymbol>>,
296    pub promo: bool,
297    pub promo_types: Option<Vec<String>>,
298    pub purchase_uris: Option<PurchaseUris>,
299    pub rarity: Rarity,
300    pub related_uris: Option<RelatedUris>,
301    pub released_at: Date,
302    pub reprint: bool,
303    pub reserved: bool,
304    pub rulings_uri: Url,
305    pub scryfall_set_uri: Url,
306    pub scryfall_uri: Url,
307    pub security_stamp: Option<String>,
308    pub set: String,
309    pub set_id: String,
310    pub set_name: String,
311    pub set_search_uri: Url,
312    pub set_type: String,
313    pub set_uri: Url,
314    pub story_spotlight: bool,
315    pub tcgplayer_id: Option<i32>,
316    pub tcgplayer_etched_id: Option<i32>,
317    pub textless: bool,
318    pub toughness: Option<String>,
319    pub type_line: String,
320    pub uri: Url,
321    pub variation: bool,
322    pub variation_of: Option<Uuid>,
323}
324
325#[derive(Debug, Serialize, Deserialize, PartialEq)]
326pub struct RelatedCard {
327    pub component: String,
328    pub id: Uuid,
329    #[serde(rename = "object")]
330    pub kind: ResourceKind,
331    pub name: String,
332    pub type_line: String,
333    pub uri: Url,
334}
335
336#[derive(Debug, Serialize, Deserialize, PartialEq)]
337#[serde(rename_all = "lowercase")]
338pub enum CardFinish {
339    Etched,
340    Foil, 
341    Glossy,
342    NonFoil, 
343}
344
345#[derive(Debug, Serialize, Deserialize, PartialEq)]
346#[serde(rename_all = "lowercase")]
347pub enum GameKind {
348    Arena,
349    Mtgo,
350    Paper,
351}
352
353#[derive(Debug, Serialize, Deserialize, PartialEq)]
354#[serde(rename_all = "snake_case")]
355pub enum ImageStatus {
356    HighresScan,
357    Lowres,
358    Missing,
359    Placeholder,
360}
361
362#[derive(Debug, Serialize, Deserialize, PartialEq)]
363#[serde(rename_all = "snake_case")]
364pub enum Layout {
365    Adventure,
366    ArtSeries,
367    Augment,
368    Class,
369    DoubleFacedToken,
370    Emblem,
371    Flip,
372    Host,
373    Leveler,
374    Meld,
375    ModalDfc,
376    Normal,
377    Planar,
378    ReversibleCard,
379    Saga,
380    Scheme,
381    Split,
382    Token,
383    Transform,
384    Vanguard,
385}
386
387#[derive(Debug, Serialize, Deserialize, PartialEq)]
388#[serde(rename_all = "lowercase")]
389pub enum Rarity {
390    Bonus,
391    Common,
392    Mythic,
393    Rare,
394    Special,
395    Uncommon,
396}
397
398#[derive(Debug, Serialize, Deserialize, PartialEq)]
399pub struct CardPage {
400    pub data: Vec<Card>,
401    pub has_more: bool,
402    #[serde(rename = "object")]
403    pub kind: ResourceKind,
404    pub next_page: Option<Url>,
405    pub total_cards: i64,
406}
407
408/// A struct representing the face of a card
409#[derive(Debug, Serialize, Deserialize, PartialEq)]
410pub struct CardFace {
411    pub artist: Option<String>,
412    pub artist_id: Option<Uuid>,
413    pub cmc: Option<f64>,
414    pub color_indicator: Option<Vec<ColorSymbol>>,
415    pub colors: Option<Vec<ColorSymbol>>,
416    pub flavor_name: Option<String>,
417    pub flavor_text: Option<String>,
418    pub illustration_id: Option<Uuid>,
419    pub image_uris: Option<ImageUris>,
420    #[serde(rename = "object")]
421    pub kind: ResourceKind,
422    pub layout: Option<Layout>,
423    pub loyalty: Option<String>,
424    pub mana_cost: String,
425    pub name: String,
426    pub oracle_id: Option<Uuid>,
427    pub oracle_text: Option<String>,
428    pub power: Option<String>,
429    pub printed_name: Option<String>,
430    pub printed_text: Option<String>,
431    pub printed_type_line: Option<String>,
432    pub toughness: Option<String>,
433    pub type_line: Option<String>,
434    pub watermark: Option<String>,
435}
436
437/// Container for image URLs
438#[derive(Debug, Serialize, Deserialize, PartialEq)]
439pub struct ImageUris {
440    pub art_crop: Url,
441    pub border_crop: Url,
442    pub large: Url,
443    pub normal: Url,
444    pub png: Url,
445    pub small: Url,
446}
447
448/// Container for card legalities
449#[derive(Debug, Serialize, Deserialize, PartialEq)]
450pub struct Legalities {
451    pub alchemy: Legality,
452    pub brawl: Legality,
453    pub commander: Legality,
454    pub duel: Legality,
455    pub explorer: Legality,
456    pub future: Legality,
457    pub gladiator: Legality,
458    pub historic: Legality,
459    pub historicbrawl: Legality,
460    pub legacy: Legality,
461    pub modern: Legality,
462    pub oldschool: Legality,
463    pub pauper: Legality,
464    pub paupercommander: Legality,
465    pub penny: Legality,
466    pub pioneer: Legality,
467    pub premodern: Legality,
468    pub standard: Legality,
469    pub vintage: Legality,
470}
471
472/// Container for card prices
473#[derive(Debug, Serialize, Deserialize, PartialEq)]
474pub struct Prices {
475    pub eur: Option<String>,
476    pub eur_foil: Option<String>,
477    pub tix: Option<String>,
478    pub usd: Option<String>,
479    pub usd_etched: Option<String>,
480    pub usd_foil: Option<String>,
481}
482
483/// Container for card purchase URLs
484#[derive(Debug, Serialize, Deserialize, PartialEq)]
485pub struct PurchaseUris {
486    pub cardhoarder: Url,
487    pub cardmarket: Url,
488    pub tcgplayer: Url,
489}
490
491/// Container for other card related URLs
492#[derive(Debug, Serialize, Deserialize, PartialEq)]
493pub struct RelatedUris {
494    pub edhrec: Option<Url>,
495    pub gatherer: Option<Url>,
496    pub tcgplayer_infinite_articles: Option<Url>,
497    pub tcgplayer_infinite_decks: Option<Url>,
498}
499
500/// Card legality enum
501#[derive(Serialize, Deserialize, Debug, PartialEq)]
502pub enum Legality {
503    #[serde(rename = "banned")]
504    Banned,
505
506    #[serde(rename = "legal")]
507    Legal,
508
509    #[serde(rename = "not_legal")]
510    NotLegal,
511
512    #[serde(rename = "restricted")]
513    Restricted,
514}
515
516pub struct SearchQueryParams {
517    pub dir: Option<OrderDirection>,
518    pub include_extras: Option<bool>,
519    pub include_multilingual: Option<bool>,
520    pub include_variations: Option<bool>,
521    pub order: Option<OrderField>,
522    pub page: Option<u32>,
523    pub q: String,
524    pub unique: Option<UniqueMode>,
525}
526
527impl SearchQueryParams {
528    pub fn as_query_str(&self) -> String {
529        let mut query = format!("?q={}", self.q);
530
531        query.push_str(self.unique.as_ref()
532            .map_or("".into(),
533                    |mode| format!("&unique={}", mode)
534            ).as_str()
535        );
536
537        query.push_str(self.order.as_ref()
538            .map_or("".into(),
539                    |field| format!("&order={}", field)
540            ).as_str()
541        );
542
543        query.push_str(self.dir.as_ref()
544            .map_or("".into(),
545                    |dir| format!("&dir={}", dir)
546            ).as_str()
547        );
548
549        query.push_str(self.include_extras.as_ref()
550            .map_or("".into(),
551                    |b| format!("&include_extras={}", b)
552            ).as_str()
553        );
554
555        query.push_str(self.include_multilingual.as_ref()
556            .map_or("".into(),
557                    |b| format!("&include_multilingual={}", b),
558            ).as_str()
559        );
560
561        query.push_str(self.include_variations.as_ref()
562            .map_or("".into(),
563                    |b| format!("&include_variations={}", b),
564            ).as_str()
565        );
566
567        query.push_str(self.page.as_ref()
568            .map_or("".into(),
569                    |p| format!("&page={}", p),
570            ).as_str()
571        );
572
573        query
574    }
575
576    pub fn with_q(q: &str) -> Self {
577        SearchQueryParams {
578            q: q.into(),
579            unique: None,
580            order: None,
581            dir: None,
582            include_extras: None,
583            include_multilingual: None,
584            include_variations: None,
585            page: None,
586        }
587    }
588}
589
590#[derive(Default, Display)]
591#[strum(serialize_all = "snake_case")]
592pub enum UniqueMode {
593    Art,
594    #[default]
595    Cards,
596    Prints,
597}
598
599#[derive(Default, Display)]
600#[strum(serialize_all = "snake_case")]
601pub enum OrderField {
602    Artist,
603    Cmc,
604    Color,
605    Edhrec,
606    Eur,
607    #[default]
608    Name,
609    Penny,
610    Power,
611    Rarity,
612    Released,
613    Review,
614    Set,
615    Tix,
616    Toughness,
617    Usd,
618}
619
620#[derive(Default, Display)]
621#[strum(serialize_all = "snake_case")]
622pub enum OrderDirection {
623    #[default]
624    Auto,
625    Asc,
626    Desc,
627}
628
629#[derive(Debug, Serialize, Deserialize, PartialEq)]
630pub struct CardCollection {
631    #[serde(rename = "object")]
632    pub kind: ResourceKind,
633
634    pub not_found: Vec<CardIdentifier>,
635
636    #[serde(rename = "data")]
637    pub cards: Vec<Card>,
638}
639
640#[derive(Debug, Serialize, PartialEq)]
641pub struct CardIdentifiers {
642    pub identifiers: Vec<CardIdentifier>
643}
644
645#[derive(Debug, Display, Serialize, Deserialize, PartialEq)]
646#[serde(untagged)]
647pub enum CardIdentifier {
648    IllustrationId { 
649        #[serde(rename="illustration_id")] 
650        val: String,
651    },
652    MtgoId { 
653        #[serde(rename="mtgo_id")] 
654        val: String,
655    },
656    MutliverseId { 
657        #[serde(rename="multiverse_id")] 
658        val: u32,
659    },
660    Name { 
661        #[serde(rename="name")] 
662        val: String,
663    },
664    OracleId { 
665        #[serde(rename="oracle_id")] 
666        val: String,
667    },
668    ScryfallId { 
669        #[serde(rename="id")] 
670        val: String,
671    },
672    SetAndName { 
673        set: String,
674        name: String, 
675    },
676    SetAndNumber {
677        set: String,
678
679        #[serde(rename="collector_number")] 
680        number: String,
681    },
682}
683
684#[cfg(test)]
685mod tests {
686    use super::*;
687    use rstest::rstest;
688
689    #[rstest]
690    #[case::by_id(CardResource::ById("123"), "cards/123")]
691    #[case::by_arena_id(CardResource::ByArenaId("123"), "cards/arena/123")]
692    #[case::by_cardmarket_id(CardResource::ByCardmarketId("123"), "cards/cardmarket/123")]
693    #[case::by_code(CardResource::ByCode("123", "456"), "cards/123/456")]
694    #[case::by_mtgo_id(CardResource::ByMtgoId("123"), "cards/mtgo/123")]
695    #[case::by_multiverse_id(CardResource::ByMultiverseId("123"), "cards/multiverse/123")]
696    #[case::by_tcgplayer_id(CardResource::ByTcgplayerId("123"), "cards/tcgplayer/123")]
697    #[case::named_exact(CardResource::NamedExact("name"), "cards/named?exact=name")]
698    #[case::named_fuzzy(CardResource::NamedFuzzy("name"), "cards/named?fuzzy=name")]
699    #[case::random(CardResource::Random(None), "cards/random")]
700    #[case::random(CardResource::Random(Some("name")), "cards/random?q=name")]
701    fn card_resource_should_return_path_and_method(
702        #[case] resource: CardResource,
703        #[case] expected: &str
704    ) {
705        assert_eq!(expected, resource.path());
706        assert_eq!(Method::GET, resource.method());
707    }
708
709    #[rstest]
710    #[case::search(CardPageResource::Search(SearchQueryParams::with_q("test")), "cards/search?q=test")]
711    fn card_page_resource_should_return_path_and_method(
712        #[case] resource: CardPageResource,
713        #[case] expected: &str
714     ) {
715        assert_eq!(expected, resource.path());
716        assert_eq!(Method::GET, resource.method());
717    }
718
719    #[rstest]
720    #[case::autocomplete(CardCatalogResource::Autocomplete("test"), "cards/autocomplete?q=test")]
721    fn card_catalog_resource_should_return_path_and_method(
722        #[case] resource: CardCatalogResource,
723        #[case] expected: &str
724    ) {
725        assert_eq!(expected, resource.path());
726        assert_eq!(Method::GET, resource.method());
727    }
728
729    #[rstest]
730    fn card_collection_resource_should_return_path_method_and_json_body() {
731        let resource = CardCollectionResource::WithIdentifiers(
732            CardIdentifiers {
733                identifiers: vec![
734                    CardIdentifier::ScryfallId { val: "123".into() }
735                ]
736            }
737        );
738
739        assert_eq!("cards/collection", resource.path());
740        assert_eq!(Method::POST, resource.method());
741        assert_eq!(String::from("{\"identifiers\":[{\"id\":\"123\"}]}"), resource.json().unwrap());
742    }
743
744    #[rstest]
745    #[case::all_off("q", None, None, None, None, None, None, None, "?q=q")]
746    #[case::all_on("q",
747        Some(UniqueMode::default()),
748        Some(OrderField::default()),
749        Some(OrderDirection::default()),
750        Some(true), Some(true), Some(true), Some(1),
751        "?q=q&unique=cards&order=name&dir=auto&include_extras=true&include_multilingual=true&include_variations=true&page=1"
752    )]
753    fn search_query_params_should_parse_as_query_string(
754        #[case] q: String,
755        #[case] unique: Option<UniqueMode>,
756        #[case] order: Option<OrderField>,
757        #[case] dir: Option<OrderDirection>,
758        #[case] include_extras: Option<bool>,
759        #[case] include_multilingual: Option<bool>,
760        #[case] include_variations: Option<bool>,
761        #[case] page: Option<u32>,
762        #[case] expected: String
763    ) {
764        let params = SearchQueryParams {
765            q,
766            unique,
767            order,
768            dir,
769            include_extras,
770            include_multilingual,
771            include_variations,
772            page,
773        };
774
775        assert_eq!(expected, params.as_query_str())
776    }
777}