├── .github ├── ISSUE_TEMPLATE │ ├── deserialization-failed.md │ └── unsupported-search-syntax.md └── workflows │ ├── rust.yml │ └── scheduled.yml ├── .gitignore ├── .travis.yml ├── Cargo.lock ├── Cargo.toml ├── LICENSE ├── README.md ├── examples ├── bulk │ └── main.rs └── top-printings │ └── main.rs ├── rustfmt.toml ├── src ├── bin │ └── search.rs ├── bulk.rs ├── card.rs ├── card │ ├── border_color.rs │ ├── card_faces.rs │ ├── color.rs │ ├── finishes.rs │ ├── frame.rs │ ├── frame_effect.rs │ ├── game.rs │ ├── image_status.rs │ ├── layout.rs │ ├── legality.rs │ ├── preview.rs │ ├── price.rs │ ├── produced_mana.rs │ ├── promo_types.rs │ ├── rarity.rs │ ├── related_card.rs │ └── security_stamp.rs ├── catalog.rs ├── error.rs ├── format.rs ├── lib.rs ├── list.rs ├── ruling.rs ├── search.rs ├── search │ ├── advanced.rs │ ├── param.rs │ ├── param │ │ ├── compare.rs │ │ ├── criteria.rs │ │ └── value.rs │ └── query.rs ├── set.rs ├── set │ ├── set_code.rs │ └── set_type.rs ├── uri.rs ├── util.rs └── util │ └── streaming_deserializer.rs └── tests └── variants-feature ├── default_variants.rs ├── main.rs ├── unknown_variants.rs └── unknown_variants_slim.rs /.github/ISSUE_TEMPLATE/deserialization-failed.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Deserialization Failed 3 | about: Something from this crate failed to deserialize, usualy related to scryfall 4 | making a backwards incompatible change. 5 | title: '' 6 | labels: bug 7 | assignees: '' 8 | 9 | --- 10 | 11 | **Describe the bug** 12 | What object did you try to retrieve from scryfall. 13 | 14 | **To Reproduce** 15 | Steps to reproduce the behavior 16 | 17 | **Field that failed to deserialise/serde error message** 18 | 19 | 20 | **Screenshots** 21 | If applicable, add screenshots to help explain your problem. 22 | 23 | **Version:** 24 | - `scryfall` version 25 | 26 | **Additional context** 27 | Add any other context about the problem here. 28 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/unsupported-search-syntax.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Unsupported search syntax 3 | about: Request that new scryfall search syntax not supported yet by this crate 4 | title: '' 5 | labels: enhancement 6 | assignees: msmorgan, mendess 7 | 8 | --- 9 | 10 | **Describe the search option you'd like** 11 | It's syntax, parameters, what it does. 12 | 13 | **Link showing the option** 14 | Link to the docs or a link show how the option can be used. 15 | -------------------------------------------------------------------------------- /.github/workflows/rust.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: [ master ] 6 | pull_request: 7 | branches: [ master ] 8 | 9 | env: 10 | CARGO_TERM_COLOR: always 11 | 12 | jobs: 13 | build-and-test: 14 | runs-on: ubuntu-latest 15 | 16 | steps: 17 | - uses: actions/checkout@v2 18 | - uses: taiki-e/install-action@cargo-hack 19 | - name: Build 20 | run: cargo hack build --each-feature --verbose 21 | - name: Run tests 22 | run: cargo hack test --each-feature --verbose 23 | -------------------------------------------------------------------------------- /.github/workflows/scheduled.yml: -------------------------------------------------------------------------------- 1 | name: Weekly Check 2 | 3 | on: 4 | schedule: 5 | - cron: "0 0 * * Mon" 6 | 7 | env: 8 | CARGO_TERM_COLOR: always 9 | 10 | jobs: 11 | default-features-test: 12 | runs-on: ubuntu-latest 13 | steps: 14 | - uses: actions/checkout@v2 15 | - name: Build 16 | run: cargo build --verbose 17 | - name: Run tests 18 | run: cargo test --verbose all_cards -- --ignored 19 | unknown-variants-test: 20 | runs-on: ubuntu-latest 21 | steps: 22 | - uses: actions/checkout@v2 23 | - name: Build 24 | run: cargo build --verbose 25 | - name: Run tests 26 | run: cargo test --features unknown_variants --verbose all_cards -- --ignored 27 | unknown-variants-slim-test: 28 | runs-on: ubuntu-latest 29 | steps: 30 | - uses: actions/checkout@v2 31 | - name: Build 32 | run: cargo build --verbose 33 | - name: Run tests 34 | run: cargo test --features unknown_variants_slim --verbose all_cards -- --ignored 35 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | **/*.rs.bk 3 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: rust 2 | rust: 3 | - stable 4 | - beta 5 | - nightly 6 | matrix: 7 | allow_failures: 8 | - rust: nightly 9 | fast_finish: true 10 | cache: cargo 11 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "scryfall" 3 | version = "0.21.0" 4 | authors = ["Mendess2526 "] 5 | edition = "2021" 6 | description = "A wrapper around the scryfall magic the gathering api" 7 | license = "MIT" 8 | repository = "https://github.com/mendess/scryfall-rs" 9 | readme = "README.md" 10 | keywords = ["mtg", "Magic", "API", "Scryfall"] 11 | categories = ["api-bindings", "games"] 12 | 13 | [package.metadata.docs.rs] 14 | features = ["unknown_variants"] 15 | 16 | [features] 17 | default = ["bulk_caching"] 18 | # default = [] 19 | bulk_caching = ["dep:heck"] 20 | unknown_variants = [] 21 | unknown_variants_slim = [] 22 | bin = ["tokio/macros", "tokio/rt-multi-thread"] 23 | 24 | [dependencies] 25 | async-trait = "0.1.81" 26 | bytes = "1.10.0" 27 | cfg-if = "1" 28 | chrono = { version = "0.4", features = ["serde"] } 29 | futures = "0.3.30" 30 | futures-util = {version = "0.3.31"} 31 | heck = { version = "0.5", optional = true } 32 | httpstatus = "0.1" 33 | itertools = "0.13" 34 | once_cell = "1" 35 | percent-encoding = "2" 36 | reqwest = {version = "0.12.12", features = ["json" ,"blocking", "stream"] } 37 | serde = { version = "1", features = ["derive"] } 38 | serde_json = "1" 39 | serde_urlencoded = "0.7" 40 | static_assertions = "1" 41 | thiserror = "1" 42 | tinyvec = "1" 43 | tokio = { version = "1", default-features = false, features = ["sync", "fs"] } 44 | tokio-stream = {version = "0.1.17", features = ["sync"]} 45 | tokio-util = {version = "0.7.13", features = ["io-util", "io"]} 46 | url = { version = "2", features = ["serde"] } 47 | uuid = { version = "1", features = ["serde"] } 48 | 49 | [dev-dependencies] 50 | strum = { version = "0.26", features = ["derive"] } 51 | tokio = { version = "1", features = ["full"] } 52 | tokio-test = "0.4.4" 53 | static_assertions = "1.1.0" 54 | rand = "0.9.0" 55 | 56 | [[bin]] 57 | name = "search" 58 | path = "src/bin/search.rs" 59 | required-features = ["bin"] 60 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Pedro Mendes 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # scryfall-rs 2 | 3 | A wrapper around the scryfall magic the gathering API 4 | 5 | [![Crates.io](https://img.shields.io/crates/v/scryfall.svg)](https://crates.io/crates/scryfall) 6 | [![Documentation](https://docs.rs/scryfall/badge.svg)](https://docs.rs/scryfall) 7 | ![License](https://img.shields.io/badge/license-MIT-blue.svg) 8 | ![Rust](https://github.com/mendess/scryfall-rs/actions/workflows/rust.yml/badge.svg) 9 | 10 | It wraps the scryfall API as close to it as possible and I try to keep it up to 11 | date 12 | 13 | 14 | ## Cards 15 | 16 | The main way to fetch cards from this API is the `Card` struct. 17 | 18 | This allows you to get cards from `scryfall` using all of their available 19 | REST Apis 20 | 21 | ```rust 22 | use scryfall::card::Card; 23 | match Card::named_fuzzy("Light Bolt") { 24 | Ok(card) => assert_eq!(card.name, "Lightning Bolt"), 25 | Err(e) => panic!(format!("{:?}", e)) 26 | } 27 | ``` 28 | 29 | ## Sets 30 | 31 | You can also fetch information about a card set. 32 | 33 | The available routes for this can be seen on `Set` 34 | 35 | ```rust 36 | use scryfall::set::Set; 37 | assert_eq!(Set::code("mmq").unwrap().name, "Mercadian Masques") 38 | ``` 39 | 40 | ## Dealing with breaking changes 41 | 42 | Scryfall makes a lot of breaking api changes, mostly because magic makes a lot 43 | of breaking changes 😅. Due to the strong typing of this crate, this means that 44 | sometimes code that works one day breaks the next day. For example, there's a 45 | [`PromoType`][promo-type-enum] enum. This enum, when deserializing, will strictly 46 | reject any format it doesn't know about. This means that everytime wizards adds 47 | a new format, scryfall will start returning this new format from its API 48 | which will make your code fail at runtime. 49 | 50 | To cope with this I've added a feature called `unknown_variants`. This feature 51 | adds to these troublesome enums a variant called [`Unknown`][promo-type-unknown], which contains the 52 | string representation of the unknown format. 53 | 54 | This has a few pros and cons: 55 | 56 | - Pros: 57 | - Your code is much less likely to stop working from one day to the next. 58 | - You can exhaustively match on the enum 59 | - Cons: 60 | - The size of the enum is now 24 bytes, instead of 1 61 | - It is no longer Copy 62 | - If you ever depend on a variant being passed through the unknown variant, 63 | when the new variant is added to the enum, it will stop showing up in the 64 | unknown variant. For example, if tomorrow wizards adds a promo type called 65 | "Transparent" and you have `unknown_variants` enabled, `"transparent"` will 66 | start showing up inside the [`PromoType::Unknown`][promo-type-unknown] variant. But in the next 67 | version of this crate, I will add `PromoType::Transparent`, which means that if 68 | you upgrade your dependency on this crate, `"transparent"` will no longer 69 | show up inside the [`PromoType::Unknown`][promo-type-unknown] variant. If you depend on that 70 | behaviour it will be considered a breaking change. 71 | 72 | If you want to have the unknown variant but don't want to pay for the 24 byte 73 | cost, you can opt for the `unknown_variants_slim` feature, which will simply add 74 | an empty `Unknown` variant instead. 75 | 76 | These two features are incompatible and `unknown_variants` will take 77 | precedence if both are present. 78 | 79 | [promo-type-enum]: https://docs.rs/scryfall/latest/scryfall/card/enum.PromoType.html 80 | [promo-type-unknown]: https://docs.rs/scryfall/latest/scryfall/card/enum.PromoType.html#variant.Unknown 81 | -------------------------------------------------------------------------------- /examples/bulk/main.rs: -------------------------------------------------------------------------------- 1 | use futures::StreamExt; 2 | 3 | #[tokio::main] 4 | async fn main() -> scryfall::Result<()> { 5 | let mut stream = scryfall::bulk::all_cards().await?; 6 | 7 | let mut error_count = 0; 8 | let mut count = 0; 9 | 10 | while let Some(card) = stream.next().await { 11 | match card { 12 | Ok(_) => { 13 | count += 1; 14 | if count % 5000 == 0 { 15 | println!("{count}"); 16 | } 17 | }, 18 | Err(e) => { 19 | println!("{:?}", e); 20 | error_count += 1; 21 | }, 22 | } 23 | } 24 | 25 | println!("Found {} cards and {} errors", count, error_count); 26 | 27 | Ok(()) 28 | } 29 | -------------------------------------------------------------------------------- /examples/top-printings/main.rs: -------------------------------------------------------------------------------- 1 | use futures::future; 2 | use futures::stream::StreamExt; 3 | use scryfall::card::Game; 4 | use scryfall::search::prelude::*; 5 | use scryfall::Card; 6 | 7 | #[tokio::main] 8 | async fn main() -> scryfall::Result<()> { 9 | let card_name = std::env::args().nth(1).expect("expected a card name param"); 10 | 11 | let mut search_options = SearchOptions::new(); 12 | search_options 13 | .unique(UniqueStrategy::Prints) 14 | .sort(SortOrder::Usd, SortDirection::Descending) 15 | .query(exact(card_name).and(in_game(Game::Paper))); 16 | 17 | println!("{}", serde_urlencoded::to_string(&search_options).unwrap()); 18 | 19 | let cards: Vec = search_options 20 | .search() 21 | .await? 22 | .into_stream_buffered(10) 23 | .filter_map(|card| async move { card.ok() }) 24 | .filter(|card| { 25 | future::ready( 26 | card.prices.usd.is_some() || (!card.nonfoil && card.prices.usd_foil.is_some()), 27 | ) 28 | }) 29 | .collect() 30 | .await; 31 | 32 | for card in cards { 33 | println!( 34 | "{name} | {set:>6} {cn:<4} | {usd}", 35 | name = card.name, 36 | set = card.set, 37 | cn = card.collector_number, 38 | usd = card 39 | .prices 40 | .usd 41 | .or(card.prices.usd_foil) 42 | .unwrap_or_else(|| "-".to_string()) 43 | ); 44 | } 45 | 46 | Ok(()) 47 | } 48 | -------------------------------------------------------------------------------- /rustfmt.toml: -------------------------------------------------------------------------------- 1 | edition = "2021" 2 | match_block_trailing_comma = true 3 | use_field_init_shorthand = true 4 | -------------------------------------------------------------------------------- /src/bin/search.rs: -------------------------------------------------------------------------------- 1 | use std::process::ExitCode; 2 | 3 | async fn run() -> Result<(), Box> { 4 | let mut argv = std::env::args().skip(1); 5 | let card = match argv.next().as_deref() { 6 | Some("-i") => { 7 | scryfall::Card::scryfall_id( 8 | argv.next() 9 | .expect("missing scryfall_id argument") 10 | .parse() 11 | .expect("invalid uuid"), 12 | ) 13 | .await? 14 | }, 15 | Some("-n") => { 16 | scryfall::Card::named(&argv.next().expect("missing scryfall_id argument")).await? 17 | }, 18 | Some(opt) => return Err(format!("invalid option {opt}").into()), 19 | None => scryfall::Card::random().await?, 20 | }; 21 | 22 | println!("{card:#?}"); 23 | Ok(()) 24 | } 25 | 26 | #[tokio::main] 27 | async fn main() -> ExitCode { 28 | if let Err(e) = run().await { 29 | eprintln!("{e:#?}"); 30 | ExitCode::FAILURE 31 | } else { 32 | ExitCode::SUCCESS 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/bulk.rs: -------------------------------------------------------------------------------- 1 | //! Scryfall provides daily exports of their card data in bulk files. Each of 2 | //! these files is represented as a bulk_data object via the API. URLs for files 3 | //! change their timestamp each day, and can be fetched programmatically. 4 | //! 5 | //! # Warning 6 | //! 7 | //! These bulk dumps are not paginated, this means that they will be potentially 8 | //! stored in memory in its entirety while being iterated over. 9 | //! 10 | //! # Features 11 | //! 12 | //! With the `bulk_caching` feature enabled, bulk data files will be stored in 13 | //! the OS temp folder. This prevents duplicate downloads if the version has 14 | //! already been saved. 15 | //! 16 | //! See also: [Official Docs](https://scryfall.com/docs/api/bulk-data) 17 | 18 | use std::io::BufReader; 19 | use std::path::Path; 20 | 21 | use cfg_if::cfg_if; 22 | use chrono::{DateTime, Utc}; 23 | use futures::Stream; 24 | use serde::de::DeserializeOwned; 25 | use serde::Deserialize; 26 | use tokio::io::AsyncRead; 27 | use tokio_stream::StreamExt; 28 | use tokio_util::io::StreamReader; 29 | use uuid::Uuid; 30 | 31 | cfg_if! { 32 | if #[cfg(not(feature = "bulk_caching"))] { 33 | use bytes::Buf; 34 | } 35 | } 36 | 37 | use crate::card::Card; 38 | use crate::ruling::Ruling; 39 | use crate::uri::Uri; 40 | use crate::util::{streaming_deserializer, BULK_DATA_URL}; 41 | 42 | /// Scryfall provides daily exports of our card data in bulk files. Each of 43 | /// these files is represented as a bulk_data object via the API. URLs for files 44 | /// change their timestamp each day, and can be fetched programmatically. 45 | /// 46 | /// ## Please note: 47 | /// 48 | /// * Card objects in bulk data include price information, but prices should be 49 | /// considered dangerously stale after 24 hours. Only use bulk price 50 | /// information to track trends or provide a general estimate of card value. 51 | /// Prices are not updated frequently enough to power a storefront or sales 52 | /// system. You consume price information at your own risk. 53 | /// * Updates to gameplay data (such as card names, Oracle text, mana costs, 54 | /// etc) are much less frequent. If you only need gameplay information, 55 | /// downloading card data once per week or right after set releases would most 56 | /// likely be sufficient. 57 | /// * Every card type in every product is included, including planar cards, 58 | /// schemes, Vanguard cards, tokens, emblems, and funny cards. Make sure 59 | /// you’ve reviewed documentation for the Card type. 60 | /// 61 | /// 62 | /// Bulk data is only collected once every 12 hours. You can use the card API 63 | /// methods to retrieve fresh objects instead. 64 | #[derive(Deserialize, Debug, Clone)] 65 | #[cfg_attr(test, serde(deny_unknown_fields))] 66 | #[non_exhaustive] 67 | pub struct BulkDataFile { 68 | /// A unique ID for this bulk item. 69 | pub id: Uuid, 70 | 71 | /// The Scryfall API URI for this file. 72 | pub uri: Uri>, 73 | 74 | /// A computer-readable string for the kind of bulk item. 75 | #[serde(rename = "type")] 76 | pub bulk_type: String, 77 | 78 | /// A human-readable name for this file. 79 | pub name: String, 80 | 81 | /// A human-readable description for this file. 82 | pub description: String, 83 | 84 | /// The URI that hosts this bulk file for fetching. 85 | pub download_uri: Uri>, 86 | 87 | /// The time when this file was last updated. 88 | pub updated_at: DateTime, 89 | 90 | /// The size of this file in integer bytes. 91 | pub compressed_size: Option, 92 | 93 | /// The MIME type of this file. 94 | pub content_type: String, 95 | 96 | /// The Content-Encoding encoding that will be used to transmit this file 97 | /// when you download it. 98 | pub content_encoding: String, 99 | 100 | /// The byte size of the bulk file. 101 | pub size: usize, 102 | 103 | #[cfg(test)] 104 | #[serde(rename = "object")] 105 | _object: String, 106 | } 107 | 108 | impl BulkDataFile { 109 | cfg_if! { 110 | if #[cfg(feature = "bulk_caching")] { 111 | /// The full temp path where this file will be downloaded with `load`. The 112 | /// file name has the form "<type>-<date>.json". 113 | fn cache_path(&self) -> std::path::PathBuf { 114 | use heck::ToKebabCase; 115 | std::env::temp_dir().join(format!( 116 | "{}-{}.json", 117 | self.bulk_type.to_kebab_case(), 118 | self.updated_at.format("%Y%m%d%H%M%S"), 119 | )) 120 | } 121 | 122 | async fn get_reader(&self) -> crate::Result> { 123 | let cache_path = self.cache_path(); 124 | if !cache_path.exists() { 125 | self.download(&cache_path).await?; 126 | } 127 | Ok(BufReader::new(std::fs::File::open(cache_path)?)) 128 | } 129 | 130 | async fn get_async_reader(&self) -> crate::Result { 131 | let cache_path = self.cache_path(); 132 | if !cache_path.exists() { 133 | self.download(&cache_path).await?; 134 | } 135 | 136 | let file = tokio::fs::File::open(&cache_path).await?; 137 | 138 | Ok(tokio::io::BufReader::new(file)) 139 | } 140 | } else { 141 | async fn get_reader(&self) -> crate::Result> { 142 | 143 | let response = self.download_uri.fetch_raw().await?; 144 | let body = response.bytes().await.map_err(|e| { 145 | crate::Error::ReqwestError { error: Box::new(e), url: self.download_uri.inner().clone() } 146 | })?; 147 | Ok(BufReader::new(body.reader())) 148 | } 149 | 150 | async fn get_async_reader(&self) -> crate::Result { 151 | let response = self.download_uri.fetch_raw().await?; 152 | let stream = response.bytes_stream() 153 | .map(|bytes_result| { 154 | bytes_result 155 | .map_err(|e| std::io::Error::new(std::io::ErrorKind::Other, e.to_string())) 156 | // .map(|bytes| bytes.to_vec()) 157 | }); 158 | 159 | Ok(StreamReader::new(stream)) 160 | } 161 | } 162 | } 163 | 164 | /// Gets a BulkDataFile of the specified type. 165 | pub async fn of_type(bulk_type: &str) -> crate::Result { 166 | Uri::from(BULK_DATA_URL.join(bulk_type)?).fetch().await 167 | } 168 | 169 | /// Gets a BulkDataFile with the specified unique ID. 170 | pub async fn id(id: Uuid) -> crate::Result { 171 | Uri::from(BULK_DATA_URL.join(id.to_string().as_str())?) 172 | .fetch() 173 | .await 174 | } 175 | 176 | /// Loads the objects from this bulk data download into a `Vec`. 177 | /// 178 | /// Downloads and stores the file in the computer's temp folder if this 179 | /// version hasn't been downloaded yet. Otherwise uses the stored copy. 180 | pub async fn load(&self) -> crate::Result> { 181 | Ok(serde_json::from_reader(self.get_reader().await?)?) 182 | } 183 | 184 | /// Returns an async Stream over the objects from this bulk data download. 185 | /// 186 | /// Downloads and stores the file in the computer's temp folder if this 187 | /// version hasn't been downloaded yet. Otherwise uses the stored copy. 188 | pub async fn load_stream(&self) -> crate::Result>> 189 | where 190 | T: Send + 'static, 191 | { 192 | let reader = self.get_async_reader().await?; 193 | Ok(streaming_deserializer::create(reader)) 194 | } 195 | 196 | /// Downloads this file, saving it to `path`. Overwrites the file if it 197 | /// already exists. 198 | pub async fn download(&self, path: impl AsRef) -> crate::Result<()> { 199 | let path = path.as_ref(); 200 | let response = self.download_uri.fetch_raw().await?; 201 | 202 | let body = response.bytes_stream().map(|bytes_result| { 203 | bytes_result.map_err(|e| std::io::Error::new(std::io::ErrorKind::Other, e.to_string())) 204 | }); 205 | let mut file = tokio::fs::File::create(path).await?; 206 | 207 | tokio::io::copy(&mut StreamReader::new(body), &mut file).await?; 208 | 209 | Ok(()) 210 | } 211 | } 212 | 213 | /// An async Stream containing one Scryfall card object for each Oracle ID on 214 | /// Scryfall. The chosen sets for the cards are an attempt to return the most 215 | /// up-to-date recognizable version of the card. 216 | pub async fn oracle_cards() -> crate::Result>> { 217 | BulkDataFile::of_type("oracle_cards") 218 | .await? 219 | .load_stream() 220 | .await 221 | } 222 | 223 | /// An async Stream of Scryfall card objects that together contain all unique 224 | /// artworks. The chosen cards promote the best image scans. 225 | pub async fn unique_artwork() -> crate::Result>> { 226 | BulkDataFile::of_type("unique_artwork") 227 | .await? 228 | .load_stream() 229 | .await 230 | } 231 | 232 | /// An async Stream containing every card object on Scryfall in English or the 233 | /// printed language if the card is only available in one language. 234 | pub async fn default_cards() -> crate::Result>> { 235 | BulkDataFile::of_type("default_cards") 236 | .await? 237 | .load_stream() 238 | .await 239 | } 240 | 241 | /// An async Stream of every card object on Scryfall in every language. 242 | pub async fn all_cards() -> crate::Result>> { 243 | BulkDataFile::of_type("all_cards") 244 | .await? 245 | .load_stream() 246 | .await 247 | } 248 | 249 | /// An async Stream of all Rulings on Scryfall. Each ruling refers to cards via an 250 | /// `oracle_id`. 251 | pub async fn rulings() -> crate::Result>> { 252 | BulkDataFile::of_type("rulings").await?.load_stream().await 253 | } 254 | 255 | #[cfg(test)] 256 | mod tests { 257 | use futures::StreamExt; 258 | 259 | use crate::util::streaming_deserializer; 260 | 261 | #[tokio::test] 262 | #[ignore] 263 | async fn oracle_cards() { 264 | let mut stream = super::oracle_cards().await.unwrap(); 265 | while let Some(card) = stream.next().await { 266 | card.unwrap(); 267 | } 268 | } 269 | 270 | #[tokio::test] 271 | #[ignore] 272 | async fn unique_artwork() { 273 | let mut stream = super::unique_artwork().await.unwrap(); 274 | while let Some(card) = stream.next().await { 275 | card.unwrap(); 276 | } 277 | } 278 | 279 | #[tokio::test] 280 | #[ignore] 281 | async fn default_cards() { 282 | let mut stream = super::default_cards().await.unwrap(); 283 | while let Some(card) = stream.next().await { 284 | card.unwrap(); 285 | } 286 | } 287 | 288 | #[tokio::test] 289 | #[ignore] 290 | async fn all_cards() { 291 | let mut stream = super::all_cards().await.unwrap(); 292 | while let Some(card) = stream.next().await { 293 | card.unwrap(); 294 | } 295 | } 296 | 297 | #[tokio::test] 298 | #[ignore] 299 | async fn rulings() { 300 | let mut stream = super::rulings().await.unwrap(); 301 | while let Some(card) = stream.next().await { 302 | card.unwrap(); 303 | } 304 | } 305 | 306 | #[tokio::test] 307 | async fn test_parse_list() { 308 | use crate::ruling::Ruling; 309 | let s = r#"[ 310 | { 311 | "object": "ruling", 312 | "oracle_id": "0004ebd0-dfd6-4276-b4a6-de0003e94237", 313 | "source": "wotc", 314 | "published_at": "2004-10-04", 315 | "comment": "If there are two of these on the battlefield, they do not add together. The result is that only two permanents can be untapped." 316 | }, 317 | { 318 | "object": "ruling", 319 | "oracle_id": "0007c283-5b7a-4c00-9ca1-b455c8dff8c3", 320 | "source": "wotc", 321 | "published_at": "2019-08-23", 322 | "comment": "The “commander tax” increases based on how many times a commander was cast from the command zone. Casting a commander from your hand doesn’t require that additional cost, and it doesn’t increase what the cost will be the next time you cast that commander from the command zone." 323 | } 324 | ]"#; 325 | let mut stream = 326 | streaming_deserializer::create(s.as_bytes()).map(|r: crate::Result| r.unwrap()); 327 | 328 | while let Some(r) = stream.next().await { 329 | drop(r) 330 | } 331 | } 332 | } 333 | -------------------------------------------------------------------------------- /src/card/border_color.rs: -------------------------------------------------------------------------------- 1 | //! Enum defining the colors a mtg card border can have. 2 | use serde::{Deserialize, Serialize}; 3 | 4 | /// Enum defining the colors a mtg card border can have. 5 | #[derive(Default, Serialize, Deserialize, Copy, Clone, Eq, PartialEq, Hash, Debug)] 6 | #[cfg_attr(test, serde(deny_unknown_fields))] 7 | #[serde(rename_all = "snake_case")] 8 | #[allow(missing_docs)] 9 | #[non_exhaustive] 10 | pub enum BorderColor { 11 | #[default] 12 | Black, 13 | Borderless, 14 | Gold, 15 | White, 16 | Silver, 17 | Yellow, 18 | } 19 | 20 | impl std::fmt::Display for BorderColor { 21 | fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { 22 | use BorderColor::*; 23 | write!( 24 | f, 25 | "{}", 26 | match self { 27 | Black => "black", 28 | Borderless => "borderless", 29 | Gold => "gold", 30 | White => "white", 31 | Silver => "silver", 32 | Yellow => "yellow", 33 | } 34 | ) 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/card/card_faces.rs: -------------------------------------------------------------------------------- 1 | use serde::{Deserialize, Serialize}; 2 | use uuid::Uuid; 3 | 4 | use crate::card::Color; 5 | use crate::card::ImageUris; 6 | 7 | use super::Layout; 8 | 9 | /// Multiface cards have a card_faces property containing at least two Card Face 10 | /// objects. 11 | /// 12 | /// --- 13 | /// 14 | /// For more information, refer to the [official docs](https://scryfall.com/docs/api/cards#card-face-objects). 15 | #[derive(Serialize, Deserialize, Clone, PartialEq, Debug)] 16 | #[non_exhaustive] 17 | #[cfg_attr(test, serde(deny_unknown_fields))] 18 | pub struct CardFace { 19 | /// The name of the illustrator of this card face. Newly spoiled cards may 20 | /// not have this field yet. 21 | pub artist: Option, 22 | 23 | /// The colors in this face’s color indicator, if any. 24 | pub color_indicator: Option>, 25 | 26 | /// This face’s colors, if the game defines colors for the individual face 27 | /// of this card. 28 | pub colors: Option>, 29 | 30 | /// The mana value of this particular face, if the card is reversible. 31 | pub cmc: Option, 32 | 33 | /// The flavor text printed on this face, if any. 34 | pub flavor_text: Option, 35 | 36 | /// A unique identifier for the card face artwork that remains consistent 37 | /// across reprints. Newly spoiled cards may not have this field yet. 38 | pub illustration_id: Option, 39 | 40 | /// An object providing URIs to imagery for this face, if this is a 41 | /// double-sided card. If this card is not double-sided, then the image_uris 42 | /// property will be part of the parent object instead. 43 | pub image_uris: Option, 44 | 45 | /// This face’s loyalty, if any. 46 | pub loyalty: Option, 47 | 48 | /// The mana cost for this face. This value will be any empty string "" if 49 | /// the cost is absent. Remember that per the game rules, a missing mana 50 | /// cost and a mana cost of `{0}` are different values. 51 | pub mana_cost: String, 52 | 53 | /// The name of this particular face. 54 | pub name: String, 55 | 56 | /// The Oracle ID of this particular face, if the card is reversible. 57 | pub oracle_id: Option, 58 | 59 | /// The Oracle text for this face, if any. 60 | pub oracle_text: Option, 61 | 62 | /// This face’s power, if any. Note that some cards have powers that are not 63 | /// numeric, such as `*`. 64 | pub power: Option, 65 | 66 | /// The localized name printed on this face, if any. 67 | pub printed_name: Option, 68 | 69 | /// The localized text printed on this face, if any. 70 | pub printed_text: Option, 71 | 72 | /// The type line as printed on the card. 73 | pub printed_type_line: Option, 74 | 75 | /// This face’s toughness, if any. Note that some cards have powers that are not 76 | /// numeric, such as `*`. 77 | pub toughness: Option, 78 | 79 | /// The type line of this particular face. 80 | pub type_line: Option, 81 | 82 | /// The watermark on this particulary card face, if any. 83 | pub watermark: Option, 84 | 85 | /// The ID of the illustrator of this card face. Newly spoiled cards may not have this field yet. 86 | pub artist_id: Option, 87 | 88 | /// The just-for-fun name printed on the card (such as for Godzilla series cards). 89 | pub flavor_name: Option, 90 | 91 | /// This face’s defense, if it's a battle. 92 | pub defense: Option, 93 | 94 | /// The layout of this card face, if the card is reversible. 95 | pub layout: Option, 96 | 97 | #[cfg(test)] 98 | #[serde(rename = "object")] 99 | _object: String, 100 | } 101 | -------------------------------------------------------------------------------- /src/card/color.rs: -------------------------------------------------------------------------------- 1 | use std::fmt; 2 | 3 | use serde::{Deserialize, Serialize}; 4 | 5 | use self::Color::*; 6 | 7 | /// Enum defining the 5 colors of magic, plus colorless. 8 | #[derive( 9 | Default, Serialize, Deserialize, Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug, 10 | )] 11 | #[allow(missing_docs)] 12 | #[repr(u8)] 13 | pub enum Color { 14 | #[default] 15 | #[serde(rename = "C")] 16 | Colorless = 0, 17 | #[serde(rename = "W")] 18 | White = 1 << 0, 19 | #[serde(rename = "U")] 20 | Blue = 1 << 1, 21 | #[serde(rename = "B")] 22 | Black = 1 << 2, 23 | #[serde(rename = "R")] 24 | Red = 1 << 3, 25 | #[serde(rename = "G")] 26 | Green = 1 << 4, 27 | } 28 | 29 | impl fmt::Display for Color { 30 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 31 | write!( 32 | f, 33 | "{}", 34 | match self { 35 | Color::Colorless => "C", 36 | Color::White => "W", 37 | Color::Blue => "U", 38 | Color::Black => "B", 39 | Color::Red => "R", 40 | Color::Green => "G", 41 | } 42 | ) 43 | } 44 | } 45 | 46 | /// Definition of a cards colors. This can be used in conjunction with 47 | /// the `search` module as a 48 | /// [`ColorValue`][crate::search::param::value::ColorValue]. 49 | #[derive(Serialize, Deserialize, Copy, Clone, Eq, PartialEq, Hash, Debug, Default)] 50 | #[cfg_attr(test, serde(deny_unknown_fields))] 51 | pub struct Colors(u8); 52 | 53 | macro_rules! color_consts { 54 | ($( 55 | $(#[$($attr:meta)*])* 56 | $($name:ident),+ => [$($color:tt)*]; 57 | )*) => { 58 | $( 59 | color_consts!(@inner ($(($($attr)*))*) ($($name),+) [$($color)*]); 60 | )* 61 | }; 62 | 63 | ( 64 | @inner 65 | $attrs:tt ($($name:ident),+) $colors:tt 66 | ) => { 67 | $( 68 | color_consts!(@inner $attrs $name $colors); 69 | )+ 70 | }; 71 | 72 | ( 73 | @inner 74 | ($(($($attr:meta)*))*) 75 | $name:ident [$($color:ident),*] 76 | ) => { 77 | $(#[$($attr)*])* 78 | pub const $name: Self = Colors::from_slice(&[$($color),*]); 79 | }; 80 | } 81 | 82 | impl Colors { 83 | color_consts! { 84 | #[doc = "Colorless."] 85 | #[doc(alias = "c")] 86 | COLORLESS => []; 87 | 88 | #[doc = "White."] 89 | #[doc(alias = "w")] 90 | WHITE => [White]; 91 | #[doc = "Blue."] 92 | #[doc(alias = "u")] 93 | BLUE => [Blue]; 94 | #[doc = "Black."] 95 | #[doc(alias = "b")] 96 | BLACK => [Black]; 97 | #[doc = "Red."] 98 | #[doc(alias = "r")] 99 | RED => [Red]; 100 | #[doc = "Green."] 101 | #[doc(alias = "g")] 102 | GREEN => [Green]; 103 | 104 | #[doc = "White and blue. The colors of the Azorius Senate from Ravnica."] 105 | #[doc(alias = "uw")] 106 | #[doc(alias = "wu")] 107 | AZORIUS => [White, Blue]; 108 | #[doc = "Blue and black. The colors of House Dimir from Ravnica."] 109 | #[doc(alias = "ub")] 110 | DIMIR => [Blue, Black]; 111 | #[doc = "Black and red. The colors of the Cult of Rakdos from Ravnica."] 112 | #[doc(alias = "br")] 113 | RAKDOS => [Black, Red]; 114 | #[doc = "Red and green. The colors of the Gruul Clans from Ravnica."] 115 | #[doc(alias = "rg")] 116 | GRUUL => [Red, Green]; 117 | #[doc = "Green and white. The colors of the Selesnya Conclave from Ravnica."] 118 | #[doc(alias = "gw")] 119 | SELESNYA => [Green, White]; 120 | #[doc = "White and black. The colors of the Orzhov Syndicate from Ravnica."] 121 | #[doc(alias = "wb")] 122 | #[doc(alias = "bw")] 123 | ORZHOV => [White, Black]; 124 | #[doc = "Blue and red. The colors of the Izzet League from Ravnica."] 125 | #[doc(alias = "ur")] 126 | IZZET => [Blue, Red]; 127 | #[doc = "Black and green. The colors of the Golgari Swarm from Ravnica."] 128 | #[doc(alias = "bg")] 129 | GOLGARI => [Black, Green]; 130 | #[doc = "Red and white. The colors of the Boros Legion from Ravnica."] 131 | #[doc(alias = "rw")] 132 | BOROS => [Red, White]; 133 | #[doc = "Green and blue. The colors of the Simic Combine from Ravnica."] 134 | #[doc(alias = "gu")] 135 | #[doc(alias = "ug")] 136 | SIMIC => [Green, Blue]; 137 | 138 | #[doc = "White, blue, and black. The colors of the Esper shard of Alara."] 139 | #[doc(alias = "wub")] 140 | ESPER => [White, Blue, Black]; 141 | #[doc = "Blue, black, and red. The colors of the Grixis shard of Alara."] 142 | #[doc(alias = "ubr")] 143 | GRIXIS => [Blue, Black, Red]; 144 | #[doc = "Black, red, and green. The colors of the Jund shard of Alara."] 145 | #[doc(alias = "brg")] 146 | JUND => [Black, Red, Green]; 147 | #[doc = "Red, green, and white. The colors of the Naya shard of Alara."] 148 | #[doc(alias = "rgw")] 149 | NAYA => [Red, Green, White]; 150 | #[doc = "Green, white, and blue. The colors of the Bant shard of Alara."] 151 | #[doc(alias = "gwu")] 152 | BANT => [Green, White, Blue]; 153 | #[doc = "White, black, and green. The colors of the Abzan Houses from Tarkir."] 154 | #[doc(alias = "junk")] 155 | #[doc(alias = "bgw")] 156 | ABZAN => [White, Black, Green]; 157 | #[doc = "Blue, red, and white. The colors of the Jeskai Way from Tarkir."] 158 | #[doc(alias = "american")] 159 | #[doc(alias = "ruw")] 160 | JESKAI => [Blue, Red, White]; 161 | #[doc = "Black, green, and blue. The colors of the Sultai Brood from Tarkir."] 162 | #[doc(alias = "bug")] 163 | SULTAI => [Black, Green, Blue]; 164 | #[doc = "Red, white, and black. The colors of the Mardu Horde from Tarkir."] 165 | #[doc(alias = "rbw")] 166 | MARDU => [Red, White, Black]; 167 | #[doc = "Green, blue, and red. The colors of the Temur Frontier from Tarkir."] 168 | #[doc(alias = "rug")] 169 | TEMUR => [Green, Blue, Red]; 170 | 171 | #[doc = "White, blue, black, and red. The colors of artifice and \ 172 | [Yore-Tiller Nephilim](https://scryfall.com/card/gpt/140)."] 173 | #[doc(alias = "wubr")] 174 | ARTIFICE => [White, Blue, Black, Red]; 175 | #[doc = "Blue, black, red, and green. The colors of chaos and \ 176 | [Glint-Eye Nephilim](https://scryfall.com/card/gpt/115)."] 177 | #[doc(alias = "ubrg")] 178 | CHAOS => [Blue, Black, Red, Green]; 179 | #[doc = "Black, red, green, and white. The colors of aggression and \ 180 | [Dune-Brood Nephilim](https://scryfall.com/card/gpt/110)."] 181 | #[doc(alias = "brgw")] 182 | AGGRESSION => [Black, Red, Green, White]; 183 | #[doc = "Red, green, white, and blue. The colors of altruism and \ 184 | [Ink-treader Nephilim](https://scryfall.com/card/gpt/117)."] 185 | #[doc(alias = "rgwu")] 186 | ALTRUISM => [Red, Green, White, Blue]; 187 | #[doc = "Green, white, blue, and black. The colors of growth and \ 188 | [Witch-Maw Nephilim](https://scryfall.com/card/gpt/138)."] 189 | #[doc(alias = "gwub")] 190 | GROWTH => [Green, White, Blue, Black]; 191 | 192 | #[doc = "White, blue, black, red, and green. All five colors."] 193 | #[doc(alias = "wubrg")] 194 | ALL => [White, Blue, Black, Red, Green]; 195 | } 196 | 197 | /// Constructs an instance from a list of `colors`. 198 | pub const fn from_slice(colors: &[Color]) -> Self { 199 | let mut result = Colors::colorless(); 200 | let mut i = 0; 201 | while i < colors.len() { 202 | result = result.with(colors[i]); 203 | i += 1; 204 | } 205 | result 206 | } 207 | 208 | /// Constructs an instance representing a single `color`. 209 | pub const fn monocolor(color: Color) -> Self { 210 | Colors(color as u8) 211 | } 212 | 213 | /// Creates an instance representing a colorless card. 214 | pub const fn colorless() -> Self { 215 | Colors(Colorless as u8) 216 | } 217 | 218 | /// Checks if this instance is a certain color. 219 | pub const fn is(self, color: Color) -> bool { 220 | self.0 & color as u8 != 0 221 | } 222 | 223 | /// Checks if this instance is multicolored, which is true if it contains 224 | /// more than one color flag. 225 | pub const fn is_multicolored(self) -> bool { 226 | self.0.count_ones() > 1 227 | } 228 | 229 | /// Checks if this instance is colorless. 230 | pub const fn is_colorless(self) -> bool { 231 | self.0 == Colorless as u8 232 | } 233 | 234 | /// Produces a new instance with all the colors from both `self` and 235 | /// `other`. 236 | pub const fn union(self, other: Colors) -> Self { 237 | Colors(self.0 | other.0) 238 | } 239 | 240 | /// Produces a new instance with the colors that `self` and `other` have in 241 | /// common. 242 | pub const fn intersection(self, other: Colors) -> Self { 243 | Colors(self.0 & other.0) 244 | } 245 | 246 | /// Produces a new instance with the colors from `other` removed. 247 | pub const fn difference(self, other: Colors) -> Self { 248 | Colors(self.0 & !other.0) 249 | } 250 | 251 | /// Produces a new instance with the colors that are in `self` or `other`, 252 | /// but not both. 253 | pub const fn symmetric_difference(self, other: Colors) -> Self { 254 | Colors((self.0 ^ other.0) & (self.0 | other.0)) 255 | } 256 | 257 | /// Produces a new instance with the specified color added. 258 | pub const fn with(self, color: Color) -> Self { 259 | Colors(self.0 | color as u8) 260 | } 261 | 262 | /// Produces a new instance with the specified color removed. 263 | pub const fn without(self, color: Color) -> Self { 264 | Colors(self.0 & !(color as u8)) 265 | } 266 | } 267 | 268 | impl std::fmt::Display for Colors { 269 | fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { 270 | if self.is_colorless() { 271 | write!(f, "c") 272 | } else { 273 | let mut s = String::with_capacity(5); 274 | if self.is(White) { 275 | s.push('w'); 276 | } 277 | if self.is(Blue) { 278 | s.push('u'); 279 | } 280 | if self.is(Black) { 281 | s.push('b'); 282 | } 283 | if self.is(Red) { 284 | s.push('r'); 285 | } 286 | if self.is(Green) { 287 | s.push('g'); 288 | } 289 | write!(f, "{}", s) 290 | } 291 | } 292 | } 293 | 294 | impl From<&[Color]> for Colors { 295 | fn from(colors: &[Color]) -> Self { 296 | Colors::from_slice(colors) 297 | } 298 | } 299 | 300 | impl From for Colors { 301 | fn from(color: Color) -> Self { 302 | Colors::monocolor(color) 303 | } 304 | } 305 | 306 | /// Multicolored card. This can be used as a 307 | /// [`ColorValue`][crate::search::param::value::ColorValue] for searching 308 | /// Scryfall. 309 | #[derive(Copy, Clone, Eq, PartialEq, Hash, Debug)] 310 | pub struct Multicolored; 311 | 312 | impl fmt::Display for Multicolored { 313 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 314 | write!(f, "m") 315 | } 316 | } 317 | 318 | #[cfg(test)] 319 | mod tests { 320 | use super::*; 321 | 322 | #[test] 323 | fn union() { 324 | assert_eq!(Colors::RED.union(Colors::WHITE), Colors::BOROS); 325 | assert_eq!(Colors::BLACK.union(Colorless.into()), Colors::BLACK); 326 | assert_eq!(Colors::GOLGARI.union(Colors::WHITE), Colors::ABZAN); 327 | assert_eq!(Colors::ALL.union(Colors::GREEN), Colors::ALL); 328 | } 329 | 330 | #[test] 331 | fn intersection() { 332 | assert_eq!( 333 | Colors::ORZHOV.intersection(Colors::IZZET), 334 | Colors::COLORLESS 335 | ); 336 | assert_eq!(Colors::NAYA.intersection(Colors::ESPER), Colors::WHITE); 337 | assert_eq!(Colors::ALL.intersection(Colors::GRUUL), Colors::GRUUL); 338 | } 339 | 340 | #[test] 341 | fn difference() { 342 | assert_eq!(Colors::SELESNYA.difference(Colors::ALL), Colors::COLORLESS); 343 | assert_eq!(Colors::BANT.difference(Colors::RAKDOS), Colors::BANT); 344 | assert_eq!(Colors::CHAOS.difference(Colors::JESKAI), Colors::GOLGARI); 345 | } 346 | 347 | #[test] 348 | fn symmetric_difference() { 349 | assert_eq!( 350 | Colors::WHITE.symmetric_difference(Colors::BLACK), 351 | Colors::ORZHOV 352 | ); 353 | assert_eq!( 354 | Colors::SIMIC.symmetric_difference(Colors::BLUE), 355 | Colors::GREEN 356 | ); 357 | assert_eq!( 358 | Colors::GRIXIS.symmetric_difference(Colors::TEMUR), 359 | Colors::GOLGARI 360 | ); 361 | } 362 | } 363 | -------------------------------------------------------------------------------- /src/card/finishes.rs: -------------------------------------------------------------------------------- 1 | use serde::{Deserialize, Serialize}; 2 | 3 | /// The finish the card can come in. 4 | #[derive(Serialize, Deserialize, Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug)] 5 | #[cfg_attr(test, serde(deny_unknown_fields))] 6 | #[cfg_attr(not(feature = "unknown_variants"), derive(Copy))] 7 | #[cfg_attr( 8 | all( 9 | not(feature = "unknown_variants"), 10 | not(feature = "unknown_variants_slim") 11 | ), 12 | non_exhaustive 13 | )] 14 | #[serde(rename_all = "lowercase")] 15 | pub enum Finishes { 16 | /// Nonfoil. 17 | Nonfoil, 18 | /// Foil. 19 | Foil, 20 | /// Etched foil. 21 | Etched, 22 | #[cfg_attr( 23 | docsrs, 24 | doc(cfg(any(feature = "unknown_variants", feature = "unknown_variants_slim"))) 25 | )] 26 | #[cfg(feature = "unknown_variants")] 27 | #[serde(untagged)] 28 | /// Unknown frame effect 29 | Unknown(Box), 30 | #[cfg_attr( 31 | docsrs, 32 | doc(cfg(any(feature = "unknown_variants", feature = "unknown_variants_slim"))) 33 | )] 34 | #[cfg(all(not(feature = "unknown_variants"), feature = "unknown_variants_slim"))] 35 | #[serde(other)] 36 | /// Unknown frame effect 37 | Unknown, 38 | } 39 | -------------------------------------------------------------------------------- /src/card/frame.rs: -------------------------------------------------------------------------------- 1 | use serde::{Deserialize, Serialize}; 2 | 3 | /// The frame field tracks the major edition of the card frame of used for the 4 | /// re/print in question. The frame has gone though several major revisions in 5 | /// Magic’s lifetime. 6 | /// 7 | /// [Official docs](https://scryfall.com/docs/api/layouts#frames) 8 | #[derive(Serialize, Deserialize, Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug)] 9 | #[non_exhaustive] 10 | #[cfg_attr(test, serde(deny_unknown_fields))] 11 | pub enum Frame { 12 | /// The original Magic card frame, starting from Limited Edition Alpha. 13 | #[serde(rename = "1993")] 14 | Y1993, 15 | /// The updated classic frame starting from Mirage block. 16 | #[serde(rename = "1997")] 17 | Y1997, 18 | /// The “modern” Magic card frame, introduced in Eighth Edition and Mirrodin 19 | /// block. 20 | #[serde(rename = "2003")] 21 | Y2003, 22 | /// The holofoil-stamp Magic card frame, introduced in Magic 2015. 23 | #[serde(rename = "2015")] 24 | Y2015, 25 | /// The frame used on cards from the future. 26 | #[serde(rename = "future")] 27 | Future, 28 | } 29 | 30 | impl std::fmt::Display for Frame { 31 | fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { 32 | use Frame::*; 33 | write!( 34 | f, 35 | "{}", 36 | match self { 37 | Y1993 => "1993", 38 | Y1997 => "1997", 39 | Y2003 => "2003", 40 | Y2015 => "2015", 41 | Future => "future", 42 | } 43 | ) 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/card/frame_effect.rs: -------------------------------------------------------------------------------- 1 | use serde::{Deserialize, Serialize}; 2 | 3 | /// The frame_effects field tracks additional frame artwork applied over a 4 | /// particular frame. For example, there are both 2003 and 2015-frame cards with 5 | /// the Nyx-touched effect. 6 | /// 7 | /// [Official docs](https://scryfall.com/docs/api/layouts#frame-effects) 8 | #[derive(Serialize, Deserialize, Clone, Eq, PartialEq, Hash, Debug)] 9 | #[cfg_attr(not(feature = "unknown_variants"), derive(Copy))] 10 | #[cfg_attr( 11 | all( 12 | not(feature = "unknown_variants"), 13 | not(feature = "unknown_variants_slim") 14 | ), 15 | non_exhaustive 16 | )] 17 | #[cfg_attr(test, serde(deny_unknown_fields))] 18 | #[serde(rename_all = "lowercase")] 19 | pub enum FrameEffect { 20 | /// The cards have a legendary crown. 21 | Legendary, 22 | /// The miracle frame effect. 23 | Miracle, 24 | /// The Nyx-touched frame effect. 25 | Nyxtouched, 26 | /// The draft-matters frame effect. 27 | Draft, 28 | /// The Devoid frame effect. 29 | Devoid, 30 | /// The Odyssey tombstone mark. 31 | Tombstone, 32 | /// A colorshifted frame. 33 | Colorshifted, 34 | /// The FNM-style inverted frame. 35 | Inverted, 36 | /// The sun and moon transform marks. 37 | SunMoonDfc, 38 | /// The compass and land transform marks. 39 | CompassLandDfc, 40 | /// The Origins and planeswalker transform marks. 41 | OriginPwDfc, 42 | /// The moon and Eldrazi transform marks. 43 | MoonEldraziDfc, 44 | /// The waxing and waning moon transform marks. 45 | WaxingAndWaningMoonDfc, 46 | /// A custom Showcase frame. 47 | Showcase, 48 | /// An extended art frame. 49 | ExtendedArt, 50 | /// The cards have a companion frame. 51 | Companion, 52 | /// The cards have an etched foil treatment. 53 | Etched, 54 | /// The cards have the snowy frame effect. 55 | Snow, 56 | /// The cards have the Lesson frame effect. 57 | Lesson, 58 | /// The cards have the Shattered Glass frame effect. 59 | ShatteredGlass, 60 | /// The cards have More Than Meets the Eye™ marks. 61 | ConvertDfc, 62 | /// The cards have fan transforming marks. 63 | FanDfc, 64 | /// The cards have the Upside Down transforming marks. 65 | UpsideDownDfc, 66 | /// The waxing and waning crescent moon transform marks. 67 | MoonReverseMoonDfc, 68 | /// The cards have the enchantment frame effect. 69 | Enchantment, 70 | /// A full art frame. Undocumented and unsupported for search. 71 | FullArt, 72 | /// A nyxborn card frame. Undocumented and unsupported for search. 73 | Nyxborn, 74 | /// The booster card frame. Undocumented and unsupported for search. 75 | Booster, 76 | /// A textless card frame. Undocumented and unsupported for search. 77 | Textless, 78 | /// Is a story spotlight card 79 | StorySpotlight, 80 | /// The only card that has this, as of this writting is this version of [Commodore 81 | /// Guff](https://api.scryfall.com/cards/9a5bb122-19f3-4e46-a71c-b8a53e9aacc7) 82 | Thick, 83 | /// Borderless frame 84 | Borderless, 85 | /// Is a vehicle 86 | Vehicle, 87 | /// Spree 88 | Spree, 89 | #[cfg_attr( 90 | docsrs, 91 | doc(cfg(any(feature = "unknown_variants", feature = "unknown_variants_slim"))) 92 | )] 93 | #[cfg(feature = "unknown_variants")] 94 | #[serde(untagged)] 95 | /// Unknown frame effect 96 | Unknown(Box), 97 | #[cfg_attr( 98 | docsrs, 99 | doc(cfg(any(feature = "unknown_variants", feature = "unknown_variants_slim"))) 100 | )] 101 | #[cfg(all(not(feature = "unknown_variants"), feature = "unknown_variants_slim"))] 102 | #[serde(other)] 103 | /// Unknown frame effect 104 | Unknown, 105 | } 106 | 107 | impl std::fmt::Display for FrameEffect { 108 | fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { 109 | use FrameEffect::*; 110 | write!( 111 | f, 112 | "{}", 113 | match self { 114 | Legendary => "legendary", 115 | Miracle => "miracle", 116 | Nyxtouched => "nyxtouched", 117 | Draft => "draft", 118 | Devoid => "devoid", 119 | Tombstone => "tombstone", 120 | Colorshifted => "colorshifted", 121 | Inverted => "inverted", 122 | SunMoonDfc => "sunmoondfc", 123 | CompassLandDfc => "compasslanddfc", 124 | OriginPwDfc => "originpwdfc", 125 | MoonEldraziDfc => "mooneldrazidfc", 126 | MoonReverseMoonDfc => "moonreversemoondfc", 127 | Showcase => "showcase", 128 | ExtendedArt => "extendedart", 129 | Companion => "companion", 130 | Etched => "etched", 131 | Snow => "snow", 132 | Lesson => "lesson", 133 | ShatteredGlass => "shatteredglass", 134 | ConvertDfc => "convertdfc", 135 | FanDfc => "fandfc", 136 | UpsideDownDfc => "upsidedowndfc", 137 | Enchantment => "enchantment", 138 | FullArt => "fullart", 139 | Nyxborn => "nyxborn", 140 | WaxingAndWaningMoonDfc => "waxingandwaningmoondfc", 141 | Booster => "booster", 142 | Textless => "textless", 143 | StorySpotlight => "storyspotlight", 144 | Thick => "thick", 145 | Borderless => "borderless", 146 | Vehicle => "vehicle", 147 | Spree => "spree", 148 | #[cfg(feature = "unknown_variants")] 149 | Unknown(s) => s, 150 | #[cfg(all(not(feature = "unknown_variants"), feature = "unknown_variants_slim"))] 151 | Unknown => "unknown-frame-effect", 152 | } 153 | ) 154 | } 155 | } 156 | -------------------------------------------------------------------------------- /src/card/game.rs: -------------------------------------------------------------------------------- 1 | //! Enum defining the exiting platforms on with a magic card can exist. 2 | use serde::{Deserialize, Serialize}; 3 | 4 | /// Enum defining the exiting platforms on with a magic card can exist. 5 | #[derive(Serialize, Deserialize, Copy, Clone, Eq, PartialEq, Hash, Debug)] 6 | #[cfg_attr(test, serde(deny_unknown_fields))] 7 | #[serde(rename_all = "snake_case")] 8 | #[allow(missing_docs)] 9 | #[non_exhaustive] 10 | pub enum Game { 11 | Paper, 12 | Arena, 13 | Mtgo, 14 | Astral, 15 | Sega, 16 | } 17 | 18 | impl std::fmt::Display for Game { 19 | fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { 20 | write!( 21 | f, 22 | "{}", 23 | match self { 24 | Game::Paper => "paper", 25 | Game::Arena => "arena", 26 | Game::Mtgo => "mtgo", 27 | Game::Astral => "astral", 28 | Game::Sega => "sega", 29 | } 30 | ) 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/card/image_status.rs: -------------------------------------------------------------------------------- 1 | use serde::{Deserialize, Serialize}; 2 | 3 | /// As a card goes through spoiler season or other data entry, it may have no 4 | /// imagery for a period, or low-quality imagery. You can get a 5 | /// computer-readable value of the image’s state using the image_status field 6 | /// on card objects. 7 | #[derive(Serialize, Deserialize, Copy, Clone, Eq, PartialEq, Hash, Debug)] 8 | #[cfg_attr(test, serde(deny_unknown_fields))] 9 | #[serde(rename_all = "snake_case")] 10 | pub enum ImageStatus { 11 | /// The card has no image, or the image is being processed. 12 | /// This value should only be temporary for very new cards. 13 | Missing, 14 | /// Scryfall doesn’t have an image of this card, but we know it exists and 15 | /// we have uploaded a placeholder in the meantime. This value is most 16 | /// common on localized cards. 17 | Placeholder, 18 | /// The card’s image is low-quality, either because it was just spoiled or 19 | /// we don’t have better photography for it yet. 20 | Lowres, 21 | /// This card has a full-resolution scanner image. Crisp and glossy! 22 | HighresScan, 23 | } 24 | -------------------------------------------------------------------------------- /src/card/layout.rs: -------------------------------------------------------------------------------- 1 | use serde::{Deserialize, Serialize}; 2 | 3 | /// The layout property categorizes the arrangement of card parts, faces, and 4 | /// other bounded regions on cards. The layout can be used to programmatically 5 | /// determine which other properties on a card you can expect. 6 | /// 7 | /// Specifically: 8 | /// 9 | /// * Cards with the layouts split, flip, transform, and double_faced_token will 10 | /// always have a card_faces property describing the distinct faces. 11 | /// 12 | /// * Cards with the layout meld will always have a related_cards property 13 | /// pointing to the other meld parts. 14 | /// 15 | /// [Official docs](https://scryfall.com/docs/api/layouts#layout) 16 | #[derive(Serialize, Deserialize, Clone, Eq, PartialEq, Hash, Debug)] 17 | #[cfg_attr(not(feature = "unknown_variants"), derive(Copy))] 18 | #[cfg_attr( 19 | all( 20 | not(feature = "unknown_variants"), 21 | not(feature = "unknown_variants_slim") 22 | ), 23 | non_exhaustive 24 | )] 25 | #[cfg_attr(test, serde(deny_unknown_fields))] 26 | #[serde(rename_all = "snake_case")] 27 | pub enum Layout { 28 | /// A standard Magic card with one face. 29 | Normal, 30 | /// A split-faced card. 31 | Split, 32 | /// Cards that invert vertically with the flip keyword. 33 | Flip, 34 | /// Double-sided cards that transform. 35 | Transform, 36 | /// Double-sided cards that can be played either-side. 37 | ModalDfc, 38 | /// Cards with meld parts printed on the back. 39 | Meld, 40 | /// Cards with Level Up. 41 | Leveler, 42 | /// Class-type enchantment cards 43 | Class, 44 | /// Saga-type cards. 45 | Saga, 46 | /// Cards with an Adventure spell part. 47 | Adventure, 48 | /// Plane and Phenomenon-type cards. 49 | Planar, 50 | /// Scheme-type cards. 51 | Scheme, 52 | /// Vanguard-type cards. 53 | Vanguard, 54 | /// Token cards. 55 | Token, 56 | /// Tokens with another token printed on the back. 57 | DoubleFacedToken, 58 | /// Emblem cards. 59 | Emblem, 60 | /// Cards with Augment. 61 | Augment, 62 | /// Host-type cards. 63 | Host, 64 | /// Art Series collectable double-faced cards. 65 | ArtSeries, 66 | /// A Magic card with two sides that are unrelated. 67 | ReversibleCard, 68 | /// Prototype 69 | Prototype, 70 | /// Mutate 71 | Mutate, 72 | /// Case 73 | Case, 74 | #[cfg_attr( 75 | docsrs, 76 | doc(cfg(any(feature = "unknown_variants", feature = "unknown_variants_slim"))) 77 | )] 78 | #[cfg(feature = "unknown_variants")] 79 | #[serde(untagged)] 80 | /// Unknown layout 81 | Unknown(Box), 82 | #[cfg_attr( 83 | docsrs, 84 | doc(cfg(any(feature = "unknown_variants", feature = "unknown_variants_slim"))) 85 | )] 86 | #[cfg(all(not(feature = "unknown_variants"), feature = "unknown_variants_slim"))] 87 | #[serde(other)] 88 | /// Unknown layout 89 | Unknown, 90 | } 91 | -------------------------------------------------------------------------------- /src/card/legality.rs: -------------------------------------------------------------------------------- 1 | //! Enum describing the 4 states of legality a card can have. 2 | use std::cmp::Ordering; 3 | 4 | use serde::{Deserialize, Serialize}; 5 | 6 | /// Enum describing the 4 states of legality a card can have. 7 | #[derive(Default, Serialize, Deserialize, Copy, Clone, Eq, PartialEq, Hash, Debug)] 8 | #[cfg_attr(test, serde(deny_unknown_fields))] 9 | #[serde(rename_all = "snake_case")] 10 | #[allow(missing_docs)] 11 | pub enum Legality { 12 | Legal, 13 | #[default] 14 | NotLegal, 15 | Restricted, 16 | Banned, 17 | } 18 | 19 | impl PartialOrd for Legality { 20 | fn partial_cmp(&self, other: &Self) -> Option { 21 | match (self, other) { 22 | (Legality::NotLegal, _) | (_, Legality::NotLegal) => None, 23 | (a, b) if a == b => Some(Ordering::Equal), 24 | (Legality::Legal, _) => Some(Ordering::Greater), 25 | (_, Legality::Legal) => Some(Ordering::Less), 26 | (Legality::Restricted, Legality::Banned) => Some(Ordering::Greater), 27 | (Legality::Banned, Legality::Restricted) => Some(Ordering::Less), 28 | _ => unreachable!(), 29 | } 30 | } 31 | } 32 | 33 | #[cfg(test)] 34 | mod tests { 35 | use super::*; 36 | 37 | #[test] 38 | fn compare_legalities() { 39 | use Legality::*; 40 | use Ordering::*; 41 | 42 | let matrix = vec![ 43 | ((Legal, Legal), Some(Equal)), 44 | ((Legal, NotLegal), None), 45 | ((Legal, Restricted), Some(Greater)), 46 | ((Legal, Banned), Some(Greater)), 47 | ((NotLegal, Legal), None), 48 | ((NotLegal, NotLegal), None), 49 | ((NotLegal, Restricted), None), 50 | ((NotLegal, Banned), None), 51 | ((Restricted, Legal), Some(Less)), 52 | ((Restricted, NotLegal), None), 53 | ((Restricted, Restricted), Some(Equal)), 54 | ((Restricted, Banned), Some(Greater)), 55 | ((Banned, Legal), Some(Less)), 56 | ((Banned, NotLegal), None), 57 | ((Banned, Restricted), Some(Less)), 58 | ((Banned, Banned), Some(Equal)), 59 | ]; 60 | 61 | for ((a, b), order) in &matrix { 62 | assert_eq!(&a.partial_cmp(b), order); 63 | } 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /src/card/preview.rs: -------------------------------------------------------------------------------- 1 | //! Struct describing card preview information. 2 | use chrono::NaiveDate; 3 | use serde::{Deserialize, Serialize}; 4 | use url::Url; 5 | 6 | /// Struct describing card preview information. 7 | #[derive(Serialize, Deserialize, Clone, Eq, PartialEq, Hash, Debug, Default)] 8 | #[cfg_attr(test, serde(deny_unknown_fields))] 9 | pub struct Preview { 10 | /// The date this card was previewed. 11 | pub previewed_at: Option, 12 | 13 | /// A link to the preview for this card. 14 | /// 15 | /// NOTE: Sometimes this is an empty string, causing the `Url` 16 | /// deserialization to fail. If this happens, a `None` variant is used 17 | /// instead. 18 | #[serde(deserialize_with = "crate::util::deserialize_or_none")] 19 | pub source_uri: Option, 20 | 21 | /// The name of the source that previewed this card. 22 | pub source: Option, 23 | } 24 | -------------------------------------------------------------------------------- /src/card/price.rs: -------------------------------------------------------------------------------- 1 | //! Module defining a price object containing data in various currencies. 2 | use std::cmp::Ordering; 3 | 4 | use serde::{Deserialize, Serialize}; 5 | 6 | /// Struct defining a price object containing data in various currencies. 7 | #[derive(Serialize, Deserialize, Clone, Eq, PartialEq, Hash, Debug, Default)] 8 | #[cfg_attr(test, serde(deny_unknown_fields))] 9 | #[allow(missing_docs)] 10 | #[non_exhaustive] 11 | pub struct Price { 12 | pub usd: Option, 13 | pub usd_foil: Option, 14 | pub eur: Option, 15 | pub eur_foil: Option, 16 | pub tix: Option, 17 | pub usd_etched: Option, 18 | } 19 | 20 | impl Price { 21 | /// Creates an array of component prices that can be iterated over. 22 | fn to_array(&self) -> [&Option; 5] { 23 | [ 24 | &self.usd, 25 | &self.usd_foil, 26 | &self.eur, 27 | &self.eur_foil, 28 | &self.tix, 29 | ] 30 | } 31 | } 32 | 33 | /// Compares two prices as floating-point numbers. 34 | fn compare_prices(a: &Option, b: &Option) -> Option { 35 | if let (Some(a), Some(b)) = (a, b) { 36 | if let (Ok(a), Ok(b)) = (a.parse::(), b.parse()) { 37 | return a.partial_cmp(&b); 38 | } 39 | } 40 | None 41 | } 42 | 43 | impl PartialOrd for Price { 44 | fn partial_cmp(&self, other: &Self) -> Option { 45 | let mut result = None; 46 | for (a, b) in self.to_array().iter().zip(other.to_array().iter()) { 47 | match (result, compare_prices(a, b)) { 48 | // If either ordering is `None`, use the other. Then if either is `Some(Equal)`, 49 | // use the other. 50 | (None, order) 51 | | (order, None) 52 | | (Some(Ordering::Equal), order) 53 | | (order, Some(Ordering::Equal)) => { 54 | result = order; 55 | }, 56 | // If the two orderings already agree, do nothing. 57 | (Some(a), Some(b)) if a == b => {}, 58 | // Otherwise, they disagree, so these prices cannot be ordered. 59 | _ => return None, 60 | } 61 | } 62 | result 63 | } 64 | } 65 | 66 | #[cfg(test)] 67 | mod tests { 68 | use super::*; 69 | 70 | #[test] 71 | fn no_prices() { 72 | let a = Price::default(); 73 | let b = Price::default(); 74 | 75 | assert_eq!(a.partial_cmp(&b), None); 76 | } 77 | 78 | #[test] 79 | fn prices_agree() { 80 | let a = Price { 81 | usd: Some("5".to_string()), 82 | usd_foil: Some("8".to_string()), 83 | eur: Some("3".to_string()), 84 | ..Default::default() 85 | }; 86 | let b = Price { 87 | usd: Some("10".to_string()), 88 | usd_foil: Some("14".to_string()), 89 | tix: Some("1".to_string()), 90 | ..Default::default() 91 | }; 92 | 93 | assert_eq!(a.partial_cmp(&b), Some(Ordering::Less)); 94 | } 95 | 96 | #[test] 97 | fn prices_disagree() { 98 | let a = Price { 99 | usd: Some("0.1".to_string()), 100 | tix: Some("15".to_string()), 101 | ..Default::default() 102 | }; 103 | let b = Price { 104 | usd: Some("2".to_string()), 105 | tix: Some(".5".to_string()), 106 | ..Default::default() 107 | }; 108 | 109 | assert_eq!(a.partial_cmp(&b), None); 110 | } 111 | 112 | #[test] 113 | fn prices_equal() { 114 | let a = Price { 115 | usd: Some("3.99".to_string()), 116 | tix: Some("2.1".to_string()), 117 | ..Default::default() 118 | }; 119 | let b = Price { 120 | usd: Some("3.99".to_string()), 121 | eur: Some("4.20".to_string()), 122 | ..Default::default() 123 | }; 124 | 125 | assert_eq!(a.partial_cmp(&b), Some(Ordering::Equal)); 126 | } 127 | } 128 | -------------------------------------------------------------------------------- /src/card/produced_mana.rs: -------------------------------------------------------------------------------- 1 | use serde::{Deserialize, Serialize}; 2 | 3 | #[derive(Debug, Copy, Clone, Eq, PartialEq, Serialize, Deserialize)] 4 | #[cfg_attr(test, serde(deny_unknown_fields))] 5 | #[serde(untagged)] 6 | /// A type of mana a card can produce 7 | pub enum ProducedMana { 8 | /// A normal color of magic 9 | Color(super::color::Color), 10 | /// An unfinity kind of mana 11 | UnfinityMana(UnfinityMana), 12 | } 13 | 14 | impl ProducedMana { 15 | #[allow(non_upper_case_globals)] 16 | /// Alias to unfinity 2 mana symbol 17 | pub const Two: Self = Self::UnfinityMana(UnfinityMana::Two); 18 | 19 | #[allow(non_upper_case_globals)] 20 | /// Alias to unfinity "tap" mana symbol 21 | pub const Tap: Self = Self::UnfinityMana(UnfinityMana::Two); 22 | } 23 | 24 | /// Kinds of mana only produced in unfinity 25 | #[derive(Debug, Copy, Clone, Eq, PartialEq, Serialize, Deserialize)] 26 | #[cfg_attr(test, serde(deny_unknown_fields))] 27 | pub enum UnfinityMana { 28 | /// Some sticker sheets have stickers that give creatures the ability to generate 2 29 | /// colorless mana, for some reason wizards used the old templating 30 | /// 31 | /// ## Examples 32 | /// - [Happy Dead Squirrel](https://scryfall.com/card/sunf/8/happy-dead-squirrel) 33 | /// - [Unglued Pea-Brained Dinosaur](https://scryfall.com/card/sunf/45/unglued-pea-brained-dinosaur) 34 | #[serde(rename = "2")] 35 | Two, 36 | /// The is one unfinity card that produces this: 37 | /// [Sole Performer](https://scryfall.com/card/unf/440/sole-performer) 38 | #[serde(rename = "T")] 39 | Tap, 40 | } 41 | 42 | #[cfg(test)] 43 | mod test { 44 | use super::super::color::Color; 45 | use super::*; 46 | use serde_json::from_str; 47 | 48 | #[test] 49 | fn color() { 50 | let c = from_str::("\"W\"").unwrap(); 51 | assert_eq!(c, ProducedMana::Color(Color::White)) 52 | } 53 | 54 | #[test] 55 | fn two() { 56 | let c = from_str::("\"2\"").unwrap(); 57 | assert_eq!(c, ProducedMana::UnfinityMana(UnfinityMana::Two)) 58 | } 59 | 60 | #[test] 61 | fn tap() { 62 | let c = from_str::("\"T\"").unwrap(); 63 | assert_eq!(c, ProducedMana::UnfinityMana(UnfinityMana::Tap)) 64 | } 65 | 66 | #[test] 67 | fn in_json() { 68 | let s = r#"{ "produced_mana": [ "B" ] }"#; 69 | #[derive(Deserialize)] 70 | struct T { 71 | produced_mana: Vec, 72 | } 73 | 74 | let t = from_str::(s).unwrap(); 75 | assert_eq!(t.produced_mana, [ProducedMana::Color(Color::Black)]) 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /src/card/promo_types.rs: -------------------------------------------------------------------------------- 1 | use serde::{Deserialize, Serialize}; 2 | 3 | /// The finish the card can come in. 4 | #[derive(Serialize, Deserialize, Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug)] 5 | #[cfg_attr(not(feature = "unknown_variants"), derive(Copy))] 6 | #[cfg_attr( 7 | all( 8 | not(feature = "unknown_variants"), 9 | not(feature = "unknown_variants_slim") 10 | ), 11 | non_exhaustive 12 | )] 13 | #[cfg_attr(test, serde(deny_unknown_fields))] 14 | #[serde(rename_all = "lowercase")] 15 | #[allow(missing_docs)] 16 | pub enum PromoType { 17 | Alchemy, 18 | Arenaleague, 19 | Beginnerbox, 20 | Boosterfun, 21 | Boxtopper, 22 | Brawldeck, 23 | Bringafriend, 24 | Bundle, 25 | Buyabox, 26 | Commanderpromo, 27 | Commanderparty, 28 | Concept, 29 | Confettifoil, 30 | Convention, 31 | Datestamped, 32 | Dossier, 33 | Doubleexposure, 34 | Doublerainbow, 35 | Draculaseries, 36 | Draftweekend, 37 | DragonScaleFoil, 38 | Duels, 39 | Embossed, 40 | Event, 41 | FFI, 42 | FFII, 43 | FFIII, 44 | FFIV, 45 | FFV, 46 | FFVI, 47 | FFVII, 48 | FFVIII, 49 | FFIX, 50 | FFX, 51 | FFXI, 52 | FFXII, 53 | FFXIII, 54 | FFXIV, 55 | FFXV, 56 | FFXVI, 57 | FirstPlaceFoil, 58 | Fnm, 59 | Fracturefoil, 60 | Galaxyfoil, 61 | Gameday, 62 | Giftbox, 63 | Gilded, 64 | Glossy, 65 | Godzillaseries, 66 | Halofoil, 67 | Imagine, 68 | Instore, 69 | Intropack, 70 | Invisibleink, 71 | Jpwalker, 72 | Judgegift, 73 | League, 74 | Magnified, 75 | Manafoil, 76 | Mediainsert, 77 | Moonlitland, 78 | Neonink, 79 | Oilslick, 80 | Openhouse, 81 | Planeswalkerdeck, 82 | Plastic, 83 | Playerrewards, 84 | Playpromo, 85 | Playtest, 86 | Portrait, 87 | Poster, 88 | Premiereshop, 89 | Prerelease, 90 | Promopack, 91 | Rainbowfoil, 92 | Raisedfoil, 93 | Ravnicacity, 94 | Rebalanced, 95 | Release, 96 | Resale, 97 | Ripplefoil, 98 | Schinesealtart, 99 | Scroll, 100 | Serialized, 101 | Setextension, 102 | Setpromo, 103 | Silverfoil, 104 | Sldbonus, 105 | Stamped, 106 | Startercollection, 107 | Starterdeck, 108 | Stepandcompleat, 109 | Storechampionship, 110 | Surgefoil, 111 | Textured, 112 | Themepack, 113 | Thick, 114 | Tourney, 115 | UpsideDown, 116 | UpsideDownBack, 117 | Vault, 118 | Wizardsplaynetwork, 119 | #[cfg_attr( 120 | docsrs, 121 | doc(cfg(any(feature = "unknown_variants", feature = "unknown_variants_slim"))) 122 | )] 123 | #[cfg(feature = "unknown_variants")] 124 | #[serde(untagged)] 125 | /// Unknown variant 126 | Unknown(Box), 127 | #[cfg_attr( 128 | docsrs, 129 | doc(cfg(any(feature = "unknown_variants", feature = "unknown_variants_slim"))) 130 | )] 131 | #[cfg(all(not(feature = "unknown_variants"), feature = "unknown_variants_slim"))] 132 | #[serde(other)] 133 | Unknown, 134 | } 135 | -------------------------------------------------------------------------------- /src/card/rarity.rs: -------------------------------------------------------------------------------- 1 | //! Enum defining the 4 different rarities a card can come in. 2 | use std::fmt; 3 | 4 | use serde::{Deserialize, Serialize}; 5 | 6 | /// The rarities a card can be printed in. Aside from the usual 4 of 7 | /// `Common`, `Uncommon`, `Rare`, and `Mythic`, there are two additional 8 | /// rarities. 9 | /// - `Special` is used for timeshifted cards and has a [purple symbol](https://scryfall.com/card/tsb/24/lord-of-atlantis). 10 | /// - `Bonus` is used for the power nine in Vintage Masters, and has a 11 | /// ["glowing" mythic symbol](https://scryfall.com/card/vma/4/black-lotus). 12 | /// 13 | /// For the purposes of sorting and comparison, `Special` is considered above 14 | /// `Rare` and below `Mythic`, and `Bonus` is the rarest, above `Mythic. 15 | #[derive(Serialize, Deserialize, Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug)] 16 | #[cfg_attr(test, serde(deny_unknown_fields))] 17 | #[serde(rename_all = "snake_case")] 18 | #[non_exhaustive] 19 | pub enum Rarity { 20 | /// Black set symbol. 21 | Common, 22 | /// Silver set symbol. 23 | Uncommon, 24 | /// Gold set symbol. 25 | Rare, 26 | /// Purple set symbol, used for timeshifted cards. 27 | Special, 28 | /// Orange set symbol. 29 | Mythic, 30 | /// "Glowing" mythic symbol, used for the power nine in VMA. 31 | Bonus, 32 | } 33 | 34 | impl fmt::Display for Rarity { 35 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 36 | write!( 37 | f, 38 | "{}", 39 | match self { 40 | Rarity::Common => "common", 41 | Rarity::Uncommon => "uncommon", 42 | Rarity::Rare => "rare", 43 | Rarity::Special => "special", 44 | Rarity::Mythic => "mythic", 45 | Rarity::Bonus => "bonus", 46 | } 47 | ) 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/card/related_card.rs: -------------------------------------------------------------------------------- 1 | use serde::{Deserialize, Serialize}; 2 | use uuid::Uuid; 3 | 4 | use crate::card::Card; 5 | use crate::uri::Uri; 6 | 7 | /// Cards that are closely related to other cards (because they call them by 8 | /// name, or generate a token, or meld, etc) have a `all_parts` property that 9 | /// contains Related Card objects. 10 | /// 11 | /// For more information, refer to the [official docs](https://scryfall.com/api/cards#related-card-objects). 12 | #[derive(Serialize, Deserialize, Clone, PartialEq, Debug)] 13 | #[cfg_attr(test, serde(deny_unknown_fields))] 14 | #[non_exhaustive] 15 | pub struct RelatedCard { 16 | /// An unique ID for this card in Scryfall’s database. 17 | pub id: Uuid, 18 | 19 | /// A content type for this object, always related_card. 20 | pub component: Component, 21 | 22 | /// A field explaining what role this card plays in this relationship. 23 | pub name: String, 24 | 25 | /// The name of this particular related card. 26 | pub type_line: String, 27 | 28 | /// The name of this particular related card. 29 | pub uri: Uri, 30 | 31 | #[cfg(test)] 32 | #[serde(rename = "object")] 33 | _object: String, 34 | } 35 | 36 | /// The kind of related card. 37 | #[derive(Serialize, Deserialize, Copy, Clone, Eq, PartialEq, Hash, Debug)] 38 | #[cfg_attr(test, serde(deny_unknown_fields))] 39 | #[serde(rename_all = "snake_case")] 40 | #[non_exhaustive] 41 | #[allow(missing_docs)] 42 | pub enum Component { 43 | Token, 44 | MeldPart, 45 | MeldResult, 46 | ComboPiece, 47 | } 48 | -------------------------------------------------------------------------------- /src/card/security_stamp.rs: -------------------------------------------------------------------------------- 1 | use serde::{Deserialize, Serialize}; 2 | 3 | /// The security stamp on this card, if any. 4 | #[derive(Serialize, Deserialize, Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug)] 5 | #[cfg_attr(test, serde(deny_unknown_fields))] 6 | #[cfg_attr( 7 | all( 8 | not(feature = "unknown_variants"), 9 | not(feature = "unknown_variants_slim") 10 | ), 11 | non_exhaustive 12 | )] 13 | #[cfg_attr(not(feature = "unknown_variants"), derive(Copy))] 14 | #[serde(rename_all = "lowercase")] 15 | #[allow(missing_docs)] 16 | pub enum SecurityStamp { 17 | Oval, 18 | Triangle, 19 | Acorn, 20 | Circle, 21 | Arena, 22 | Heart, 23 | #[cfg_attr( 24 | docsrs, 25 | doc(cfg(any(feature = "unknown_variants", feature = "unknown_variants_slim"))) 26 | )] 27 | #[cfg(feature = "unknown_variants")] 28 | #[serde(untagged)] 29 | /// Unknown frame effect 30 | Unknown(Box), 31 | #[cfg_attr( 32 | docsrs, 33 | doc(cfg(any(feature = "unknown_variants", feature = "unknown_variants_slim"))) 34 | )] 35 | #[cfg(all(not(feature = "unknown_variants"), feature = "unknown_variants_slim"))] 36 | #[serde(other)] 37 | /// Unknown frame effect 38 | Unknown, 39 | } 40 | -------------------------------------------------------------------------------- /src/catalog.rs: -------------------------------------------------------------------------------- 1 | //! A Catalog object contains an array of Magic datapoints (words, card values, 2 | //! etc). Catalog objects are provided by the API as aids for building other 3 | //! Magic software and understanding possible values for a field on Card 4 | //! objects. 5 | //! 6 | //! Visit the official [docs](https://scryfall.com/docs/api/catalogs) for more documentation. 7 | 8 | use serde::{Deserialize, Serialize}; 9 | 10 | use crate::uri::Uri; 11 | use crate::util::CATALOG_URL; 12 | 13 | /// A Catalog object contains an array of Magic datapoints (words, card values, 14 | /// etc). Catalog objects are provided by the API as aids for building other 15 | /// Magic software and understanding possible values for a field on Card 16 | /// objects. 17 | #[derive(Serialize, Deserialize, Clone, Eq, PartialEq, Hash, Debug)] 18 | #[cfg_attr(test, serde(deny_unknown_fields))] 19 | pub struct Catalog { 20 | /// A link to the current catalog on Scryfall’s API. 21 | pub uri: Uri, 22 | 23 | /// The number of items in the `data` array. 24 | pub total_values: usize, 25 | 26 | /// An array of datapoints, as strings. 27 | pub data: Vec, 28 | } 29 | 30 | impl Catalog { 31 | /// Returns a list of all nontoken English card names in Scryfall’s 32 | /// database. Values are updated as soon as a new card is entered for 33 | /// spoiler seasons. 34 | /// 35 | /// # Examples 36 | /// ```rust 37 | /// use scryfall::catalog::Catalog; 38 | /// # tokio_test::block_on(async { 39 | /// assert!(Catalog::card_names().await.unwrap().data.len() > 0) 40 | /// # }) 41 | /// ``` 42 | pub async fn card_names() -> crate::Result { 43 | Uri::from(CATALOG_URL.join("card-names")?).fetch().await 44 | } 45 | 46 | /// Returns a list of all canonical artist names in Scryfall’s database. 47 | /// This catalog won’t include duplicate, misspelled, or funny names for 48 | /// artists. Values are updated as soon as a new card is entered for 49 | /// spoiler seasons. 50 | /// 51 | /// # Examples 52 | /// ```rust 53 | /// use scryfall::catalog::Catalog; 54 | /// # tokio_test::block_on(async { 55 | /// assert!(Catalog::artist_names().await.unwrap().data.len() > 0) 56 | /// # }) 57 | /// ``` 58 | pub async fn artist_names() -> crate::Result { 59 | Uri::from(CATALOG_URL.join("artist-names")?).fetch().await 60 | } 61 | 62 | /// Returns a Catalog of all English words, of length 2 or more, that could 63 | /// appear in a card name. Values are drawn from cards currently in 64 | /// Scryfall’s database. Values are updated as soon as a new card is 65 | /// entered for spoiler seasons. 66 | /// 67 | /// # Examples 68 | /// ```rust 69 | /// use scryfall::catalog::Catalog; 70 | /// # tokio_test::block_on(async { 71 | /// assert!(Catalog::word_bank().await.unwrap().data.len() > 0) 72 | /// # }) 73 | /// ``` 74 | pub async fn word_bank() -> crate::Result { 75 | Uri::from(CATALOG_URL.join("word-bank")?).fetch().await 76 | } 77 | 78 | /// Returns a Catalog of all creature types in Scryfall’s database. Values 79 | /// are updated as soon as a new card is entered for spoiler seasons. 80 | /// 81 | /// # Examples 82 | /// ```rust 83 | /// use scryfall::catalog::Catalog; 84 | /// # tokio_test::block_on(async { 85 | /// assert!(Catalog::creature_types().await.unwrap().data.len() > 0) 86 | /// # }) 87 | /// ``` 88 | pub async fn creature_types() -> crate::Result { 89 | Uri::from(CATALOG_URL.join("creature-types")?).fetch().await 90 | } 91 | 92 | /// Returns a Catalog of all Planeswalker types in Scryfall’s database. 93 | /// Values are updated as soon as a new card is entered for spoiler 94 | /// seasons. 95 | /// 96 | /// # Examples 97 | /// ```rust 98 | /// use scryfall::catalog::Catalog; 99 | /// # tokio_test::block_on(async { 100 | /// assert!(Catalog::planeswalker_types().await.unwrap().data.len() > 0) 101 | /// # }) 102 | /// ``` 103 | pub async fn planeswalker_types() -> crate::Result { 104 | Uri::from(CATALOG_URL.join("planeswalker-types")?) 105 | .fetch() 106 | .await 107 | } 108 | 109 | /// Returns a Catalog of all Land types in Scryfall’s database. Values are 110 | /// updated as soon as a new card is entered for spoiler seasons. 111 | /// 112 | /// # Examples 113 | /// ```rust 114 | /// use scryfall::catalog::Catalog; 115 | /// # tokio_test::block_on(async { 116 | /// assert!(Catalog::land_types().await.unwrap().data.len() > 0) 117 | /// # }) 118 | /// ``` 119 | pub async fn land_types() -> crate::Result { 120 | Uri::from(CATALOG_URL.join("land-types")?).fetch().await 121 | } 122 | 123 | /// Returns a Catalog of all artifact types in Scryfall’s database. Values 124 | /// are updated as soon as a new card is entered for spoiler seasons. 125 | /// 126 | /// # Examples 127 | /// ```rust 128 | /// use scryfall::catalog::Catalog; 129 | /// # tokio_test::block_on(async { 130 | /// assert!(Catalog::artifact_types().await.unwrap().data.len() > 0) 131 | /// # }) 132 | /// ``` 133 | pub async fn artifact_types() -> crate::Result { 134 | Uri::from(CATALOG_URL.join("artifact-types")?).fetch().await 135 | } 136 | 137 | /// Returns a Catalog of all enchantment types in Scryfall’s database. 138 | /// Values are updated as soon as a new card is entered for spoiler 139 | /// seasons. 140 | /// 141 | /// # Examples 142 | /// ```rust 143 | /// use scryfall::catalog::Catalog; 144 | /// # tokio_test::block_on(async { 145 | /// assert!(Catalog::enchantment_types().await.unwrap().data.len() > 0) 146 | /// # }) 147 | /// ``` 148 | pub async fn enchantment_types() -> crate::Result { 149 | Uri::from(CATALOG_URL.join("enchantment-types")?) 150 | .fetch() 151 | .await 152 | } 153 | 154 | /// Returns a Catalog of all spell types in Scryfall’s database. Values are 155 | /// updated as soon as a new card is entered for spoiler seasons. 156 | /// 157 | /// # Examples 158 | /// ```rust 159 | /// use scryfall::catalog::Catalog; 160 | /// # tokio_test::block_on(async { 161 | /// assert!(Catalog::spell_types().await.unwrap().data.len() > 0) 162 | /// # }) 163 | /// ``` 164 | pub async fn spell_types() -> crate::Result { 165 | Uri::from(CATALOG_URL.join("spell-types")?).fetch().await 166 | } 167 | 168 | /// Returns a Catalog of all possible values for a creature or vehicle’s 169 | /// power in Scryfall’s database. Values are updated as soon as a new 170 | /// card is entered for spoiler seasons. 171 | /// 172 | /// # Examples 173 | /// ```rust 174 | /// use scryfall::catalog::Catalog; 175 | /// # tokio_test::block_on(async { 176 | /// assert!(Catalog::powers().await.unwrap().data.len() > 0) 177 | /// # }) 178 | /// ``` 179 | pub async fn powers() -> crate::Result { 180 | Uri::from(CATALOG_URL.join("powers")?).fetch().await 181 | } 182 | 183 | /// Returns a Catalog of all possible values for a creature or vehicle’s 184 | /// toughness in Scryfall’s database. Values are updated as soon as a 185 | /// new card is entered for spoiler seasons. 186 | /// 187 | /// # Examples 188 | /// ```rust 189 | /// use scryfall::catalog::Catalog; 190 | /// # tokio_test::block_on(async { 191 | /// assert!(Catalog::toughnesses().await.unwrap().data.len() > 0) 192 | /// # }) 193 | /// ``` 194 | pub async fn toughnesses() -> crate::Result { 195 | Uri::from(CATALOG_URL.join("toughnesses")?).fetch().await 196 | } 197 | 198 | /// Returns a Catalog of all possible values for a Planeswalker’s loyalty in 199 | /// Scryfall’s database. Values are updated as soon as a new card is 200 | /// entered for spoiler seasons. 201 | /// 202 | /// # Examples 203 | /// ```rust 204 | /// use scryfall::catalog::Catalog; 205 | /// # tokio_test::block_on(async { 206 | /// assert!(Catalog::loyalties().await.unwrap().data.len() > 0) 207 | /// # }) 208 | /// ``` 209 | pub async fn loyalties() -> crate::Result { 210 | Uri::from(CATALOG_URL.join("loyalties")?).fetch().await 211 | } 212 | 213 | /// Returns a Catalog of all card watermarks in Scryfall’s database. Values 214 | /// are updated as soon as a new card is entered for spoiler seasons. 215 | /// 216 | /// # Examples 217 | /// ```rust 218 | /// use scryfall::catalog::Catalog; 219 | /// # tokio_test::block_on(async { 220 | /// assert!(Catalog::watermarks().await.unwrap().data.len() > 0) 221 | /// # }) 222 | /// ``` 223 | pub async fn watermarks() -> crate::Result { 224 | Uri::from(CATALOG_URL.join("watermarks")?).fetch().await 225 | } 226 | 227 | /// Returns a Catalog of all keyword abilities in Scryfall’s database. 228 | /// Values are updated as soon as a new card is entered for spoiler seasons. 229 | /// 230 | /// # Examples 231 | /// ```rust 232 | /// # use scryfall::catalog::Catalog; 233 | /// # tokio_test::block_on(async { 234 | /// assert!( 235 | /// Catalog::keyword_abilities().await 236 | /// .unwrap() 237 | /// .data 238 | /// .iter() 239 | /// .find(|a| a.as_str() == "Haste") 240 | /// .is_some() 241 | /// ); 242 | /// # }); 243 | /// ``` 244 | pub async fn keyword_abilities() -> crate::Result { 245 | Uri::from(CATALOG_URL.join("keyword-abilities")?) 246 | .fetch() 247 | .await 248 | } 249 | 250 | /// Returns a Catalog of all keyword actions in Scryfall’s database. Values 251 | /// are updated as soon as a new card is entered for spoiler seasons. 252 | /// 253 | /// # Examples 254 | /// ```rust 255 | /// # use scryfall::catalog::Catalog; 256 | /// # tokio_test::block_on(async { 257 | /// assert!( 258 | /// Catalog::keyword_actions().await 259 | /// .unwrap() 260 | /// .data 261 | /// .iter() 262 | /// .find(|a| a.as_str() == "Scry") 263 | /// .is_some() 264 | /// ); 265 | /// # }) 266 | /// ``` 267 | pub async fn keyword_actions() -> crate::Result { 268 | Uri::from(CATALOG_URL.join("keyword-actions")?) 269 | .fetch() 270 | .await 271 | } 272 | 273 | /// Returns a Catalog of all ability words in Scryfall’s database. Values 274 | /// are updated as soon as a new card is entered for spoiler seasons. 275 | /// 276 | /// # Examples 277 | /// ```rust 278 | /// # use scryfall::catalog::Catalog; 279 | /// assert!( 280 | /// # tokio_test::block_on(async { 281 | /// Catalog::ability_words().await 282 | /// .unwrap() 283 | /// .data 284 | /// .iter() 285 | /// .find(|a| a.as_str() == "Landfall") 286 | /// .is_some() 287 | /// # }) 288 | /// ); 289 | /// ``` 290 | pub async fn ability_words() -> crate::Result { 291 | Uri::from(CATALOG_URL.join("ability-words")?).fetch().await 292 | } 293 | } 294 | -------------------------------------------------------------------------------- /src/error.rs: -------------------------------------------------------------------------------- 1 | //! This module exposes the possible errors this crate has, and ways to interact 2 | //! with them. 3 | use std::{fmt, io}; 4 | 5 | use httpstatus::StatusCode; 6 | use itertools::Itertools; 7 | use reqwest::Error as ReqwestError; 8 | use serde::{Deserialize, Serialize}; 9 | use serde_json::Error as SerdeError; 10 | use url::ParseError as UrlParseError; 11 | 12 | /// The errors that may occur when interacting with the scryfall API. 13 | #[derive(Debug, thiserror::Error)] 14 | pub enum Error { 15 | /// Couldn't parse the json returned from scryfall. This error should never 16 | /// occur. If it does, please 17 | /// [open an issue](https://github.com/Mendess2526/scryfall-rs/issues). 18 | #[error("Error deserializing json: {0}")] 19 | JsonError(#[from] SerdeError), 20 | 21 | /// Couldn't write URL query params. 22 | #[error("Error writing URL query: {0}")] 23 | UrlEncodedError(#[from] serde_urlencoded::ser::Error), 24 | 25 | /// A URL could not be parsed. 26 | #[error("Error parsing URL: {0}")] 27 | UrlParseError(#[from] UrlParseError), 28 | 29 | /// Something went wrong when making the HTTP request. 30 | #[error("Error making request to {url}: {error}")] 31 | ReqwestError { 32 | /// The error reqwest returned. 33 | error: Box, 34 | /// The url that was involved. 35 | url: url::Url, 36 | }, 37 | 38 | /// Scryfall error. Please refer to the [official docs](https://scryfall.com/docs/api/errors). 39 | #[error("Scryfall error: {0}")] 40 | ScryfallError(Box), 41 | 42 | /// HTTP error with status code. 43 | #[error("HTTP error: {0}")] 44 | HttpError(StatusCode), 45 | 46 | /// IO error. 47 | #[error("IO error: {0}")] 48 | IoError(#[from] io::Error), 49 | 50 | /// Other. 51 | #[error("{0}")] 52 | Other(String), 53 | } 54 | 55 | impl From for Box { 56 | fn from(err: SerdeError) -> Self { 57 | Box::new(err.into()) 58 | } 59 | } 60 | 61 | impl From for Box { 62 | fn from(err: UrlParseError) -> Self { 63 | Box::new(err.into()) 64 | } 65 | } 66 | 67 | /// An Error object represents a failure to find information or understand the 68 | /// input you provided to the API. 69 | /// 70 | /// [Official docs](https://scryfall.com/docs/api/errors) 71 | #[derive(Serialize, Deserialize, Clone, Eq, PartialEq, Hash, Debug)] 72 | #[cfg_attr(test, serde(deny_unknown_fields))] 73 | pub struct ScryfallError { 74 | /// An integer HTTP status code for this error. 75 | pub status: u16, 76 | 77 | /// A computer-friendly string representing the appropriate HTTP status 78 | /// code. 79 | pub code: String, 80 | 81 | /// A human-readable string explaining the error. 82 | pub details: String, 83 | 84 | /// A computer-friendly string that provides additional context for the main 85 | /// error. For example, an endpoint many generate HTTP 404 errors for 86 | /// different kinds of input. This field will provide a label for the 87 | /// specific kind of 404 failure, such as ambiguous. 88 | #[serde(rename = "type")] 89 | pub error_type: Option, 90 | 91 | /// If your input also generated non-failure warnings, they will be provided 92 | /// as human-readable strings in this array. 93 | #[serde(default)] 94 | pub warnings: Vec, 95 | 96 | #[cfg(test)] 97 | #[serde(rename = "object")] 98 | _object: String, 99 | } 100 | 101 | impl fmt::Display for ScryfallError { 102 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 103 | write!( 104 | f, 105 | "\n\tdetails:{}{}", 106 | self.details, 107 | if self.warnings.is_empty() { 108 | String::new() 109 | } else { 110 | format!( 111 | "\n\twarnings:\n{}", 112 | self.warnings 113 | .iter() 114 | .map(|w| format!("\t\t{}", w)) 115 | .join("\n") 116 | ) 117 | } 118 | ) 119 | } 120 | } 121 | -------------------------------------------------------------------------------- /src/format.rs: -------------------------------------------------------------------------------- 1 | //! The available magic the gathering formats. 2 | use std::fmt; 3 | 4 | use serde::{Deserialize, Serialize}; 5 | 6 | #[derive(Serialize, Deserialize, Copy, Clone, Eq, PartialEq, Hash, Debug)] 7 | #[allow(missing_docs)] 8 | #[non_exhaustive] 9 | pub enum Format { 10 | Standard, 11 | Modern, 12 | Legacy, 13 | Vintage, 14 | Commander, 15 | Future, 16 | Pauper, 17 | Pioneer, 18 | Penny, 19 | Duel, 20 | OldSchool, 21 | Historic, 22 | Gladiator, 23 | Brawl, 24 | Premodern, 25 | PauperCommander, 26 | Alchemy, 27 | Explorer, 28 | Predh, 29 | Oathbreaker, 30 | Timeless, 31 | StandardBrawl, 32 | HistoricBrawl, 33 | } 34 | 35 | impl fmt::Display for Format { 36 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 37 | use Format::*; 38 | write!( 39 | f, 40 | "{}", 41 | match self { 42 | Standard => "standard", 43 | Modern => "modern", 44 | Legacy => "legacy", 45 | Vintage => "vintage", 46 | Commander => "commander", 47 | Future => "future", 48 | Pauper => "pauper", 49 | Pioneer => "pioneer", 50 | Penny => "penny", 51 | Duel => "duel", 52 | OldSchool => "oldschool", 53 | Historic => "historic", 54 | Gladiator => "gladiator", 55 | Brawl => "brawl", 56 | Premodern => "premodern", 57 | PauperCommander => "paupercommander", 58 | Alchemy => "alchemy", 59 | Explorer => "explorer", 60 | Predh => "predh", 61 | Oathbreaker => "oathbreaker", 62 | Timeless => "timeless", 63 | StandardBrawl => "standardbrawl", 64 | HistoricBrawl => "historicbrawl", 65 | } 66 | ) 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | #![cfg_attr(docsrs, feature(doc_cfg))] 2 | #![deny(missing_docs)] 3 | //! [Scryfall](https://scryfall.com) provides a REST-like API for ingesting our card data 4 | //! programatically. The API exposes information available on the regular site 5 | //! in easy-to-consume formats. 6 | //! 7 | //! ## Cards 8 | //! The main way to fetch cards from this API is the [`Card`] struct. 9 | //! 10 | //! This allows you to get cards from `scryfall` using all of their available 11 | //! REST Apis 12 | //! 13 | //! ```rust 14 | //! use scryfall::card::Card; 15 | //! # tokio_test::block_on(async { 16 | //! match Card::named_fuzzy("Light Bolt").await { 17 | //! Ok(card) => assert_eq!(card.name, "Lightning Bolt"), 18 | //! Err(e) => panic!("{e:?}"), 19 | //! } 20 | //! # }) 21 | //! ``` 22 | //! 23 | //! Double faced cards have some of their properties inside the card_faces array instead of at 24 | //! the top level. 25 | //! ``` 26 | //! use scryfall::card::{Card, Color}; 27 | //! # tokio_test::block_on(async { 28 | //! 29 | //! match Card::named("Delver of Secrets").await { 30 | //! Ok(card) => { 31 | //! assert!(card.colors.is_none()); 32 | //! assert_eq!(card.card_faces.unwrap()[0].colors, Some(vec![Color::Blue])); 33 | //! } 34 | //! Err(e) => panic!("{e:?}"), 35 | //! } 36 | //! 37 | //! # }) 38 | //! ``` 39 | //! 40 | //! ## Sets 41 | //! You can also fetch information about a card set. 42 | //! 43 | //! The available routes for this can be seen on [`Set`] 44 | //! 45 | //! ```rust 46 | //! use scryfall::set::Set; 47 | //! # tokio_test::block_on(async { 48 | //! assert_eq!(Set::code("mmq").await.unwrap().name, "Mercadian Masques") 49 | //! # }) 50 | //! ``` 51 | //! 52 | //! ## Catalogs 53 | //! Finally `scryfall` also allows you to fetch *catalogs* which 54 | //! are collections of Magic the Gathering data points. 55 | //! 56 | //! For example, one could fetch all available card names. 57 | //! ```rust,no_run 58 | //! use scryfall::catalog::Catalog; 59 | //! # tokio_test::block_on(async { 60 | //! assert!(Catalog::card_names().await.unwrap().data.len() > 0) 61 | //! # }) 62 | //! ``` 63 | //! 64 | //! ## Advanced Search 65 | //! 66 | //! One of the main features of `scryfall` is its advanced search. 67 | //! For this the [`search`] module provides a type safe api 68 | //! to interact and query the search engine. For advanced features like 69 | //! sorting and collation, see [`search::advanced`]. 70 | pub mod bulk; 71 | pub mod card; 72 | pub mod catalog; 73 | pub mod error; 74 | pub mod format; 75 | pub mod list; 76 | pub mod ruling; 77 | pub mod search; 78 | pub mod set; 79 | pub mod uri; 80 | mod util; 81 | 82 | /// The result type used to describe all fallible operations of the scryfall 83 | /// crate. 84 | pub type Result = std::result::Result; 85 | 86 | pub use card::Card; 87 | pub use catalog::Catalog; 88 | pub use error::Error; 89 | pub use ruling::Ruling; 90 | pub use set::Set; 91 | 92 | #[cfg(test)] 93 | mod tests { 94 | use std::convert::TryFrom; 95 | 96 | use futures::stream::StreamExt; 97 | 98 | use serde_json::{from_str, to_string}; 99 | 100 | use crate::search::prelude::*; 101 | use crate::set::{Set, SetCode}; 102 | 103 | #[test] 104 | fn set_code_serde_test() { 105 | let instance = SetCode::try_from("war").unwrap(); 106 | let new_instance: SetCode = from_str(&to_string(&instance).unwrap()).unwrap(); 107 | assert_eq!(new_instance, instance); 108 | 109 | let instance = SetCode::try_from("wwar").unwrap(); 110 | let new_instance: SetCode = from_str(&to_string(&instance).unwrap()).unwrap(); 111 | assert_eq!(new_instance, instance) 112 | } 113 | 114 | #[tokio::test] 115 | #[ignore] 116 | async fn all_sets() { 117 | Set::all() 118 | .await 119 | .unwrap() 120 | .into_stream() 121 | .map(Result::unwrap) 122 | .for_each(|set| async move { 123 | assert!(set.code.get().len() >= 3); 124 | }) 125 | .await; 126 | } 127 | 128 | #[tokio::test] 129 | #[ignore] 130 | async fn all_sets_buffered() { 131 | Set::all() 132 | .await 133 | .unwrap() 134 | .into_stream_buffered(10) 135 | .map(Result::unwrap) 136 | .for_each(|set| async move { 137 | assert!(set.code.get().len() >= 3); 138 | }) 139 | .await; 140 | } 141 | 142 | #[tokio::test] 143 | #[ignore] 144 | async fn latest_cards() { 145 | Set::all() 146 | .await 147 | .unwrap() 148 | .into_stream() 149 | .map(Result::unwrap) 150 | .take(30) 151 | .for_each_concurrent(None, |s| async move { 152 | let set_cards = set(s.code).search().await; 153 | if let Err(e) = set_cards { 154 | println!("Could not search for cards in '{}' - {}", s.name, e); 155 | } 156 | }) 157 | .await; 158 | } 159 | 160 | #[tokio::test] 161 | #[ignore] 162 | async fn latest_cards_buffered() { 163 | Set::all() 164 | .await 165 | .unwrap() 166 | .into_stream_buffered(10) 167 | .map(Result::unwrap) 168 | .take(30) 169 | .for_each_concurrent(None, |s| async move { 170 | let set_cards = set(s.code).search().await; 171 | if let Err(e) = set_cards { 172 | println!("Could not search for cards in '{}' - {}", s.name, e); 173 | } 174 | }) 175 | .await; 176 | } 177 | } 178 | -------------------------------------------------------------------------------- /src/list.rs: -------------------------------------------------------------------------------- 1 | //! A [`List`] object represents a requested sequence of other objects (Cards, 2 | //! Sets, etc). List objects may be paginated, and also include information 3 | //! about issues raised when generating the list. 4 | //! 5 | //! This module also defines [`ListIter`], which can iterate over the contents 6 | //! of a `List`. If the list is paginated, the `ListIter` will request each page 7 | //! lazily. 8 | 9 | use futures::{future, stream, Future, Stream, StreamExt}; 10 | use serde::de::DeserializeOwned; 11 | use serde::{Deserialize, Serialize}; 12 | use std::vec; 13 | 14 | use crate::uri::Uri; 15 | 16 | /// A List object represents a requested sequence of other objects (Cards, Sets, 17 | /// etc). List objects may be paginated, and also include information about 18 | /// issues raised when generating the list. 19 | /// 20 | /// --- 21 | /// 22 | /// For more information, visit the [official docs](https://scryfall.com/docs/api/lists). 23 | #[derive(Serialize, Deserialize, Clone, Eq, PartialEq, Hash, Debug)] 24 | #[cfg_attr(test, serde(deny_unknown_fields))] 25 | pub struct List { 26 | /// An array of the requested objects, in a specific order. 27 | pub data: Vec, 28 | 29 | /// True if this List is paginated and there is a page beyond the current 30 | /// page. 31 | pub has_more: bool, 32 | 33 | /// If there is a page beyond the current page, this field will contain a 34 | /// full API URI to that page. You may submit a HTTP GET request to that URI 35 | /// to continue paginating forward on this List. 36 | pub next_page: Option>>, 37 | 38 | /// If this is a list of Card objects, this field will contain the total 39 | /// number of cards found across all pages. 40 | pub total_cards: Option, 41 | 42 | /// An array of human-readable warnings issued when generating this list, as 43 | /// strings. Warnings are non-fatal issues that the API discovered with your 44 | /// input. In general, they indicate that the List will not contain the all 45 | /// of the information you requested. You should fix the warnings and 46 | /// re-submit your request. 47 | pub warnings: Option>, 48 | 49 | #[cfg(test)] 50 | #[serde(rename = "object")] 51 | _object: String, 52 | } 53 | 54 | impl List { 55 | /// Creates an iterator over all the pages of this list. 56 | pub fn into_page_iter(self) -> PageIter { 57 | PageIter { 58 | curr: Some(self), 59 | page_num: 1, 60 | } 61 | } 62 | 63 | /// Creates a ListIter from a List 64 | pub fn into_list_iter(self) -> ListIter { 65 | // `has_more` is assumed to be redundant. 66 | debug_assert!(self.has_more == self.next_page.is_some()); 67 | 68 | ListIter { 69 | inner: self.data.into_iter(), 70 | next_uri: self.next_page, 71 | page_num: 1, 72 | total: self.total_cards, 73 | remaining: self.total_cards, 74 | } 75 | } 76 | } 77 | 78 | /// An iterator that moves objects out of a list. 79 | /// 80 | /// This struct is created by the `into_iter` method on `List`. 81 | /// 82 | /// Upon reaching the end of a page, further pages will be requested and the 83 | /// iterator will continue yielding items from those pages. As a consequence, 84 | /// the `Item` type of this iterator is a `Result` in case those calls fail. 85 | #[derive(Debug, Clone)] 86 | pub struct ListIter { 87 | inner: vec::IntoIter, 88 | next_uri: Option>>, 89 | page_num: usize, 90 | total: Option, 91 | remaining: Option, 92 | } 93 | 94 | impl ListIter { 95 | /// Gets a `ListIter` for the next page of objects by requesting it from the 96 | /// API. 97 | /// 98 | /// # Example 99 | /// ```rust 100 | /// # use scryfall::Set; 101 | /// # tokio_test::block_on(async { 102 | /// let page_1 = Set::code("inn").await.unwrap().cards().await.unwrap(); 103 | /// let mut page_2 = Box::new(page_1.next_page().await.unwrap().unwrap()); 104 | /// assert_eq!( 105 | /// page_2 106 | /// .next() 107 | /// .await 108 | /// .unwrap() 109 | /// .unwrap() 110 | /// .collector_number 111 | /// .parse::() 112 | /// .unwrap(), 113 | /// page_1.into_inner().len() + 1 114 | /// ); 115 | /// # }) 116 | /// ``` 117 | pub async fn next_page(&self) -> crate::Result> { 118 | if let Some(uri) = self.next_uri.as_ref() { 119 | let mut new_iter = uri.fetch_iter().await?; 120 | new_iter.remaining = self.remaining.map(|r| r - self.inner.len()); 121 | new_iter.page_num = self.page_num + 1; 122 | 123 | // The new total should be the same as the old total. 124 | debug_assert_eq!(self.total, new_iter.total); 125 | 126 | Ok(Some(new_iter)) 127 | } else { 128 | Ok(None) 129 | } 130 | } 131 | 132 | /// Asynchronously returns next element of the stream 133 | /// Will automatically handle pagination 134 | /// Returns None if the Stream is exausted, Result otherwise 135 | pub async fn next(&mut self) -> Option> { 136 | match self.inner.next() { 137 | Some(next) => { 138 | self.remaining = self.remaining.map(|r| r - 1); 139 | Some(Ok(next)) 140 | }, 141 | None => match self.next_page().await { 142 | Ok(Some(new_iter)) => { 143 | *self = new_iter; 144 | match self.inner.next() { 145 | Some(next) => { 146 | self.remaining = self.remaining.map(|r| r - 1); 147 | Some(Ok(next)) 148 | }, 149 | None => None, 150 | } 151 | }, 152 | Ok(None) => None, 153 | Err(e) => { 154 | self.next_uri = None; 155 | self.remaining = Some(0); 156 | Some(Err(e)) 157 | }, 158 | }, 159 | } 160 | } 161 | 162 | async fn stream_next(&mut self) -> Option>> { 163 | match self.inner.next() { 164 | Some(next) => { 165 | self.remaining = self.remaining.map(|r| r - 1); 166 | Some(future::ready(Ok(next))) 167 | }, 168 | None => match self.next_page().await { 169 | Ok(Some(new_iter)) => { 170 | *self = new_iter; 171 | match self.inner.next() { 172 | Some(next) => { 173 | self.remaining = self.remaining.map(|r| r - 1); 174 | Some(future::ready(Ok(next))) 175 | }, 176 | None => None, 177 | } 178 | }, 179 | Ok(None) => None, 180 | Err(e) => { 181 | self.next_uri = None; 182 | self.remaining = Some(0); 183 | Some(future::ready(Err(e))) 184 | }, 185 | }, 186 | } 187 | } 188 | 189 | /// Creates a Stream from a ListIter 190 | pub fn into_stream(self) -> impl Stream> + Unpin { 191 | Box::pin(stream::unfold(self, |mut state| async move { 192 | let item = state.stream_next().await; 193 | if let Some(val) = item { 194 | Some((val.await, state)) 195 | } else { 196 | None 197 | } 198 | })) 199 | } 200 | 201 | /// Creates a Stream from a ListIter that is buffered by n items 202 | pub fn into_stream_buffered( 203 | self, 204 | buf_factor: usize, 205 | ) -> impl Stream> + Unpin { 206 | Box::pin( 207 | stream::unfold(self, |mut state| async move { 208 | let item = state.stream_next().await; 209 | item.map(|val| (val, state)) 210 | }) 211 | .buffered(buf_factor), 212 | ) 213 | } 214 | 215 | /// Creates a Stream from a ListIter that is buffered by n items in a non-deterministic order 216 | pub fn into_stream_buffered_unordered( 217 | self, 218 | buf_factor: usize, 219 | ) -> impl Stream> + Unpin { 220 | Box::pin( 221 | stream::unfold(self, |mut state| async move { 222 | let item = state.stream_next().await; 223 | item.map(|val| (val, state)) 224 | }) 225 | .buffer_unordered(buf_factor), 226 | ) 227 | } 228 | 229 | /// Returns approximate size of Listiter 230 | pub fn size_hint(&self) -> (usize, Option) { 231 | if let Some(len) = self.remaining { 232 | (len, Some(len)) 233 | } else { 234 | let len = self.inner.len(); 235 | ( 236 | len, 237 | if self.next_uri.is_some() { 238 | None 239 | } else { 240 | Some(len) 241 | }, 242 | ) 243 | } 244 | } 245 | /// Extracts the inner [`vec::IntoIter`] that holds this page of data. 246 | /// Further pages will not be fetched when it gets to the end. 247 | /// 248 | /// # Examples 249 | /// ```rust 250 | /// # use scryfall::Card; 251 | /// # tokio_test::block_on(async { 252 | /// let card_names = Card::search("stormcrow").await 253 | /// .unwrap() 254 | /// .into_inner() 255 | /// .map(|c| c.name) 256 | /// .collect::>(); 257 | /// assert_eq!(card_names, ["Mindstorm Crown", "Storm Crow"]); 258 | /// # }) 259 | /// ``` 260 | pub fn into_inner(self) -> vec::IntoIter { 261 | self.inner 262 | } 263 | } 264 | 265 | /// An iterator over the pages of a list. Before returning each page, the next 266 | /// page is requested. 267 | pub struct PageIter { 268 | curr: Option>, 269 | page_num: usize, 270 | } 271 | 272 | impl PageIter { 273 | async fn stream_next(&mut self) -> Option>> { 274 | if let Some(curr) = self.curr.take() { 275 | self.curr = match &curr.next_page { 276 | Some(uri) => match uri.fetch().await { 277 | Ok(page) => { 278 | self.page_num += 1; 279 | Some(page) 280 | }, 281 | Err(e) => { 282 | eprintln!("Error fetching page {} - {}", self.page_num + 1, e); 283 | None 284 | }, 285 | }, 286 | None => None, 287 | }; 288 | Some(future::ready(curr)) 289 | } else { 290 | None 291 | } 292 | } 293 | 294 | /// Creates a Stream from a PageIter 295 | pub fn into_stream(self) -> impl Stream> + Unpin { 296 | Box::pin(stream::unfold(self, |mut state| async move { 297 | if let Some(val) = state.stream_next().await { 298 | Some((val.await, state)) 299 | } else { 300 | None 301 | } 302 | })) 303 | } 304 | 305 | /// Creates a Stream from a PageIter 306 | pub fn into_stream_buffered(self, buf_factor: usize) -> impl Stream> + Unpin { 307 | Box::pin( 308 | stream::unfold(self, |mut state| async move { 309 | state.stream_next().await.map(|val| (val, state)) 310 | }) 311 | .buffered(buf_factor), 312 | ) 313 | } 314 | 315 | /// Creates a Stream from a PageIter 316 | pub fn into_stream_buffered_unordered( 317 | self, 318 | buf_factor: usize, 319 | ) -> impl Stream> + Unpin { 320 | Box::pin( 321 | stream::unfold(self, |mut state| async move { 322 | state.stream_next().await.map(|val| (val, state)) 323 | }) 324 | .buffer_unordered(buf_factor), 325 | ) 326 | } 327 | } 328 | -------------------------------------------------------------------------------- /src/ruling.rs: -------------------------------------------------------------------------------- 1 | //! Rulings represent Oracle rulings, Wizards of the Coast set release notes, or 2 | //! Scryfall notes for a particular card. 3 | //! 4 | //! If two cards have the same name, they will have the same set of rulings 5 | //! objects. If a card has rulings, it usually has more than one. 6 | //! 7 | //! Rulings with a `Scryfall` source have been added by the Scryfall team, 8 | //! either to provide additional context for the card, or explain how the card 9 | //! works in an unofficial format (such as Duel Commander). 10 | 11 | use chrono::NaiveDate; 12 | use serde::{Deserialize, Serialize}; 13 | use uuid::Uuid; 14 | 15 | use crate::list::ListIter; 16 | use crate::uri::Uri; 17 | use crate::util::{API_RULING, CARDS_URL}; 18 | 19 | /// Rulings represent Oracle rulings, Wizards of the Coast set release notes, or 20 | /// Scryfall notes for a particular card. 21 | // If two cards have the same name, they will have the same set of rulings objects. If a card has 22 | // rulings, it usually has more than one. 23 | // 24 | // Rulings with a scryfall source have been added by the Scryfall team, either to provide additional 25 | // context for the card, or explain how the card works in an unofficial format (such as Duel 26 | // Commander). 27 | /// --- 28 | /// 29 | /// For more information, refer to the [official docs](https://scryfall.com/docs/api/rulings). 30 | #[derive(Serialize, Deserialize, Clone, Eq, PartialEq, Hash, Debug)] 31 | #[cfg_attr(test, serde(deny_unknown_fields))] 32 | #[non_exhaustive] 33 | pub struct Ruling { 34 | /// A unique ID for the oracle identity of the card this ruling is about. 35 | /// This value is consistent across reprinted card editions, and unique 36 | /// among different cards with the same name (tokens, Unstable variants, 37 | /// etc). 38 | pub oracle_id: Uuid, 39 | 40 | /// A computer-readable string indicating which company produced this 41 | /// ruling, either wotc or scryfall. 42 | pub source: Source, 43 | 44 | /// The date when the ruling or note was published. 45 | pub published_at: NaiveDate, 46 | 47 | /// The text of the ruling. 48 | pub comment: String, 49 | 50 | #[cfg(test)] 51 | #[serde(rename = "object")] 52 | _object: String, 53 | } 54 | 55 | /// The two possible ruling sources 56 | #[derive(Serialize, Deserialize, Copy, Clone, Eq, PartialEq, Hash, Debug)] 57 | #[cfg_attr(test, serde(deny_unknown_fields))] 58 | #[serde(rename_all = "snake_case")] 59 | #[allow(missing_docs)] 60 | pub enum Source { 61 | Wotc, 62 | Scryfall, 63 | } 64 | 65 | impl Ruling { 66 | /// Returns a List of rulings for a card with the given Multiverse ID. If 67 | /// the card has multiple multiverse IDs, this method can find either of 68 | /// them. 69 | /// 70 | /// # Examples 71 | /// ```rust 72 | /// use scryfall::ruling::Ruling; 73 | /// use futures::stream::StreamExt; 74 | /// use futures::future; 75 | /// # tokio_test::block_on(async { 76 | /// assert!( 77 | /// Ruling::multiverse_id(3255) 78 | /// .await 79 | /// .unwrap() 80 | /// .into_stream() 81 | /// .map(Result::unwrap) 82 | /// .any(|r| future::ready(r.comment.ends_with("Yes, this is a bit weird."))) 83 | /// .await 84 | /// ); 85 | /// # }) 86 | /// ``` 87 | /// 88 | /// ```rust 89 | /// use scryfall::ruling::Ruling; 90 | /// use futures::stream::StreamExt; 91 | /// use futures::future; 92 | /// # tokio_test::block_on(async { 93 | /// assert!( 94 | /// Ruling::multiverse_id(3255) 95 | /// .await 96 | /// .unwrap() 97 | /// .into_stream_buffered(10) 98 | /// .map(Result::unwrap) 99 | /// .any(|r| future::ready(r.comment.ends_with("Yes, this is a bit weird."))) 100 | /// .await 101 | /// ); 102 | /// # }) 103 | /// ``` 104 | pub async fn multiverse_id(id: usize) -> crate::Result> { 105 | Uri::from( 106 | CARDS_URL 107 | .join("multiverse/")? 108 | .join(&format!("{}/", id))? 109 | .join(API_RULING)?, 110 | ) 111 | .fetch_iter() 112 | .await 113 | } 114 | 115 | /// Returns rulings for a card with the given MTGO ID (also known as the 116 | /// Catalog ID). The ID can either be the card’s `mtgo_id` or its 117 | /// `mtgo_foil_id`. 118 | /// 119 | /// # Examples 120 | /// ```rust 121 | /// use scryfall::ruling::Ruling; 122 | /// use futures::stream::StreamExt; 123 | /// use futures::future; 124 | /// # tokio_test::block_on(async { 125 | /// assert!( 126 | /// Ruling::mtgo_id(57934) 127 | /// .await 128 | /// .unwrap() 129 | /// .into_stream() 130 | /// .map(Result::unwrap) 131 | /// .any(|r| future::ready(r.comment.ends_with("You read the whole contract, right?"))) 132 | /// .await 133 | /// ); 134 | /// # }) 135 | /// ``` 136 | /// 137 | /// ```rust 138 | /// use scryfall::ruling::Ruling; 139 | /// use futures::stream::StreamExt; 140 | /// use futures::future; 141 | /// # tokio_test::block_on(async { 142 | /// assert!( 143 | /// Ruling::mtgo_id(57934) 144 | /// .await 145 | /// .unwrap() 146 | /// .into_stream_buffered(10) 147 | /// .map(Result::unwrap) 148 | /// .any(|r| future::ready(r.comment.ends_with("You read the whole contract, right?"))) 149 | /// .await 150 | /// ); 151 | /// # }) 152 | /// ``` 153 | pub async fn mtgo_id(id: usize) -> crate::Result> { 154 | Uri::from( 155 | CARDS_URL 156 | .join("mtgo/")? 157 | .join(&format!("{}/", id))? 158 | .join(API_RULING)?, 159 | ) 160 | .fetch_iter() 161 | .await 162 | } 163 | 164 | /// Returns rulings for a card with the given Magic: The Gathering Arena ID. 165 | /// 166 | /// ```rust 167 | /// use scryfall::ruling::Ruling; 168 | /// use futures::stream::StreamExt; 169 | /// use futures::future; 170 | /// # tokio_test::block_on(async { 171 | /// assert!( 172 | /// Ruling::arena_id(67462) 173 | /// .await 174 | /// .unwrap() 175 | /// .into_stream() 176 | /// .map(Result::unwrap) 177 | /// .any(|r| { 178 | /// future::ready(r.comment 179 | /// .starts_with("Once a chapter ability has triggered,")) 180 | /// }) 181 | /// .await 182 | /// ); 183 | /// # }) 184 | /// ``` 185 | /// 186 | /// ```rust 187 | /// use scryfall::ruling::Ruling; 188 | /// use futures::stream::StreamExt; 189 | /// use futures::future; 190 | /// # tokio_test::block_on(async { 191 | /// assert!( 192 | /// Ruling::arena_id(67462) 193 | /// .await 194 | /// .unwrap() 195 | /// .into_stream_buffered(10) 196 | /// .map(Result::unwrap) 197 | /// .any(|r| { 198 | /// future::ready(r.comment 199 | /// .starts_with("Once a chapter ability has triggered,")) 200 | /// }) 201 | /// .await 202 | /// ); 203 | /// # }) 204 | /// ``` 205 | pub async fn arena_id(id: usize) -> crate::Result> { 206 | Uri::from( 207 | CARDS_URL 208 | .join("arena/")? 209 | .join(&format!("{}/", id))? 210 | .join(API_RULING)?, 211 | ) 212 | .fetch_iter() 213 | .await 214 | } 215 | 216 | /// Returns a List of rulings for the card with the given set code and 217 | /// collector number. 218 | /// 219 | /// # Examples 220 | /// ```rust 221 | /// use scryfall::ruling::Ruling; 222 | /// use futures::stream::StreamExt; 223 | /// use futures::future; 224 | /// # tokio_test::block_on(async { 225 | /// assert!( 226 | /// Ruling::set_and_number("bfz", 17) 227 | /// .await 228 | /// .unwrap() 229 | /// .into_stream() 230 | /// .map(Result::unwrap) 231 | /// .any(|r| future::ready(dbg!(dbg!(r.comment) == "Yes, your opponent can't even. We know."))) 232 | /// .await, 233 | /// ); 234 | /// # }) 235 | /// ``` 236 | /// 237 | /// ```rust 238 | /// use scryfall::ruling::Ruling; 239 | /// use futures::stream::StreamExt; 240 | /// use futures::future; 241 | /// # tokio_test::block_on(async { 242 | /// assert!( 243 | /// Ruling::set_and_number("bfz", 17) 244 | /// .await 245 | /// .unwrap() 246 | /// .into_stream_buffered(10) 247 | /// .map(Result::unwrap) 248 | /// .any(|r| future::ready(r.comment == "Yes, your opponent can't even. We know.")) 249 | /// .await 250 | /// ); 251 | /// # }) 252 | /// ``` 253 | pub async fn set_and_number(set: &str, number: u32) -> crate::Result> { 254 | Uri::from( 255 | CARDS_URL 256 | .join(&format!("{}/{}/", set, number))? 257 | .join(API_RULING)?, 258 | ) 259 | .fetch_iter() 260 | .await 261 | } 262 | 263 | /// Returns a List of rulings for a card with the given Scryfall ID. 264 | /// 265 | /// # Examples 266 | /// ```rust 267 | /// use scryfall::ruling::Ruling; 268 | /// use futures::stream::StreamExt; 269 | /// use futures::future; 270 | /// # tokio_test::block_on(async { 271 | /// assert!( 272 | /// Ruling::uuid("f2b9983e-20d4-4d12-9e2c-ec6d9a345787".parse().unwrap()) 273 | /// .await 274 | /// .unwrap() 275 | /// .into_stream() 276 | /// .map(Result::unwrap) 277 | /// .any(|r| future::ready(r.comment == "It must flip like a coin and not like a Frisbee.")) 278 | /// .await 279 | /// ); 280 | /// # }) 281 | /// ``` 282 | /// 283 | /// ```rust 284 | /// use scryfall::ruling::Ruling; 285 | /// use futures::stream::StreamExt; 286 | /// use futures::future; 287 | /// # tokio_test::block_on(async { 288 | /// assert!( 289 | /// Ruling::uuid("f2b9983e-20d4-4d12-9e2c-ec6d9a345787".parse().unwrap()) 290 | /// .await 291 | /// .unwrap() 292 | /// .into_stream_buffered(10) 293 | /// .map(Result::unwrap) 294 | /// .any(|r| future::ready(r.comment == "It must flip like a coin and not like a Frisbee.")) 295 | /// .await 296 | /// ); 297 | /// # }) 298 | /// ``` 299 | pub async fn uuid(id: Uuid) -> crate::Result> { 300 | Uri::from(CARDS_URL.join(&format!("{}/", id))?.join(API_RULING)?) 301 | .fetch_iter() 302 | .await 303 | } 304 | } 305 | -------------------------------------------------------------------------------- /src/search.rs: -------------------------------------------------------------------------------- 1 | //! This module provides an abstraction over the search parameters available in 2 | //! Scryfall. For a complete documentation, refer to the 3 | //! [official site](https://scryfall.com/docs/syntax). 4 | //! 5 | //! The [`Search`] trait defines a type that can be used to 6 | //! search for cards. This is implemented for some of the types 7 | //! provided by this module, and additionally implemented for string 8 | //! types allowing for custom queries. 9 | //! 10 | //! # Prelude 11 | //! 12 | //! For convenience, this crate provides the [`search::prelude`][prelude] 13 | //! module, which includes all the types and functions used by `search`. 14 | //! To use the prelude, import all of its members as in the following example. 15 | //! 16 | //! ```rust,no_run 17 | //! use scryfall::search::prelude::*; 18 | //! ``` 19 | //! 20 | //! # Queries 21 | //! 22 | //! The [`Query`][self::query::Query] object provides a mechanism for 23 | //! constructing simple and complex Scryfall queries. 24 | //! complex queries to Scryfall. 25 | use async_trait::async_trait; 26 | use url::Url; 27 | 28 | use crate::list::ListIter; 29 | use crate::Card; 30 | 31 | pub mod advanced; 32 | pub mod param; 33 | pub mod query; 34 | 35 | /// A type implementing `Search` can be turned into a Scryfall query. This is 36 | /// the argument type for [`Card::search`] and 37 | /// [`search_random`][Card::search_random]. 38 | /// 39 | /// The `scryfall` crate provides the type [`Query`][self::query::Query] for 40 | /// specifying search expressions. For advanced search, use 41 | /// [`SearchOptions`][self::advanced::SearchOptions] to specify sorting, 42 | /// unique rollup, and other options. 43 | /// 44 | /// The `Search` trait is implemented for `&str` and `String` as well, 45 | /// supporting custom searches using [Scryfall syntax](https://scryfall.com/docs/syntax). 46 | #[async_trait] 47 | pub trait Search { 48 | /// Write this search as the query for the given `Url`. 49 | fn write_query(&self, url: &mut Url) -> crate::Result<()>; 50 | 51 | /// Returns the constructed query string for testing purposes. 52 | #[cfg(test)] 53 | fn query_string(&self) -> crate::Result { 54 | let mut url = Url::parse("http://localhost")?; 55 | self.write_query(&mut url)?; 56 | Ok(url.query().unwrap_or_default().to_string()) 57 | } 58 | 59 | /// Convenience method for passing this object to [`Card::search`]. 60 | async fn search(&self) -> crate::Result> { 61 | Card::search(self).await 62 | } 63 | 64 | /// Convenience method for passing this object to [`Card::search_all`]. 65 | async fn search_all(&self) -> crate::Result> { 66 | Card::search_all(self).await 67 | } 68 | 69 | /// Convenience method for passing this object to [`Card::search_random`]. 70 | async fn random(&self) -> crate::Result { 71 | Card::search_random(self).await 72 | } 73 | } 74 | 75 | impl Search for &T { 76 | fn write_query(&self, url: &mut Url) -> crate::Result<()> { 77 | ::write_query(*self, url) 78 | } 79 | } 80 | 81 | impl Search for &mut T { 82 | fn write_query(&self, url: &mut Url) -> crate::Result<()> { 83 | ::write_query(*self, url) 84 | } 85 | } 86 | 87 | #[inline] 88 | fn write_query_string(query: &S, url: &mut Url) -> crate::Result<()> { 89 | url.query_pairs_mut() 90 | .append_pair("q", query.to_string().as_str()); 91 | Ok(()) 92 | } 93 | 94 | impl Search for str { 95 | fn write_query(&self, url: &mut Url) -> crate::Result<()> { 96 | write_query_string(self, url) 97 | } 98 | } 99 | 100 | impl Search for String { 101 | fn write_query(&self, url: &mut Url) -> crate::Result<()> { 102 | write_query_string(self, url) 103 | } 104 | } 105 | 106 | /// This module re-exports types used for card searches into a common 107 | /// place, so they can all be imported with a glob. 108 | /// 109 | /// ```rust,no_run 110 | /// use scryfall::search::prelude::*; 111 | /// ``` 112 | pub mod prelude { 113 | pub use super::advanced::{SearchOptions, SortDirection, SortOrder, UniqueStrategy}; 114 | pub use super::param::compare::{eq, gt, gte, lt, lte, neq}; 115 | pub use super::param::criteria::{CardIs, PrintingIs}; 116 | pub use super::param::value::{ 117 | artist, artist_count, banned, block, border_color, cheapest, cmc, collector_number, color, 118 | color_count, color_identity, color_identity_count, cube, date, devotion, eur, flavor_text, 119 | format, frame, full_oracle_text, game, illustration_count, in_game, in_language, in_rarity, 120 | in_set, in_set_type, keyword, language, loyalty, mana, name, oracle_text, 121 | paper_print_count, paper_set_count, pow_tou, power, print_count, produces, rarity, 122 | restricted, set, set_count, set_type, tix, toughness, type_line, usd, usd_foil, watermark, 123 | year, Devotion, NumProperty, Regex, 124 | }; 125 | pub use super::param::{exact, Param}; 126 | pub use super::query::{not, Query}; 127 | pub use super::Search; 128 | } 129 | 130 | #[cfg(test)] 131 | mod tests { 132 | use super::prelude::*; 133 | use crate::Card; 134 | 135 | use futures::stream::StreamExt; 136 | 137 | #[tokio::test] 138 | async fn basic_search() { 139 | let cards = SearchOptions::new() 140 | .query(Query::And(vec![ 141 | name("lightning"), 142 | name("helix"), 143 | cmc(eq(2)), 144 | ])) 145 | .unique(UniqueStrategy::Prints) 146 | .search() 147 | .await 148 | .unwrap() 149 | .into_stream() 150 | .map(|c| c.unwrap()) 151 | .collect::>() 152 | .await; 153 | 154 | assert!(cards.len() > 1); 155 | 156 | for card in cards { 157 | assert_eq!(card.name, "Lightning Helix") 158 | } 159 | } 160 | 161 | #[tokio::test] 162 | async fn basic_search_buffered() { 163 | let cards = SearchOptions::new() 164 | .query(Query::And(vec![ 165 | name("lightning"), 166 | name("helix"), 167 | cmc(eq(2)), 168 | ])) 169 | .unique(UniqueStrategy::Prints) 170 | .search() 171 | .await 172 | .unwrap() 173 | .into_stream_buffered(10) 174 | .map(|c| c.unwrap()) 175 | .collect::>() 176 | .await; 177 | 178 | assert!(cards.len() > 1); 179 | 180 | for card in cards { 181 | assert_eq!(card.name, "Lightning Helix") 182 | } 183 | } 184 | 185 | #[tokio::test] 186 | async fn random_works_with_search_options() { 187 | // `SearchOptions` can set more query params than the "cards/random" API method 188 | // accepts. Scryfall should ignore these and return a random card. 189 | assert!(SearchOptions::new() 190 | .query(keyword("storm")) 191 | .unique(UniqueStrategy::Art) 192 | .sort(SortOrder::Usd, SortDirection::Ascending) 193 | .extras(true) 194 | .multilingual(true) 195 | .variations(true) 196 | .random() 197 | .await 198 | .unwrap() 199 | .oracle_text 200 | .unwrap() 201 | .to_lowercase() 202 | .contains("storm")); 203 | } 204 | 205 | #[tokio::test] 206 | async fn finds_alpha_lotus() { 207 | let mut search = SearchOptions::new(); 208 | 209 | search 210 | .query(exact("Black Lotus")) 211 | .unique(UniqueStrategy::Prints) 212 | .sort(SortOrder::Released, SortDirection::Ascending); 213 | 214 | eprintln!("search query: {}", search.query_string().unwrap()); 215 | assert_eq!( 216 | Card::search(&search) 217 | .await 218 | .expect("search failed") 219 | .into_stream() 220 | .next() 221 | .await 222 | .expect("empty stream") 223 | .expect("deserialization failed") 224 | .set 225 | .to_string(), 226 | "lea", 227 | ); 228 | } 229 | 230 | #[tokio::test] 231 | async fn finds_alpha_lotus_buffered() { 232 | let mut search = SearchOptions::new(); 233 | 234 | search 235 | .query(exact("Black Lotus")) 236 | .unique(UniqueStrategy::Prints) 237 | .sort(SortOrder::Released, SortDirection::Ascending); 238 | 239 | eprintln!("search query: {}", search.query_string().unwrap()); 240 | assert_eq!( 241 | Card::search(&search) 242 | .await 243 | .expect("search failed") 244 | .into_stream_buffered(10) 245 | .next() 246 | .await 247 | .expect("empty stream") 248 | .expect("deserialization failed") 249 | .set 250 | .to_string(), 251 | "lea", 252 | ); 253 | } 254 | 255 | #[tokio::test] 256 | async fn rarity_comparison() { 257 | use crate::card::Rarity; 258 | // The cards with "Bonus" rarity (power nine in vma). 259 | let cards = SearchOptions::new() 260 | .query(rarity(gt(Rarity::Mythic))) 261 | .search() 262 | .await 263 | .unwrap() 264 | .into_stream() 265 | .collect::>() 266 | .await; 267 | 268 | assert!(cards.len() >= 9, "Couldn't find the Power Nine from VMA."); 269 | 270 | assert!(cards 271 | .into_iter() 272 | .map(|c| c.unwrap()) 273 | .all(|c| c.rarity > Rarity::Mythic)); 274 | } 275 | 276 | #[tokio::test] 277 | async fn rarity_comparison_buffered() { 278 | use crate::card::Rarity; 279 | // The cards with "Bonus" rarity (power nine in vma). 280 | let cards = SearchOptions::new() 281 | .query(rarity(gt(Rarity::Mythic))) 282 | .search() 283 | .await 284 | .expect("search failed") 285 | .into_stream_buffered(10) 286 | .collect::>() 287 | .await; 288 | 289 | assert!(cards.len() >= 9, "Couldn't find the Power Nine from VMA."); 290 | 291 | assert!( 292 | cards 293 | .into_iter() 294 | .map(|c| c.unwrap()) 295 | .all(|c| c.rarity > Rarity::Mythic), 296 | "rarity should above mythic" 297 | ); 298 | } 299 | 300 | #[tokio::test] 301 | async fn numeric_property_comparison() { 302 | let card = Card::search_random(Query::And(vec![ 303 | power(eq(NumProperty::Toughness)), 304 | pow_tou(eq(NumProperty::Cmc)), 305 | not(CardIs::Funny), 306 | not(CardIs::Transform), 307 | not(CardIs::Flip), 308 | not(CardIs::ModalDfc), 309 | ])) 310 | .await 311 | .unwrap(); 312 | 313 | let power = card 314 | .power 315 | .and_then(|s| s.parse::().ok()) 316 | .unwrap_or_default(); 317 | let toughness = card 318 | .toughness 319 | .and_then(|s| s.parse::().ok()) 320 | .unwrap_or_default(); 321 | 322 | assert_eq!(power, toughness); 323 | assert_eq!(power + toughness, card.cmc.unwrap_or_default() as u32); 324 | 325 | let card = Card::search(pow_tou(gt(NumProperty::Year))) 326 | .await 327 | .unwrap() 328 | .into_stream() 329 | .map(|c| c.unwrap()) 330 | .any(|c| async move { &c.name == "Infinity Elemental" }) 331 | .await; 332 | 333 | assert!(card); 334 | } 335 | 336 | #[tokio::test] 337 | async fn numeric_property_comparison_buffered() { 338 | let card = Card::search_random(Query::And(vec![ 339 | power(eq(NumProperty::Toughness)), 340 | pow_tou(eq(NumProperty::Cmc)), 341 | not(CardIs::Funny), 342 | ])) 343 | .await 344 | .unwrap(); 345 | 346 | let power = card 347 | .power 348 | .and_then(|s| s.parse::().ok()) 349 | .unwrap_or_default(); 350 | let toughness = card 351 | .toughness 352 | .and_then(|s| s.parse::().ok()) 353 | .unwrap_or_default(); 354 | 355 | assert_eq!( 356 | power, toughness, 357 | "power was not equal to toughness for card {}", 358 | card.name 359 | ); 360 | assert_eq!( 361 | power + toughness, 362 | card.cmc.unwrap_or_default() as u32, 363 | "power and toughness added was not equal to cmc for card {}", 364 | card.name 365 | ); 366 | 367 | let card = Card::search(pow_tou(gt(NumProperty::Year))) 368 | .await 369 | .unwrap() 370 | .into_stream_buffered(10) 371 | .map(|c| c.unwrap()) 372 | .any(|c| async move { &c.name == "Infinity Elemental" }) 373 | .await; 374 | 375 | assert!(card); 376 | } 377 | 378 | #[test] 379 | fn query_string_sanity_check() { 380 | let query = cmc(4).and(name("Yargle")); 381 | assert_eq!( 382 | query.query_string().unwrap(), 383 | "q=%28cmc%3A4+AND+name%3A%22Yargle%22%29" 384 | ); 385 | } 386 | } 387 | -------------------------------------------------------------------------------- /src/search/advanced.rs: -------------------------------------------------------------------------------- 1 | //! This module provides facilities for advanced search. 2 | //! See the [`SearchOptions`] type for more details. 3 | 4 | use serde::{Serialize, Serializer}; 5 | use url::Url; 6 | 7 | use crate::search::query::Query; 8 | use crate::search::Search; 9 | 10 | /// Advanced searching options for Scryfall, including unique de-duplication 11 | /// strategy, sort order, page number, and any extras to include. For 12 | /// documentation on each option, refer to this struct's methods. 13 | /// 14 | /// For more information, refer to the [official docs](https://scryfall.com/docs/api/cards/search). 15 | #[derive(Serialize, Default, Debug)] 16 | pub struct SearchOptions { 17 | #[serde(skip_serializing_if = "is_default")] 18 | unique: UniqueStrategy, 19 | #[serde(skip_serializing_if = "is_default")] 20 | order: SortOrder, 21 | #[serde(skip_serializing_if = "is_default")] 22 | dir: SortDirection, 23 | #[serde(skip_serializing_if = "is_default")] 24 | page: usize, 25 | #[serde(skip_serializing_if = "is_default")] 26 | include_extras: bool, 27 | #[serde(skip_serializing_if = "is_default")] 28 | include_multilingual: bool, 29 | #[serde(skip_serializing_if = "is_default")] 30 | include_variations: bool, 31 | #[serde(rename = "q", serialize_with = "serialize_query")] 32 | query: Query, 33 | } 34 | 35 | fn is_default(value: &T) -> bool { 36 | value == &Default::default() 37 | } 38 | 39 | fn serialize_query(query: &Query, serializer: S) -> Result { 40 | query.to_string().serialize(serializer) 41 | } 42 | 43 | impl Search for SearchOptions { 44 | fn write_query(&self, url: &mut Url) -> crate::Result<()> { 45 | self.serialize(serde_urlencoded::Serializer::new( 46 | &mut url.query_pairs_mut(), 47 | ))?; 48 | Ok(()) 49 | } 50 | } 51 | 52 | impl SearchOptions { 53 | /// Constructs a new `SearchOptions` with default values and an empty query. 54 | pub fn new() -> Self { 55 | SearchOptions { 56 | page: 1, 57 | ..Default::default() 58 | } 59 | } 60 | 61 | /// Constructs a new `SearchOptions` with default values and the specified 62 | /// query. 63 | pub fn with_query(query: Query) -> Self { 64 | SearchOptions { 65 | query, 66 | ..Self::new() 67 | } 68 | } 69 | 70 | /// Sets the query to use for this search. 71 | pub fn query(&mut self, query: Query) -> &mut Self { 72 | self.query = query; 73 | self 74 | } 75 | 76 | /// Sets the page number to start with. Page 0 is equivalent to page 1. 77 | pub fn page(&mut self, page: usize) -> &mut Self { 78 | self.page = page; 79 | self 80 | } 81 | 82 | /// Sets the strategy for omitting similar cards. 83 | pub fn unique(&mut self, unique: UniqueStrategy) -> &mut Self { 84 | self.unique = unique; 85 | self 86 | } 87 | 88 | /// Sets the sort order and direction for returned cards. 89 | #[inline] 90 | pub fn sort(&mut self, order: SortOrder, dir: SortDirection) -> &mut Self { 91 | self.order(order).direction(dir) 92 | } 93 | 94 | /// Sets the sort order for returned cards. 95 | pub fn order(&mut self, order: SortOrder) -> &mut Self { 96 | self.order = order; 97 | self 98 | } 99 | 100 | /// Sets the sort direction for returned cards. 101 | pub fn direction(&mut self, dir: SortDirection) -> &mut Self { 102 | self.dir = dir; 103 | self 104 | } 105 | 106 | /// If true, extra cards (tokens, planes, etc) will be included. 107 | pub fn extras(&mut self, include_extras: bool) -> &mut Self { 108 | self.include_extras = include_extras; 109 | self 110 | } 111 | 112 | /// If true, cards in every language supported by Scryfall will be included. 113 | pub fn multilingual(&mut self, include_multilingual: bool) -> &mut Self { 114 | self.include_multilingual = include_multilingual; 115 | self 116 | } 117 | 118 | /// If true, rare care variants will be included, like the 119 | /// [Hairy Runesword](https://scryfall.com/card/drk/107%E2%80%A0/runesword). 120 | pub fn variations(&mut self, include_variations: bool) -> &mut Self { 121 | self.include_variations = include_variations; 122 | self 123 | } 124 | } 125 | 126 | /// The unique parameter specifies if Scryfall should remove “duplicate” results 127 | /// in your query. 128 | #[derive(Serialize, Copy, Clone, Eq, PartialEq, Hash, Debug)] 129 | #[serde(rename_all = "lowercase")] 130 | #[derive(Default)] 131 | pub enum UniqueStrategy { 132 | /// Removes duplicate gameplay objects (cards that share a name and have the 133 | /// same functionality). For example, if your search matches more than 134 | /// one print of Pacifism, only one copy of Pacifism will be returned. 135 | #[default] 136 | Cards, 137 | /// Returns only one copy of each unique artwork for matching cards. For 138 | /// example, if your search matches more than one print of Pacifism, one 139 | /// card with each different illustration for Pacifism will be returned, 140 | /// but any cards that duplicate artwork already in the results will 141 | /// be omitted. 142 | Art, 143 | /// Returns all prints for all cards matched (disables rollup). For example, 144 | /// if your search matches more than one print of Pacifism, all matching 145 | /// prints will be returned. 146 | Prints, 147 | } 148 | 149 | /// The order parameter determines how Scryfall should sort the returned cards. 150 | #[derive(Serialize, Copy, Clone, Eq, PartialEq, Hash, Debug)] 151 | #[serde(rename_all = "lowercase")] 152 | #[derive(Default)] 153 | pub enum SortOrder { 154 | /// Sort cards by name, A → Z 155 | #[default] 156 | Name, 157 | /// Sort cards by their set and collector number: AAA/#1 → ZZZ/#999 158 | Set, 159 | /// Sort cards by their release date: Newest → Oldest 160 | Released, 161 | /// Sort cards by their rarity: Common → Mythic 162 | Rarity, 163 | /// Sort cards by their color and color identity: WUBRG → multicolor → 164 | /// colorless 165 | Color, 166 | /// Sort cards by their lowest known U.S. Dollar price: 0.01 → highest, null 167 | /// last 168 | Usd, 169 | /// Sort cards by their lowest known TIX price: 0.01 → highest, null last 170 | Tix, 171 | /// Sort cards by their lowest known Euro price: 0.01 → highest, null last 172 | Eur, 173 | /// Sort cards by their converted mana cost: 0 → highest 174 | Cmc, 175 | /// Sort cards by their power: null → highest 176 | Power, 177 | /// Sort cards by their toughness: null → highest 178 | Toughness, 179 | /// Sort cards by their EDHREC ranking: lowest → highest 180 | Edhrec, 181 | /// Sort cards by their front-side artist name: A → Z 182 | Artist, 183 | } 184 | 185 | /// Which direction the sorting should occur: 186 | #[derive(Serialize, Copy, Clone, Eq, PartialEq, Hash, Debug)] 187 | #[serde(rename_all = "lowercase")] 188 | #[derive(Default)] 189 | pub enum SortDirection { 190 | /// Scryfall will automatically choose the most intuitive direction to sort 191 | #[default] 192 | Auto, 193 | /// Sort ascending (flip the direction of the arrows in [`SortMethod`]) 194 | /// 195 | /// [`SortMethod`]: enum.SortMethod.html 196 | #[serde(rename = "asc")] 197 | Ascending, 198 | /// Sort descending (flip the direction of the arrows in [`SortMethod`]) 199 | /// 200 | /// [`SortMethod`]: enum.SortMethod.html 201 | #[serde(rename = "desc")] 202 | Descending, 203 | } 204 | -------------------------------------------------------------------------------- /src/search/param.rs: -------------------------------------------------------------------------------- 1 | //! This module defines [`Param`], which represents a single search parameter 2 | //! for a Scryfall query. For combinations of parameters, see the 3 | //! [`Query`][crate::search::query] module. 4 | //! 5 | //! There are two kinds of `Param`: boolean criteria, and parameters 6 | //! that take a value. 7 | //! 8 | //! Cards and printings are tagged with many different types of criteria 9 | //! by Scryfall. Each of these represents a boolean property that the 10 | //! card either has or does not. Searching by a criterion will only match 11 | //! cards that have the flag. For example, 12 | //! ['is:firstprint'][self::criteria::PrintingIs::FirstPrint] matches only 13 | //! the first printing of a card, and 14 | //! ['has:watermark'][self::criteria::PrintingIs::Watermark] matches printings 15 | //! which have a watermark. For a list of all available criteria, see the 16 | //! [`criteria`] module. 17 | //! 18 | //! The rest of the search parameters are comprised of a name and a value, such 19 | //! as ['name:lightning'][self::value::name] or 20 | //! ['year:1995'][self::value::year]. All available value parameters are all 21 | //! available as helper functions defined in the [`value`] module. 22 | use std::fmt; 23 | 24 | use url::Url; 25 | 26 | use self::compare::CompareOp; 27 | use self::criteria::Criterion; 28 | use self::value::ValueKind; 29 | use crate::search::query::Query; 30 | use crate::search::Search; 31 | 32 | pub mod compare; 33 | pub mod criteria; 34 | pub mod value; 35 | 36 | /// A filter to provide to the search to reduce the cards returned. 37 | /// 38 | /// A `Param` can be an [exact card name][exact()], a [`Criterion`], or a 39 | /// comparison of parameter values. 40 | /// 41 | /// Usually `Param` does not need to be used directly, but instead is wrapped 42 | /// in a [`Query`] so it can be combined with other `Param`s. 43 | /// 44 | /// For more information on available parameters, refer to the 45 | /// [official docs](https://scryfall.com/docs/syntax). 46 | #[derive(Clone, Eq, PartialEq, Hash, Debug)] 47 | pub struct Param(ParamImpl); 48 | 49 | impl fmt::Display for Param { 50 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 51 | fmt::Display::fmt(&self.0, f) 52 | } 53 | } 54 | 55 | impl From for Param { 56 | fn from(criterion: Criterion) -> Self { 57 | Param::criterion(criterion) 58 | } 59 | } 60 | 61 | impl Search for Param { 62 | fn write_query(&self, url: &mut Url) -> crate::Result<()> { 63 | super::write_query_string(self, url) 64 | } 65 | } 66 | 67 | impl Param { 68 | fn exact(value: impl Into) -> Self { 69 | Param(ParamImpl::ExactName(value.into())) 70 | } 71 | 72 | fn criterion(criterion: Criterion) -> Self { 73 | Param(ParamImpl::Criterion(criterion)) 74 | } 75 | 76 | fn value(kind: ValueKind, value: impl ToString) -> Self { 77 | Param(ParamImpl::Value(kind, value.to_string())) 78 | } 79 | 80 | fn comparison(kind: ValueKind, op: CompareOp, value: impl ToString) -> Self { 81 | Param(ParamImpl::Comparison(kind, op, value.to_string())) 82 | } 83 | } 84 | 85 | #[derive(Clone, Eq, PartialEq, Hash, Debug)] 86 | enum ParamImpl { 87 | ExactName(String), 88 | Criterion(Criterion), 89 | Value(ValueKind, String), 90 | Comparison(ValueKind, CompareOp, String), 91 | } 92 | 93 | impl fmt::Display for ParamImpl { 94 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 95 | match self { 96 | ParamImpl::Criterion(prop) => write!(f, "{}", prop), 97 | ParamImpl::ExactName(name) => write!(f, "!\"{}\"", name), 98 | ParamImpl::Value(kind, value) => kind.fmt_value(value.as_str(), f), 99 | ParamImpl::Comparison(kind, op, value) => kind.fmt_comparison(*op, value, f), 100 | } 101 | } 102 | } 103 | 104 | /// Matches a card whose name is exactly `name`. 105 | pub fn exact(name: impl Into) -> Query { 106 | Query::Param(Param::exact(name)) 107 | } 108 | -------------------------------------------------------------------------------- /src/search/param/compare.rs: -------------------------------------------------------------------------------- 1 | //! This module defines the [`Compare`] type, which represents a comparison 2 | //! operator and right-hand side of a comparison expression. Certain 3 | //! [`ParamValue`] subtraits are implemented for `Compare`, depending on 4 | //! whether Scryfall syntax supports comparing for that. 5 | //! 6 | //! To construct a `Compare` instance, use the helper functions defined in this 7 | //! module: [`lt`], [`lte`], [`gt`], [`gte`], [`eq`], and [`neq`]. 8 | 9 | use std::fmt; 10 | 11 | use crate::search::param::value::{ParamValue, ValueKind}; 12 | use crate::search::param::Param; 13 | 14 | /// An operator and RHS for a comparison expression of a parameter. 15 | /// To construct an instance, use one of the helper functions from the 16 | /// [`compare`][self] module: [`lt`], [`lte`], [`gt`], [`gte`], [`eq`], or 17 | /// [`neq`]. 18 | /// 19 | /// # Example 20 | /// 21 | /// ```rust 22 | /// # use scryfall::search::prelude::*; 23 | /// # tokio_test::block_on(async { 24 | /// let query = cmc(gte(5)).and(type_line("planeswalker")); 25 | /// let card = query.random().await.unwrap(); 26 | /// 27 | /// assert!(card.cmc.unwrap() as u32 >= 5); 28 | /// assert!(card.type_line.unwrap().to_lowercase().contains("planeswalker")); 29 | /// # }) 30 | /// ``` 31 | #[derive(Clone, Eq, PartialEq, Hash, Debug)] 32 | pub struct Compare { 33 | op: CompareOp, 34 | value: T, 35 | } 36 | 37 | impl fmt::Display for Compare { 38 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 39 | write!(f, "{}{}", compare_op_str(Some(self.op)), &self.value) 40 | } 41 | } 42 | 43 | impl ParamValue for Compare { 44 | fn into_param(self, kind: ValueKind) -> Param { 45 | Param::comparison(kind, self.op, self.value) 46 | } 47 | } 48 | 49 | #[derive(Copy, Clone, Eq, PartialEq, Hash, Debug)] 50 | pub(super) enum CompareOp { 51 | Lte, 52 | Lt, 53 | Gte, 54 | Gt, 55 | Eq, 56 | Neq, 57 | } 58 | 59 | pub(super) const fn compare_op_str(op: Option) -> &'static str { 60 | match op { 61 | None => ":", 62 | Some(CompareOp::Lte) => "<=", 63 | Some(CompareOp::Lt) => "<", 64 | Some(CompareOp::Gte) => ">=", 65 | Some(CompareOp::Gt) => ">", 66 | Some(CompareOp::Eq) => "=", 67 | Some(CompareOp::Neq) => "!=", 68 | } 69 | } 70 | 71 | macro_rules! compare_fns { 72 | ($( 73 | $(#[$($attr:meta)*])* 74 | $meth:ident => $Variant:ident, 75 | )*) => { 76 | $( 77 | $(#[$($attr)*])* 78 | pub fn $meth(x: T) -> Compare { 79 | Compare { 80 | op: CompareOp::$Variant, 81 | value: x, 82 | } 83 | } 84 | )* 85 | }; 86 | } 87 | 88 | compare_fns! { 89 | #[doc = "Less than `x`."] 90 | lt => Lt, 91 | #[doc = "Less than or equal to `x`."] 92 | lte => Lte, 93 | #[doc = "Greater than or equal to `x`."] 94 | gte => Gte, 95 | #[doc = "Greater than `x`."] 96 | gt => Gt, 97 | #[doc = "Equal to `x`."] 98 | eq => Eq, 99 | #[doc = "Not equal to `x`."] 100 | neq => Neq, 101 | } 102 | -------------------------------------------------------------------------------- /src/search/query.rs: -------------------------------------------------------------------------------- 1 | //! This module defines the [`Query`] type, which allows for combinations 2 | //! of [`Param`]s. 3 | 4 | use std::fmt; 5 | 6 | use url::Url; 7 | 8 | use crate::search::param::Param; 9 | use crate::search::Search; 10 | 11 | /// A search query, composed of search parameters and boolean operations. 12 | /// 13 | /// `Query` is an expression tree, supporting `AND`, `OR`, and `NOT` operations, 14 | /// with the [`And`][Query::And], [`Or`][Query::Or], and [`Not`][Query::Not] 15 | /// variants respectively. Leaf variants are [`Param`][`Query::Param`] and 16 | /// [`Custom`][Query::Custom]. 17 | /// 18 | /// # Examples 19 | /// ```rust 20 | /// # use scryfall::search::prelude::*; 21 | /// # fn main() -> scryfall::Result<()> { 22 | /// use scryfall::card::Rarity; 23 | /// # tokio_test::block_on(async { 24 | /// let one_odd_eldrazi = Query::And(vec![ 25 | /// Query::Or(vec![power(9), toughness(9)]), 26 | /// Query::Custom("t:eldrazi".to_string()), 27 | /// set("bfz"), // A `Param` variant. 28 | /// rarity(Rarity::Mythic), // A `Param` variant. 29 | /// CardIs::OddCmc.into(), 30 | /// ]) 31 | /// .search() 32 | /// .await? 33 | /// .next() 34 | /// .await 35 | /// .unwrap()?; 36 | /// assert_eq!(one_odd_eldrazi.name, "Void Winnower"); 37 | /// # Ok(()) 38 | /// # }) 39 | /// # } 40 | /// ``` 41 | /// 42 | /// For information on search parameters, see the 43 | /// [`param`][crate::search::param] module. 44 | #[derive(Clone, PartialEq, Debug)] 45 | pub enum Query { 46 | /// The returned cards must match all of the sub-queries. 47 | And(Vec), 48 | /// The returned cards must match at least one of the sub-queries. 49 | Or(Vec), 50 | /// The returned cards must not match the sub-query. 51 | Not(Box), 52 | /// The returned cards must match the specified search `Param`. 53 | Param(Param), 54 | /// A custom query, in valid [Scryfall syntax](https://scryfall.com/docs/syntax). 55 | /// 56 | /// *Note*: This variant is provided so that users of this crate can use the 57 | /// latest search features on scryfall.com without waiting for the crate 58 | /// to be updated. If you encounter a situation where this must be used, 59 | /// please [file an issue](https://github.com/mendess/scryfall-rs/issues/new). 60 | Custom(String), 61 | } 62 | 63 | impl Default for Query { 64 | fn default() -> Self { 65 | Query::And(vec![]) 66 | } 67 | } 68 | 69 | impl fmt::Display for Query { 70 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 71 | let (exprs, sep) = match &self { 72 | Query::And(exprs) => (exprs, " AND "), 73 | Query::Or(exprs) => (exprs, " OR "), 74 | Query::Not(expr) => return write!(f, "-{}", expr), 75 | Query::Param(param) => return write!(f, "{}", param), 76 | Query::Custom(expr) => return write!(f, "({})", expr), 77 | }; 78 | 79 | use itertools::Itertools; 80 | // If `exprs` is empty, the output is '()', which will be ignored. 81 | write!(f, "({})", exprs.iter().format(sep)) 82 | } 83 | } 84 | 85 | impl Search for Query { 86 | fn write_query(&self, url: &mut Url) -> crate::Result<()> { 87 | super::write_query_string(self, url) 88 | } 89 | } 90 | 91 | impl From for Query { 92 | fn from(param: Param) -> Self { 93 | Query::Param(param) 94 | } 95 | } 96 | 97 | macro_rules! impl_and_or { 98 | ($( 99 | $(#[$($attr:meta)*])* 100 | $meth:ident($Var:ident), 101 | )*) => { 102 | $( 103 | $(#[$($attr)*])* 104 | pub fn $meth(self, other: impl Into) -> Self { 105 | match (self, other.into()) { 106 | (Query::$Var(mut a_list), Query::$Var(mut b_list)) => { 107 | a_list.append(&mut b_list); 108 | Query::$Var(a_list) 109 | }, 110 | (Query::$Var(mut a_list), b) => { 111 | a_list.push(b); 112 | Query::$Var(a_list) 113 | }, 114 | (a, Query::$Var(mut b_list)) => { 115 | b_list.insert(0, a); 116 | Query::$Var(b_list) 117 | }, 118 | (a, b) => Query::$Var(vec![a, b]), 119 | } 120 | } 121 | )* 122 | }; 123 | } 124 | 125 | impl Query { 126 | impl_and_or! { 127 | #[doc = "Combines `self` with `other` using the boolean AND operation."] 128 | and(And), 129 | #[doc = "Combines `self` with `other` using the boolean OR operation."] 130 | or(Or), 131 | } 132 | } 133 | 134 | /// Negates the specified `query`. 135 | pub fn not(query: impl Into) -> Query { 136 | match query.into() { 137 | Query::Not(q) => *q, 138 | q => Query::Not(Box::new(q)), 139 | } 140 | } 141 | 142 | #[cfg(test)] 143 | mod tests { 144 | use futures::StreamExt; 145 | 146 | use super::*; 147 | use crate::search::prelude::*; 148 | 149 | #[tokio::test] 150 | async fn even_power() -> crate::Result<()> { 151 | // Scryfall doesn't support "power:even", so let's do it manually. 152 | let normal_creatures = type_line("Creature").and(not(CardIs::Funny)); 153 | let highest_power: u32 = SearchOptions::new() 154 | .query(normal_creatures.clone()) 155 | .sort(SortOrder::Power, SortDirection::Descending) 156 | .search() 157 | .await? 158 | .into_stream() 159 | .next() 160 | .await 161 | .unwrap()? 162 | .power 163 | .and_then(|pow| pow.parse().ok()) 164 | .unwrap_or(0); 165 | let query = normal_creatures.and(Query::Or((0..=highest_power).map(power).collect())); 166 | // There are at least 1000 cards with even power. 167 | assert!(query.search().await.unwrap().size_hint().0 > 1000); 168 | Ok(()) 169 | } 170 | 171 | #[tokio::test] 172 | async fn even_power_buffered() -> crate::Result<()> { 173 | // Scryfall doesn't support "power:even", so let's do it manually. 174 | let normal_creatures = type_line("Creature").and(not(CardIs::Funny)); 175 | let highest_power: u32 = SearchOptions::new() 176 | .query(normal_creatures.clone()) 177 | .sort(SortOrder::Power, SortDirection::Descending) 178 | .search() 179 | .await? 180 | .into_stream_buffered(10) 181 | .next() 182 | .await 183 | .unwrap()? 184 | .power 185 | .and_then(|pow| pow.parse().ok()) 186 | .unwrap_or(0); 187 | let query = normal_creatures.and(Query::Or((0..=highest_power).map(power).collect())); 188 | // There are at least 1000 cards with even power. 189 | assert!(query.search().await.unwrap().size_hint().0 > 1000); 190 | Ok(()) 191 | } 192 | } 193 | -------------------------------------------------------------------------------- /src/set.rs: -------------------------------------------------------------------------------- 1 | //! A Set object represents a group of related Magic cards. All Card objects on 2 | //! Scryfall belong to exactly one set. 3 | //! 4 | //! Due to Magic’s long and complicated history, Scryfall includes many 5 | //! un-official sets as a way to group promotional or outlier cards together. 6 | //! Such sets will likely have a code that begins with `p` or `t`, such as 7 | //! `pcel` or `tori`. 8 | //! 9 | //! Official sets always have a three-letter set code, such as `zen`. 10 | mod set_code; 11 | mod set_type; 12 | 13 | use chrono::NaiveDate; 14 | use percent_encoding::{percent_encode, NON_ALPHANUMERIC}; 15 | use serde::{Deserialize, Serialize}; 16 | use uuid::Uuid; 17 | 18 | pub use self::set_code::SetCode; 19 | pub use self::set_type::SetType; 20 | use crate::card::Card; 21 | use crate::list::{List, ListIter}; 22 | use crate::uri::Uri; 23 | use crate::util::SETS_URL; 24 | 25 | /// A Set object containing all fields that `scryfall` provides. 26 | /// 27 | /// For more details visit the [official docs](https://scryfall.com/docs/api/sets). 28 | #[derive(Serialize, Deserialize, Clone, PartialEq, Debug)] 29 | #[cfg_attr(test, serde(deny_unknown_fields))] 30 | #[non_exhaustive] 31 | pub struct Set { 32 | /// A unique ID for this set on Scryfall that will not change. 33 | pub id: Uuid, 34 | 35 | /// The unique three to five-letter code for this set. 36 | pub code: SetCode, 37 | 38 | /// The unique code for this set on MTGO, which may differ from the regular 39 | /// code. 40 | pub mtgo_code: Option, 41 | 42 | /// The unique code for this set on Arena, which may differ from the regular code. 43 | pub arena_code: Option, 44 | 45 | /// This set’s ID on TCGplayer’s API, also known as the groupId. 46 | pub tcgplayer_id: Option, 47 | 48 | /// The English name of the set. 49 | pub name: String, 50 | 51 | /// A computer-readable classification for this set. 52 | pub set_type: SetType, 53 | 54 | /// The date the set was released or the first card was printed in the set 55 | /// (in GMT-8 Pacific time). 56 | pub released_at: Option, 57 | 58 | /// The block code for this set, if any. 59 | pub block_code: Option, 60 | 61 | /// The block or group name code for this set, if any. 62 | pub block: Option, 63 | 64 | /// The set code for the parent set, if any. promo and token sets often have 65 | /// a parent set. 66 | pub parent_set_code: Option, 67 | 68 | /// The number of cards in this set. 69 | pub card_count: usize, 70 | 71 | /// The denominator for the set’s printed collector numbers. 72 | pub printed_size: Option, 73 | 74 | /// True if this set was only released in a video game. 75 | pub digital: bool, 76 | 77 | /// True if this set contains only foil cards. 78 | pub foil_only: bool, 79 | 80 | /// True if this set contains only nonfoil cards. 81 | pub nonfoil_only: bool, 82 | 83 | /// A link to this set’s permapage on Scryfall’s website. 84 | pub scryfall_uri: String, 85 | 86 | /// A link to this set object on Scryfall’s API. 87 | pub uri: Uri, 88 | 89 | /// A URI to an SVG file for this set’s icon on Scryfall’s CDN. Hotlinking 90 | /// this image isn’t recommended, because it may change slightly over time. 91 | /// You should download it and use it locally for your particular user 92 | /// interface needs. 93 | pub icon_svg_uri: String, 94 | 95 | /// A Scryfall API URI that you can request to begin paginating over the 96 | /// cards in this set. 97 | pub search_uri: Uri>, 98 | 99 | #[cfg(test)] 100 | #[serde(rename = "object")] 101 | _object: String, 102 | } 103 | 104 | impl Set { 105 | /// Returns a [`ListIter`] of all the sets in the `scryfall` database. 106 | /// 107 | /// # Examples 108 | /// ```rust 109 | /// use scryfall::set::Set; 110 | /// # tokio_test::block_on(async { 111 | /// let sets = Set::all().await.unwrap().into_inner().collect::>(); 112 | /// assert!(sets.len() > 0); 113 | /// # }) 114 | /// ``` 115 | pub async fn all() -> crate::Result> { 116 | let mut url = SETS_URL.clone(); 117 | url.query_pairs_mut().append_pair("page", "1"); 118 | Uri::from(url).fetch_iter().await 119 | } 120 | 121 | /// Returns a `Set` with the given set code. 122 | /// 123 | /// The code can be either the `code` or the `mtgo_code` for the set. 124 | /// 125 | /// # Examples 126 | /// ```rust 127 | /// use scryfall::set::Set; 128 | /// # tokio_test::block_on(async { 129 | /// assert_eq!(Set::code("mmq").await.unwrap().name, "Mercadian Masques") 130 | /// # }) 131 | /// ``` 132 | pub async fn code(code: &str) -> crate::Result { 133 | Uri::from(SETS_URL.join(&percent_encode(code.as_bytes(), NON_ALPHANUMERIC).to_string())?) 134 | .fetch() 135 | .await 136 | } 137 | 138 | /// Returns a `Set` with the given `tcgplayer_id`. 139 | /// 140 | /// Also known as the `groupId` on [TCGplayer’s API](https://docs.tcgplayer.com/docs). 141 | /// 142 | /// # Examples 143 | /// 144 | /// ```rust 145 | /// use scryfall::set::Set; 146 | /// # tokio_test::block_on(async { 147 | /// assert_eq!(Set::tcgplayer(1909).await.unwrap().name, "Amonkhet Invocations") 148 | /// # }) 149 | /// ``` 150 | pub async fn tcgplayer(code: T) -> crate::Result { 151 | Uri::from( 152 | SETS_URL 153 | .join("tcgplayer/")? 154 | .join(&percent_encode(code.to_string().as_bytes(), NON_ALPHANUMERIC).to_string())?, 155 | ) 156 | .fetch() 157 | .await 158 | } 159 | 160 | /// Returns a Set with the given Scryfall `uuid`. 161 | /// 162 | /// # Examples 163 | /// ```rust 164 | /// use scryfall::set::Set; 165 | /// # tokio_test::block_on(async { 166 | /// assert_eq!( 167 | /// Set::uuid("2ec77b94-6d47-4891-a480-5d0b4e5c9372".parse().unwrap()) 168 | /// .await 169 | /// .unwrap() 170 | /// .name, 171 | /// "Ultimate Masters" 172 | /// ) 173 | /// # }) 174 | /// ``` 175 | pub async fn uuid(uuid: Uuid) -> crate::Result { 176 | Uri::from(SETS_URL.join(&uuid.to_string())?).fetch().await 177 | } 178 | 179 | /// Returns an iterator over the cards of the set. 180 | pub async fn cards(&self) -> crate::Result> { 181 | self.search_uri.fetch_iter().await 182 | } 183 | } 184 | -------------------------------------------------------------------------------- /src/set/set_code.rs: -------------------------------------------------------------------------------- 1 | //! This module defines a set code. 2 | use std::convert::{AsRef, TryFrom}; 3 | use std::{fmt, str}; 4 | 5 | use serde::de::{self, Deserializer, Visitor}; 6 | use serde::ser::Serializer; 7 | use serde::{Deserialize, Serialize}; 8 | use tinyvec::ArrayVec; 9 | 10 | /// A 3 to 6 letter set code, like 'war' for 'War of the Spark'. 11 | #[derive(Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug)] 12 | pub struct SetCode(ArrayVec<[u8; 6]>); 13 | 14 | #[allow(dead_code)] 15 | impl SetCode { 16 | /// Creates a set code from a str. 17 | /// 18 | /// Valid set codes are ascii between 3 and 6 letters long. If any of these 19 | /// conditions fails, the conversion fails. 20 | /// 21 | /// The error value is None if the `str` was not ascii, otherwise it holds 22 | /// the size of the `str`. 23 | /// 24 | /// ```rust 25 | /// use scryfall::set::SetCode; 26 | /// 27 | /// assert_eq!(SetCode::new("war").unwrap().as_ref(), "war") 28 | /// ``` 29 | pub fn new(code: &str) -> Result> { 30 | SetCode::try_from(code) 31 | } 32 | 33 | /// Returns a reference to the inner set code. 34 | pub fn get(&self) -> &str { 35 | // The inner code is always a valid utf8 str since it can 36 | // only be created from a valid &str. 37 | str::from_utf8(self.0.as_slice()).unwrap() 38 | } 39 | } 40 | 41 | impl TryFrom<&str> for SetCode { 42 | type Error = Option; 43 | 44 | /// See [`new`](#method.new) for documentation on why this might return an 45 | /// `Err`. 46 | fn try_from(code: &str) -> Result> { 47 | if !code.is_ascii() { 48 | return Err(None); 49 | } 50 | let code = code.as_bytes(); 51 | Ok(SetCode(match code.len() { 52 | 3..=6 => code.iter().cloned().collect(), 53 | invalid => return Err(Some(invalid)), 54 | })) 55 | } 56 | } 57 | 58 | impl AsRef for SetCode { 59 | fn as_ref(&self) -> &str { 60 | self.get() 61 | } 62 | } 63 | 64 | #[derive(Default)] 65 | struct SetCodeVisitor { 66 | size: Option, 67 | } 68 | 69 | impl Visitor<'_> for SetCodeVisitor { 70 | type Value = SetCode; 71 | 72 | fn expecting(&self, f: &mut fmt::Formatter) -> fmt::Result { 73 | match self.size { 74 | Some(size) => write!(f, "set code size between 3 and 6, found {}", size), 75 | None => write!(f, "set code to be ascii"), 76 | } 77 | } 78 | 79 | fn visit_str(mut self, s: &str) -> Result 80 | where 81 | E: de::Error, 82 | { 83 | SetCode::try_from(s).map_err(|size| { 84 | self.size = size; 85 | de::Error::invalid_value(de::Unexpected::Str(s), &self) 86 | }) 87 | } 88 | } 89 | 90 | impl<'de> Deserialize<'de> for SetCode { 91 | fn deserialize(deserializer: D) -> Result 92 | where 93 | D: Deserializer<'de>, 94 | { 95 | deserializer.deserialize_str(SetCodeVisitor::default()) 96 | } 97 | } 98 | 99 | impl Serialize for SetCode { 100 | fn serialize(&self, serializer: S) -> Result 101 | where 102 | S: Serializer, 103 | { 104 | serializer.serialize_str(self.get()) 105 | } 106 | } 107 | 108 | impl fmt::Display for SetCode { 109 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 110 | write!(f, "{}", self.get()) 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /src/set/set_type.rs: -------------------------------------------------------------------------------- 1 | //! Scryfall provides an overall categorization for each Set in the set_type 2 | //! property. 3 | use std::fmt; 4 | 5 | use serde::{Deserialize, Serialize}; 6 | 7 | /// Scryfall provides an overall categorization for each Set in the set_type 8 | /// property. 9 | #[derive(Serialize, Deserialize, Clone, Eq, PartialEq, Hash, Debug)] 10 | #[cfg_attr(not(feature = "unknown_variants"), derive(Copy))] 11 | #[cfg_attr( 12 | all( 13 | not(feature = "unknown_variants"), 14 | not(feature = "unknown_variants_slim") 15 | ), 16 | non_exhaustive 17 | )] 18 | #[cfg_attr(test, serde(deny_unknown_fields))] 19 | #[serde(rename_all = "snake_case")] 20 | pub enum SetType { 21 | /// A yearly Magic core set (Tenth Edition, etc) 22 | Core, 23 | /// A rotational expansion set in a block (Zendikar, etc) 24 | Expansion, 25 | /// A reprint set that contains no new cards (Modern Masters, etc) 26 | Masters, 27 | /// Masterpiece Series premium foil cards 28 | Masterpiece, 29 | /// From the Vault gift sets 30 | FromTheVault, 31 | /// Spellbook series gift sets 32 | Spellbook, 33 | /// Premium Deck Series decks 34 | PremiumDeck, 35 | /// Duel Decks 36 | DuelDeck, 37 | /// Special draft sets, like Conspiracy and Battlebond 38 | DraftInnovation, 39 | /// Magic Online treasure chest prize sets 40 | TreasureChest, 41 | /// Commander preconstructed decks 42 | Commander, 43 | /// Planechase sets 44 | Planechase, 45 | /// Archenemy sets 46 | Archenemy, 47 | /// Vanguard card sets 48 | Vanguard, 49 | /// A funny un-set or set with funny promos (Unglued, Happy Holidays, etc) 50 | Funny, 51 | /// A starter/introductory set (Portal, etc) 52 | Starter, 53 | /// A gift box set 54 | #[serde(rename = "box")] 55 | GiftBox, 56 | /// A set that contains purely promotional cards 57 | Promo, 58 | /// A set made up of tokens and emblems. 59 | Token, 60 | /// A set made up of gold-bordered, oversize, or trophy cards that are not 61 | /// legal 62 | Memorabilia, 63 | /// Alchemy sets 64 | Alchemy, 65 | /// Arsenal sets 66 | Arsenal, 67 | /// Mini game sets 68 | Minigame, 69 | #[cfg_attr( 70 | docsrs, 71 | doc(cfg(any(feature = "unknown_variants", feature = "unknown_variants_slim"))) 72 | )] 73 | #[cfg(feature = "unknown_variants")] 74 | #[serde(untagged)] 75 | /// Unknown set type 76 | Unknown(Box), 77 | #[cfg_attr( 78 | docsrs, 79 | doc(cfg(any(feature = "unknown_variants", feature = "unknown_variants_slim"))) 80 | )] 81 | #[cfg(all(not(feature = "unknown_variants"), feature = "unknown_variants_slim"))] 82 | #[serde(other)] 83 | /// Unknown set type 84 | Unknown, 85 | } 86 | 87 | impl fmt::Display for SetType { 88 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 89 | write!( 90 | f, 91 | "{}", 92 | match self { 93 | SetType::Core => "core", 94 | SetType::Expansion => "expansion", 95 | SetType::Masters => "masters", 96 | SetType::Masterpiece => "masterpiece", 97 | SetType::FromTheVault => "from_the_vault", 98 | SetType::Spellbook => "spellbook", 99 | SetType::PremiumDeck => "premium_deck", 100 | SetType::DuelDeck => "duel_deck", 101 | SetType::DraftInnovation => "draft_innovation", 102 | SetType::TreasureChest => "treasure_chest", 103 | SetType::Commander => "commander", 104 | SetType::Planechase => "planechase", 105 | SetType::Archenemy => "archenemy", 106 | SetType::Vanguard => "vanguard", 107 | SetType::Funny => "funny", 108 | SetType::Starter => "starter", 109 | SetType::GiftBox => "gift_box", 110 | SetType::Promo => "promo", 111 | SetType::Token => "token", 112 | SetType::Memorabilia => "memorabilia", 113 | SetType::Alchemy => "alchemy", 114 | SetType::Arsenal => "arsenal", 115 | SetType::Minigame => "minigame", 116 | #[cfg(feature = "unknown_variants")] 117 | SetType::Unknown(s) => s, 118 | #[cfg(all(not(feature = "unknown_variants"), feature = "unknown_variants_slim"))] 119 | SetType::Unknown => "unknown-set-type", 120 | } 121 | ) 122 | } 123 | } 124 | -------------------------------------------------------------------------------- /src/uri.rs: -------------------------------------------------------------------------------- 1 | //! Module for handling unresolved URLs returned by the scryfall api 2 | //! 3 | //! Some fields of the scryfall api have URLs referring to queries that can be 4 | //! run to obtain more information. This module abstracts the work of fetching 5 | //! that data. 6 | use std::convert::TryFrom; 7 | use std::marker::PhantomData; 8 | 9 | use httpstatus::StatusCode; 10 | use reqwest::header::{self, HeaderValue}; 11 | use serde::de::DeserializeOwned; 12 | use serde::{Deserialize, Serialize}; 13 | use url::Url; 14 | 15 | use crate::error::Error; 16 | use crate::list::{List, ListIter}; 17 | 18 | /// An unresolved URI returned by the Scryfall API, or generated by this crate. 19 | /// 20 | /// The `fetch` method handles requesting the resource from the API endpoint, 21 | /// and deserializing it into a `T` object. If the type parameter is 22 | /// [`List`]`<_>`, then additional methods `fetch_iter` 23 | /// and `fetch_all` are available, giving access to objects from all pages 24 | /// of the collection. 25 | #[derive(Serialize, Deserialize, Clone, Eq, PartialEq, Hash)] 26 | #[cfg_attr(test, serde(deny_unknown_fields))] 27 | #[serde(transparent)] 28 | pub struct Uri { 29 | url: Url, 30 | _marker: PhantomData T>, 31 | } 32 | 33 | impl std::fmt::Debug for Uri { 34 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 35 | f.debug_struct("Uri") 36 | .field("url", &self.url) 37 | .field("type", &std::any::type_name::()) 38 | .finish() 39 | } 40 | } 41 | 42 | impl TryFrom<&str> for Uri { 43 | type Error = crate::error::Error; 44 | 45 | fn try_from(url: &str) -> Result { 46 | Ok(Uri::from(Url::parse(url)?)) 47 | } 48 | } 49 | 50 | impl From for Uri { 51 | fn from(url: Url) -> Self { 52 | Uri { 53 | url, 54 | _marker: PhantomData, 55 | } 56 | } 57 | } 58 | 59 | impl Uri { 60 | /// Extract the inner url. 61 | pub fn into_inner(self) -> Url { 62 | self.url 63 | } 64 | 65 | /// Get the contained url. 66 | pub fn inner(&self) -> &Url { 67 | &self.url 68 | } 69 | 70 | /// Get the str representation of the url 71 | pub fn as_str(&self) -> &str { 72 | self.url.as_str() 73 | } 74 | } 75 | 76 | impl AsRef for Uri { 77 | fn as_ref(&self) -> &Url { 78 | self.inner() 79 | } 80 | } 81 | 82 | impl AsRef for Uri { 83 | fn as_ref(&self) -> &str { 84 | self.as_str() 85 | } 86 | } 87 | 88 | fn create_client() -> reqwest::Client { 89 | reqwest::Client::builder() 90 | .default_headers( 91 | [ 92 | (header::ACCEPT, HeaderValue::from_static("*/*")), 93 | (header::USER_AGENT, HeaderValue::from_static("scryfall-rs")), 94 | ] 95 | .into_iter() 96 | .collect(), 97 | ) 98 | .build() 99 | .unwrap() 100 | } 101 | #[cfg(test)] 102 | use create_client as client; 103 | 104 | #[cfg(not(test))] 105 | fn client() -> &'static reqwest::Client { 106 | use std::sync::OnceLock; 107 | static CLIENT: OnceLock = OnceLock::new(); 108 | CLIENT.get_or_init(create_client) 109 | } 110 | 111 | impl Uri { 112 | /// Fetches a resource from the Scryfall API and deserializes it into a type 113 | /// `T`. 114 | /// 115 | /// # Example 116 | /// ```rust 117 | /// # use std::convert::TryFrom; 118 | /// # 119 | /// # use scryfall::card::Card; 120 | /// # use scryfall::uri::Uri; 121 | /// # tokio_test::block_on(async { 122 | /// let uri = 123 | /// Uri::::try_from("https://api.scryfall.com/cards/named?exact=Lightning+Bolt").unwrap(); 124 | /// let bolt = uri.fetch().await.unwrap(); 125 | /// assert_eq!(bolt.mana_cost, Some("{R}".to_string())); 126 | /// # }) 127 | /// ``` 128 | pub async fn fetch(&self) -> crate::Result { 129 | match self.fetch_raw().await { 130 | Ok(response) => match response.status().as_u16() { 131 | 200..=299 => response.json().await.map_err(|e| Error::ReqwestError { 132 | error: e.into(), 133 | url: self.url.clone(), 134 | }), 135 | status => Err(Error::HttpError(StatusCode::from(status))), 136 | }, 137 | Err(e) => Err(e), 138 | } 139 | } 140 | 141 | pub(crate) async fn fetch_raw(&self) -> crate::Result { 142 | match client().get(self.url.clone()).send().await { 143 | Ok(response) => match response.status().as_u16() { 144 | 400..=599 => Err(Error::ScryfallError(response.json().await.map_err( 145 | |e| Error::ReqwestError { 146 | error: e.into(), 147 | url: self.url.clone(), 148 | }, 149 | )?)), 150 | _ => Ok(response), 151 | }, 152 | Err(e) => Err(Error::ReqwestError { 153 | error: e.into(), 154 | url: self.url.clone(), 155 | }), 156 | } 157 | } 158 | } 159 | 160 | impl Uri> { 161 | /// Lazily iterate over items from all pages of a list. Following pages are 162 | /// requested once the previous page has been exhausted. 163 | /// 164 | /// # Example 165 | /// ```rust 166 | /// # use std::convert::TryFrom; 167 | /// # 168 | /// # use scryfall::Card; 169 | /// # use scryfall::list::List; 170 | /// # use scryfall::uri::Uri; 171 | /// use futures::stream::StreamExt; 172 | /// use futures::future; 173 | /// # tokio_test::block_on(async { 174 | /// let uri = Uri::>::try_from("https://api.scryfall.com/cards/search?q=zurgo").unwrap(); 175 | /// assert!( 176 | /// uri.fetch_iter() 177 | /// .await 178 | /// .unwrap() 179 | /// .into_stream() 180 | /// .map(Result::unwrap) 181 | /// .filter(|c| future::ready(c.name.contains("Bellstriker"))) 182 | /// .collect::>() 183 | /// .await 184 | /// .len() 185 | /// > 0 186 | /// ); 187 | /// # }) 188 | /// ``` 189 | /// 190 | /// ```rust 191 | /// # use std::convert::TryFrom; 192 | /// # 193 | /// # use scryfall::Card; 194 | /// # use scryfall::list::List; 195 | /// # use scryfall::uri::Uri; 196 | /// use futures::stream::StreamExt; 197 | /// use futures::future; 198 | /// # tokio_test::block_on(async { 199 | /// let uri = Uri::>::try_from("https://api.scryfall.com/cards/search?q=zurgo").unwrap(); 200 | /// assert!( 201 | /// uri.fetch_iter() 202 | /// .await 203 | /// .unwrap() 204 | /// .into_stream_buffered(10) 205 | /// .map(Result::unwrap) 206 | /// .filter(|c| future::ready(c.name.contains("Bellstriker"))) 207 | /// .collect::>() 208 | /// .await 209 | /// .len() 210 | /// > 0 211 | /// ); 212 | /// # }) 213 | /// ``` 214 | pub async fn fetch_iter(&self) -> crate::Result> { 215 | Ok(self.fetch().await?.into_list_iter()) 216 | } 217 | 218 | /// Eagerly fetch items from all pages of a list. If any of the pages fail 219 | /// to load, returns an error. 220 | /// 221 | /// # Example 222 | /// ```rust 223 | /// # use std::convert::TryFrom; 224 | /// # 225 | /// # use scryfall::Card; 226 | /// # use scryfall::list::List; 227 | /// # use scryfall::uri::Uri; 228 | /// # tokio_test::block_on(async { 229 | /// let uri = 230 | /// Uri::>::try_from("https://api.scryfall.com/cards/search?q=e:ddu&unique=prints") 231 | /// .unwrap(); 232 | /// assert_eq!(uri.fetch_all().await.unwrap().len(), 76); 233 | /// # }) 234 | /// ``` 235 | pub async fn fetch_all(&self) -> crate::Result> { 236 | let mut items = vec![]; 237 | let mut next_page = Some(self.fetch().await?); 238 | while let Some(page) = next_page { 239 | items.extend(page.data.into_iter()); 240 | next_page = match page.next_page { 241 | Some(uri) => Some(uri.fetch().await?), 242 | None => None, 243 | }; 244 | } 245 | Ok(items) 246 | } 247 | } 248 | -------------------------------------------------------------------------------- /src/util.rs: -------------------------------------------------------------------------------- 1 | //! Module containing utility functions and structs. 2 | use once_cell::sync::Lazy; 3 | use serde::{Deserialize, Deserializer}; 4 | use url::Url; 5 | 6 | pub(crate) mod streaming_deserializer; 7 | 8 | /// The [scryfall](https://scryfall.com/docs/api) endpoint. 9 | pub static ROOT_URL: Lazy = Lazy::new(|| Url::parse("https://api.scryfall.com/").unwrap()); 10 | /// The [cards](https://scryfall.com/docs/api/cards) endpoint. 11 | pub static CARDS_URL: Lazy = Lazy::new(|| ROOT_URL.join("cards/").unwrap()); 12 | /// The [sets](https://scryfall.com/docs/api/sets) endpoint. 13 | pub static SETS_URL: Lazy = Lazy::new(|| ROOT_URL.join("sets/").unwrap()); 14 | /// The [bulk-data](https://scryfall.com/docs/api/bulk-data) endpoint. 15 | pub static BULK_DATA_URL: Lazy = Lazy::new(|| ROOT_URL.join("bulk-data/").unwrap()); 16 | /// The [catalog](https://scryfall.com/docs/api/catalogs) endpoint. 17 | pub static CATALOG_URL: Lazy = Lazy::new(|| ROOT_URL.join("catalog/").unwrap()); 18 | 19 | /// The [rulings](https://scryfall.com/docs/api/rulings) path segment, which goes on the end of a 20 | /// card URL. 21 | pub const API_RULING: &str = "rulings/"; 22 | 23 | /// Function for use with `#[serde(deserialize_with)]` and a field that's 24 | /// Option. If deserialization fails, use `None` as the field's value and 25 | /// don't cause an error. 26 | pub fn deserialize_or_none<'de, D: Deserializer<'de>, T: Deserialize<'de>>( 27 | deserializer: D, 28 | ) -> Result, D::Error> { 29 | T::deserialize(deserializer).map(Some).or(Ok(None)) 30 | } 31 | -------------------------------------------------------------------------------- /src/util/streaming_deserializer.rs: -------------------------------------------------------------------------------- 1 | use std::fmt; 2 | use std::marker::Send; 3 | 4 | use futures::Stream; 5 | use serde::de::DeserializeOwned; 6 | use serde::{de::Visitor, Deserialize, Deserializer}; 7 | use tokio::io::AsyncRead; 8 | use tokio::sync::mpsc::{channel, Sender}; 9 | use tokio_stream::wrappers::ReceiverStream; 10 | use tokio_util::io::SyncIoBridge; 11 | 12 | use crate::Error; 13 | 14 | pub fn create(reader: R) -> impl Stream> 15 | where 16 | Value: DeserializeOwned + Send + 'static, 17 | R: AsyncRead + Unpin + Send + 'static, 18 | { 19 | struct ItemVisitor { 20 | sender: Sender>, 21 | } 22 | 23 | impl<'de, V: Deserialize<'de>> Visitor<'de> for ItemVisitor { 24 | type Value = (); 25 | 26 | fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { 27 | formatter.write_str("seq of items") 28 | } 29 | 30 | fn visit_seq(self, mut seq: A) -> Result 31 | where 32 | A: serde::de::SeqAccess<'de>, 33 | { 34 | loop { 35 | let result = seq.next_element(); 36 | match result { 37 | Ok(Some(v)) => { 38 | if self.sender.blocking_send(Ok(v)).is_err() { 39 | break; 40 | } 41 | }, 42 | Ok(None) => break, 43 | Err(e) => return Err(e), 44 | } 45 | } 46 | Ok(()) 47 | } 48 | } 49 | 50 | let (sender, receiver) = channel::>(50); 51 | 52 | let sync_reader = SyncIoBridge::new(reader); 53 | tokio::task::spawn_blocking(move || { 54 | let mut deserializer = serde_json::Deserializer::from_reader(sync_reader); 55 | if let Err(e) = deserializer.deserialize_seq(ItemVisitor:: { 56 | sender: sender.clone(), 57 | }) { 58 | let _ = sender.blocking_send(Err(Error::JsonError(e))); //let _ = because error from calling send just means receiver has disconnected 59 | } 60 | }); 61 | 62 | ReceiverStream::new(receiver) 63 | } 64 | -------------------------------------------------------------------------------- /tests/variants-feature/default_variants.rs: -------------------------------------------------------------------------------- 1 | #![deny(unreachable_patterns)] 2 | 3 | use scryfall::{ 4 | card::{Finishes, FrameEffect, Layout, PromoType, SecurityStamp}, 5 | format::Format, 6 | set::SetType, 7 | }; 8 | 9 | use static_assertions as sa; 10 | 11 | sa::assert_impl_all!(Format: Copy); 12 | sa::assert_impl_all!(FrameEffect: Copy); 13 | sa::assert_impl_all!(Layout: Copy); 14 | sa::assert_impl_all!(SetType: Copy); 15 | sa::assert_impl_all!(PromoType: Copy); 16 | sa::assert_impl_all!(SecurityStamp: Copy); 17 | 18 | sa::assert_eq_size!(Format, u8); 19 | sa::assert_eq_size!(FrameEffect, u8); 20 | sa::assert_eq_size!(Layout, u8); 21 | sa::assert_eq_size!(SetType, u8); 22 | sa::assert_eq_size!(PromoType, u8); 23 | sa::assert_eq_size!(SecurityStamp, u8); 24 | 25 | #[allow(dead_code)] 26 | fn match_on_frame_effect(f: FrameEffect) { 27 | match f { 28 | FrameEffect::Legendary => todo!(), 29 | FrameEffect::Miracle => todo!(), 30 | FrameEffect::Nyxtouched => todo!(), 31 | FrameEffect::Draft => todo!(), 32 | FrameEffect::Devoid => todo!(), 33 | FrameEffect::Tombstone => todo!(), 34 | FrameEffect::Colorshifted => todo!(), 35 | FrameEffect::Inverted => todo!(), 36 | FrameEffect::SunMoonDfc => todo!(), 37 | FrameEffect::CompassLandDfc => todo!(), 38 | FrameEffect::OriginPwDfc => todo!(), 39 | FrameEffect::MoonEldraziDfc => todo!(), 40 | FrameEffect::WaxingAndWaningMoonDfc => todo!(), 41 | FrameEffect::Showcase => todo!(), 42 | FrameEffect::ExtendedArt => todo!(), 43 | FrameEffect::Companion => todo!(), 44 | FrameEffect::Etched => todo!(), 45 | FrameEffect::Snow => todo!(), 46 | FrameEffect::Lesson => todo!(), 47 | FrameEffect::ShatteredGlass => todo!(), 48 | FrameEffect::ConvertDfc => todo!(), 49 | FrameEffect::FanDfc => todo!(), 50 | FrameEffect::UpsideDownDfc => todo!(), 51 | FrameEffect::MoonReverseMoonDfc => todo!(), 52 | FrameEffect::FullArt => todo!(), 53 | FrameEffect::Nyxborn => todo!(), 54 | FrameEffect::Booster => todo!(), 55 | FrameEffect::Textless => todo!(), 56 | FrameEffect::StorySpotlight => todo!(), 57 | FrameEffect::Thick => todo!(), 58 | FrameEffect::Borderless => todo!(), 59 | FrameEffect::Vehicle => todo!(), 60 | FrameEffect::Spree => todo!(), 61 | _ => todo!(), 62 | } 63 | } 64 | #[allow(dead_code)] 65 | fn match_on_layout(f: Layout) { 66 | match f { 67 | Layout::Normal => todo!(), 68 | Layout::Split => todo!(), 69 | Layout::Flip => todo!(), 70 | Layout::Transform => todo!(), 71 | Layout::ModalDfc => todo!(), 72 | Layout::Meld => todo!(), 73 | Layout::Leveler => todo!(), 74 | Layout::Class => todo!(), 75 | Layout::Saga => todo!(), 76 | Layout::Adventure => todo!(), 77 | Layout::Planar => todo!(), 78 | Layout::Scheme => todo!(), 79 | Layout::Vanguard => todo!(), 80 | Layout::Token => todo!(), 81 | Layout::DoubleFacedToken => todo!(), 82 | Layout::Emblem => todo!(), 83 | Layout::Augment => todo!(), 84 | Layout::Host => todo!(), 85 | Layout::ArtSeries => todo!(), 86 | Layout::ReversibleCard => todo!(), 87 | Layout::Prototype => todo!(), 88 | Layout::Mutate => todo!(), 89 | Layout::Case => todo!(), 90 | _ => todo!(), 91 | } 92 | } 93 | 94 | #[allow(dead_code)] 95 | fn match_on_set_type(f: SetType) { 96 | match f { 97 | SetType::Core => todo!(), 98 | SetType::Expansion => todo!(), 99 | SetType::Masters => todo!(), 100 | SetType::Masterpiece => todo!(), 101 | SetType::FromTheVault => todo!(), 102 | SetType::Spellbook => todo!(), 103 | SetType::PremiumDeck => todo!(), 104 | SetType::DuelDeck => todo!(), 105 | SetType::DraftInnovation => todo!(), 106 | SetType::TreasureChest => todo!(), 107 | SetType::Commander => todo!(), 108 | SetType::Planechase => todo!(), 109 | SetType::Archenemy => todo!(), 110 | SetType::Vanguard => todo!(), 111 | SetType::Funny => todo!(), 112 | SetType::Starter => todo!(), 113 | SetType::GiftBox => todo!(), 114 | SetType::Promo => todo!(), 115 | SetType::Token => todo!(), 116 | SetType::Memorabilia => todo!(), 117 | SetType::Alchemy => todo!(), 118 | SetType::Arsenal => todo!(), 119 | SetType::Minigame => todo!(), 120 | _ => todo!(), 121 | } 122 | } 123 | 124 | #[allow(dead_code)] 125 | fn match_on_promo_type(f: PromoType) { 126 | match f { 127 | PromoType::Alchemy => todo!(), 128 | PromoType::Arenaleague => todo!(), 129 | PromoType::Boosterfun => todo!(), 130 | PromoType::Boxtopper => todo!(), 131 | PromoType::Brawldeck => todo!(), 132 | PromoType::Bringafriend => todo!(), 133 | PromoType::Bundle => todo!(), 134 | PromoType::Buyabox => todo!(), 135 | PromoType::Commanderparty => todo!(), 136 | PromoType::Commanderpromo => todo!(), 137 | PromoType::Concept => todo!(), 138 | PromoType::Confettifoil => todo!(), 139 | PromoType::Convention => todo!(), 140 | PromoType::Datestamped => todo!(), 141 | PromoType::Dossier => todo!(), 142 | PromoType::Doubleexposure => todo!(), 143 | PromoType::Doublerainbow => todo!(), 144 | PromoType::Draculaseries => todo!(), 145 | PromoType::Draftweekend => todo!(), 146 | PromoType::Duels => todo!(), 147 | PromoType::Embossed => todo!(), 148 | PromoType::Event => todo!(), 149 | PromoType::FFI => todo!(), 150 | PromoType::FFII => todo!(), 151 | PromoType::FFIII => todo!(), 152 | PromoType::FFIV => todo!(), 153 | PromoType::FFV => todo!(), 154 | PromoType::FFVI => todo!(), 155 | PromoType::FFVII => todo!(), 156 | PromoType::FFVIII => todo!(), 157 | PromoType::FFIX => todo!(), 158 | PromoType::FFX => todo!(), 159 | PromoType::FFXI => todo!(), 160 | PromoType::FFXII => todo!(), 161 | PromoType::FFXIII => todo!(), 162 | PromoType::FFXIV => todo!(), 163 | PromoType::FFXV => todo!(), 164 | PromoType::FFXVI => todo!(), 165 | PromoType::FirstPlaceFoil => todo!(), 166 | PromoType::Fnm => todo!(), 167 | PromoType::Fracturefoil => todo!(), 168 | PromoType::Galaxyfoil => todo!(), 169 | PromoType::Gameday => todo!(), 170 | PromoType::Giftbox => todo!(), 171 | PromoType::Gilded => todo!(), 172 | PromoType::Glossy => todo!(), 173 | PromoType::Godzillaseries => todo!(), 174 | PromoType::Halofoil => todo!(), 175 | PromoType::Imagine => todo!(), 176 | PromoType::Instore => todo!(), 177 | PromoType::Intropack => todo!(), 178 | PromoType::Invisibleink => todo!(), 179 | PromoType::Jpwalker => todo!(), 180 | PromoType::Judgegift => todo!(), 181 | PromoType::League => todo!(), 182 | PromoType::Magnified => todo!(), 183 | PromoType::Mediainsert => todo!(), 184 | PromoType::Moonlitland => todo!(), 185 | PromoType::Neonink => todo!(), 186 | PromoType::Oilslick => todo!(), 187 | PromoType::Openhouse => todo!(), 188 | PromoType::Planeswalkerdeck => todo!(), 189 | PromoType::Plastic => todo!(), 190 | PromoType::Playerrewards => todo!(), 191 | PromoType::Playpromo => todo!(), 192 | PromoType::Playtest => todo!(), 193 | PromoType::Portrait => todo!(), 194 | PromoType::Poster => todo!(), 195 | PromoType::Premiereshop => todo!(), 196 | PromoType::Prerelease => todo!(), 197 | PromoType::Promopack => todo!(), 198 | PromoType::Rainbowfoil => todo!(), 199 | PromoType::Raisedfoil => todo!(), 200 | PromoType::Ravnicacity => todo!(), 201 | PromoType::Rebalanced => todo!(), 202 | PromoType::Release => todo!(), 203 | PromoType::Ripplefoil => todo!(), 204 | PromoType::Schinesealtart => todo!(), 205 | PromoType::Scroll => todo!(), 206 | PromoType::Serialized => todo!(), 207 | PromoType::Setextension => todo!(), 208 | PromoType::Setpromo => todo!(), 209 | PromoType::Silverfoil => todo!(), 210 | PromoType::Stamped => todo!(), 211 | PromoType::Starterdeck => todo!(), 212 | PromoType::Stepandcompleat => todo!(), 213 | PromoType::Storechampionship => todo!(), 214 | PromoType::Surgefoil => todo!(), 215 | PromoType::Textured => todo!(), 216 | PromoType::Themepack => todo!(), 217 | PromoType::Thick => todo!(), 218 | PromoType::Tourney => todo!(), 219 | PromoType::UpsideDown => todo!(), 220 | PromoType::UpsideDownBack => todo!(), 221 | PromoType::Vault => todo!(), 222 | PromoType::Wizardsplaynetwork => todo!(), 223 | PromoType::DragonScaleFoil => todo!(), 224 | _ => todo!(), 225 | } 226 | } 227 | 228 | #[allow(dead_code)] 229 | fn match_on_security_stamp(s: SecurityStamp) { 230 | match s { 231 | SecurityStamp::Oval => todo!(), 232 | SecurityStamp::Triangle => todo!(), 233 | SecurityStamp::Acorn => todo!(), 234 | SecurityStamp::Circle => todo!(), 235 | SecurityStamp::Arena => todo!(), 236 | SecurityStamp::Heart => todo!(), 237 | _ => todo!(), 238 | } 239 | } 240 | 241 | #[allow(dead_code)] 242 | fn match_on_finishes(f: Finishes) { 243 | match f { 244 | Finishes::Nonfoil => todo!(), 245 | Finishes::Foil => todo!(), 246 | Finishes::Etched => todo!(), 247 | _ => todo!(), 248 | } 249 | } 250 | 251 | #[test] 252 | fn deserialize() { 253 | assert!(serde_json::from_str::(r#""foo""#).is_err()); 254 | assert!(serde_json::from_str::(r#""foo""#).is_err()); 255 | assert!(serde_json::from_str::(r#""foo""#).is_err()); 256 | assert!(serde_json::from_str::(r#""foo""#).is_err()); 257 | assert!(serde_json::from_str::(r#""foo""#).is_err()); 258 | assert!(serde_json::from_str::(r#""foo""#).is_err()); 259 | } 260 | -------------------------------------------------------------------------------- /tests/variants-feature/main.rs: -------------------------------------------------------------------------------- 1 | #[cfg(all( 2 | not(feature = "unknown_variants"), 3 | not(feature = "unknown_variants_slim") 4 | ))] 5 | mod default_variants; 6 | 7 | #[cfg(feature = "unknown_variants")] 8 | mod unknown_variants; 9 | 10 | #[cfg(all(not(feature = "unknown_variants"), feature = "unknown_variants_slim"))] 11 | mod unknown_variants_slim; 12 | 13 | fn main() {} 14 | -------------------------------------------------------------------------------- /tests/variants-feature/unknown_variants.rs: -------------------------------------------------------------------------------- 1 | use scryfall::{ 2 | card::{Finishes, FrameEffect, Layout, PromoType, SecurityStamp}, 3 | set::SetType, 4 | }; 5 | 6 | use static_assertions as sa; 7 | 8 | sa::assert_eq_size!(FrameEffect, [u8; 24]); 9 | sa::assert_eq_size!(Layout, [u8; 24]); 10 | sa::assert_eq_size!(SetType, [u8; 24]); 11 | sa::assert_eq_size!(PromoType, [u8; 24]); 12 | sa::assert_eq_size!(SecurityStamp, [u8; 24]); 13 | 14 | #[allow(dead_code)] 15 | fn match_on_frame_effect(f: FrameEffect) { 16 | match f { 17 | FrameEffect::Legendary => todo!(), 18 | FrameEffect::Miracle => todo!(), 19 | FrameEffect::Nyxtouched => todo!(), 20 | FrameEffect::Draft => todo!(), 21 | FrameEffect::Devoid => todo!(), 22 | FrameEffect::Tombstone => todo!(), 23 | FrameEffect::Colorshifted => todo!(), 24 | FrameEffect::Inverted => todo!(), 25 | FrameEffect::SunMoonDfc => todo!(), 26 | FrameEffect::CompassLandDfc => todo!(), 27 | FrameEffect::OriginPwDfc => todo!(), 28 | FrameEffect::MoonEldraziDfc => todo!(), 29 | FrameEffect::WaxingAndWaningMoonDfc => todo!(), 30 | FrameEffect::Showcase => todo!(), 31 | FrameEffect::ExtendedArt => todo!(), 32 | FrameEffect::Companion => todo!(), 33 | FrameEffect::Etched => todo!(), 34 | FrameEffect::Snow => todo!(), 35 | FrameEffect::Lesson => todo!(), 36 | FrameEffect::ShatteredGlass => todo!(), 37 | FrameEffect::ConvertDfc => todo!(), 38 | FrameEffect::FanDfc => todo!(), 39 | FrameEffect::UpsideDownDfc => todo!(), 40 | FrameEffect::MoonReverseMoonDfc => todo!(), 41 | FrameEffect::Enchantment => todo!(), 42 | FrameEffect::FullArt => todo!(), 43 | FrameEffect::Nyxborn => todo!(), 44 | FrameEffect::Booster => todo!(), 45 | FrameEffect::Textless => todo!(), 46 | FrameEffect::StorySpotlight => todo!(), 47 | FrameEffect::Thick => todo!(), 48 | FrameEffect::Borderless => todo!(), 49 | FrameEffect::Vehicle => todo!(), 50 | FrameEffect::Spree => todo!(), 51 | FrameEffect::Unknown(_) => todo!(), 52 | } 53 | } 54 | #[allow(dead_code)] 55 | fn match_on_layout(f: Layout) { 56 | match f { 57 | Layout::Normal => todo!(), 58 | Layout::Split => todo!(), 59 | Layout::Flip => todo!(), 60 | Layout::Transform => todo!(), 61 | Layout::ModalDfc => todo!(), 62 | Layout::Meld => todo!(), 63 | Layout::Leveler => todo!(), 64 | Layout::Class => todo!(), 65 | Layout::Saga => todo!(), 66 | Layout::Adventure => todo!(), 67 | Layout::Planar => todo!(), 68 | Layout::Scheme => todo!(), 69 | Layout::Vanguard => todo!(), 70 | Layout::Token => todo!(), 71 | Layout::DoubleFacedToken => todo!(), 72 | Layout::Emblem => todo!(), 73 | Layout::Augment => todo!(), 74 | Layout::Host => todo!(), 75 | Layout::ArtSeries => todo!(), 76 | Layout::ReversibleCard => todo!(), 77 | Layout::Prototype => todo!(), 78 | Layout::Mutate => todo!(), 79 | Layout::Case => todo!(), 80 | Layout::Unknown(_) => todo!(), 81 | } 82 | } 83 | 84 | #[allow(dead_code)] 85 | fn match_on_set_type(f: SetType) { 86 | match f { 87 | SetType::Core => todo!(), 88 | SetType::Expansion => todo!(), 89 | SetType::Masters => todo!(), 90 | SetType::Masterpiece => todo!(), 91 | SetType::FromTheVault => todo!(), 92 | SetType::Spellbook => todo!(), 93 | SetType::PremiumDeck => todo!(), 94 | SetType::DuelDeck => todo!(), 95 | SetType::DraftInnovation => todo!(), 96 | SetType::TreasureChest => todo!(), 97 | SetType::Commander => todo!(), 98 | SetType::Planechase => todo!(), 99 | SetType::Archenemy => todo!(), 100 | SetType::Vanguard => todo!(), 101 | SetType::Funny => todo!(), 102 | SetType::Starter => todo!(), 103 | SetType::GiftBox => todo!(), 104 | SetType::Promo => todo!(), 105 | SetType::Token => todo!(), 106 | SetType::Memorabilia => todo!(), 107 | SetType::Alchemy => todo!(), 108 | SetType::Arsenal => todo!(), 109 | SetType::Minigame => todo!(), 110 | SetType::Unknown(_) => todo!(), 111 | } 112 | } 113 | 114 | #[allow(dead_code)] 115 | fn match_on_promo_type(f: PromoType) { 116 | match f { 117 | PromoType::Alchemy => todo!(), 118 | PromoType::Arenaleague => todo!(), 119 | PromoType::Beginnerbox => todo!(), 120 | PromoType::Boosterfun => todo!(), 121 | PromoType::Boxtopper => todo!(), 122 | PromoType::Brawldeck => todo!(), 123 | PromoType::Bringafriend => todo!(), 124 | PromoType::Bundle => todo!(), 125 | PromoType::Buyabox => todo!(), 126 | PromoType::Commanderparty => todo!(), 127 | PromoType::Commanderpromo => todo!(), 128 | PromoType::Concept => todo!(), 129 | PromoType::Confettifoil => todo!(), 130 | PromoType::Convention => todo!(), 131 | PromoType::Datestamped => todo!(), 132 | PromoType::Dossier => todo!(), 133 | PromoType::Doubleexposure => todo!(), 134 | PromoType::Doublerainbow => todo!(), 135 | PromoType::Draculaseries => todo!(), 136 | PromoType::Draftweekend => todo!(), 137 | PromoType::DragonScaleFoil => todo!(), 138 | PromoType::Duels => todo!(), 139 | PromoType::Embossed => todo!(), 140 | PromoType::Event => todo!(), 141 | PromoType::FFI => todo!(), 142 | PromoType::FFII => todo!(), 143 | PromoType::FFIII => todo!(), 144 | PromoType::FFIV => todo!(), 145 | PromoType::FFV => todo!(), 146 | PromoType::FFVI => todo!(), 147 | PromoType::FFVII => todo!(), 148 | PromoType::FFVIII => todo!(), 149 | PromoType::FFIX => todo!(), 150 | PromoType::FFX => todo!(), 151 | PromoType::FFXI => todo!(), 152 | PromoType::FFXII => todo!(), 153 | PromoType::FFXIII => todo!(), 154 | PromoType::FFXIV => todo!(), 155 | PromoType::FFXV => todo!(), 156 | PromoType::FFXVI => todo!(), 157 | PromoType::FirstPlaceFoil => todo!(), 158 | PromoType::Fnm => todo!(), 159 | PromoType::Fracturefoil => todo!(), 160 | PromoType::Galaxyfoil => todo!(), 161 | PromoType::Gameday => todo!(), 162 | PromoType::Giftbox => todo!(), 163 | PromoType::Gilded => todo!(), 164 | PromoType::Glossy => todo!(), 165 | PromoType::Godzillaseries => todo!(), 166 | PromoType::Halofoil => todo!(), 167 | PromoType::Imagine => todo!(), 168 | PromoType::Instore => todo!(), 169 | PromoType::Intropack => todo!(), 170 | PromoType::Invisibleink => todo!(), 171 | PromoType::Jpwalker => todo!(), 172 | PromoType::Judgegift => todo!(), 173 | PromoType::League => todo!(), 174 | PromoType::Magnified => todo!(), 175 | PromoType::Manafoil => todo!(), 176 | PromoType::Mediainsert => todo!(), 177 | PromoType::Moonlitland => todo!(), 178 | PromoType::Neonink => todo!(), 179 | PromoType::Oilslick => todo!(), 180 | PromoType::Openhouse => todo!(), 181 | PromoType::Planeswalkerdeck => todo!(), 182 | PromoType::Plastic => todo!(), 183 | PromoType::Playerrewards => todo!(), 184 | PromoType::Playpromo => todo!(), 185 | PromoType::Playtest => todo!(), 186 | PromoType::Portrait => todo!(), 187 | PromoType::Poster => todo!(), 188 | PromoType::Premiereshop => todo!(), 189 | PromoType::Prerelease => todo!(), 190 | PromoType::Promopack => todo!(), 191 | PromoType::Rainbowfoil => todo!(), 192 | PromoType::Raisedfoil => todo!(), 193 | PromoType::Ravnicacity => todo!(), 194 | PromoType::Rebalanced => todo!(), 195 | PromoType::Release => todo!(), 196 | PromoType::Resale => todo!(), 197 | PromoType::Ripplefoil => todo!(), 198 | PromoType::Schinesealtart => todo!(), 199 | PromoType::Scroll => todo!(), 200 | PromoType::Serialized => todo!(), 201 | PromoType::Setextension => todo!(), 202 | PromoType::Setpromo => todo!(), 203 | PromoType::Silverfoil => todo!(), 204 | PromoType::Sldbonus => todo!(), 205 | PromoType::Stamped => todo!(), 206 | PromoType::Startercollection => todo!(), 207 | PromoType::Starterdeck => todo!(), 208 | PromoType::Stepandcompleat => todo!(), 209 | PromoType::Storechampionship => todo!(), 210 | PromoType::Surgefoil => todo!(), 211 | PromoType::Textured => todo!(), 212 | PromoType::Themepack => todo!(), 213 | PromoType::Thick => todo!(), 214 | PromoType::Tourney => todo!(), 215 | PromoType::Unknown(_) => todo!(), 216 | PromoType::UpsideDown => todo!(), 217 | PromoType::UpsideDownBack => todo!(), 218 | PromoType::Vault => todo!(), 219 | PromoType::Wizardsplaynetwork => todo!(), 220 | } 221 | } 222 | 223 | #[allow(dead_code)] 224 | fn match_on_security_stamp(s: SecurityStamp) { 225 | match s { 226 | SecurityStamp::Oval => todo!(), 227 | SecurityStamp::Triangle => todo!(), 228 | SecurityStamp::Acorn => todo!(), 229 | SecurityStamp::Circle => todo!(), 230 | SecurityStamp::Arena => todo!(), 231 | SecurityStamp::Heart => todo!(), 232 | SecurityStamp::Unknown(_) => todo!(), 233 | } 234 | } 235 | 236 | #[allow(dead_code)] 237 | fn match_on_finishes(f: Finishes) { 238 | match f { 239 | Finishes::Nonfoil => todo!(), 240 | Finishes::Foil => todo!(), 241 | Finishes::Etched => todo!(), 242 | Finishes::Unknown(_) => todo!(), 243 | } 244 | } 245 | 246 | #[test] 247 | fn deserialize() { 248 | assert_eq!( 249 | serde_json::from_str::(r#""foo""#).unwrap(), 250 | FrameEffect::Unknown("foo".into()) 251 | ); 252 | assert_eq!( 253 | serde_json::from_str::(r#""foo""#).unwrap(), 254 | Layout::Unknown("foo".into()) 255 | ); 256 | assert_eq!( 257 | serde_json::from_str::(r#""foo""#).unwrap(), 258 | SetType::Unknown("foo".into()) 259 | ); 260 | assert_eq!( 261 | serde_json::from_str::(r#""foo""#).unwrap(), 262 | PromoType::Unknown("foo".into()) 263 | ); 264 | assert_eq!( 265 | serde_json::from_str::(r#""foo""#).unwrap(), 266 | SecurityStamp::Unknown("foo".into()) 267 | ); 268 | assert_eq!( 269 | serde_json::from_str::(r#""foo""#).unwrap(), 270 | Finishes::Unknown("foo".into()) 271 | ); 272 | } 273 | -------------------------------------------------------------------------------- /tests/variants-feature/unknown_variants_slim.rs: -------------------------------------------------------------------------------- 1 | use scryfall::{ 2 | card::{Finishes, FrameEffect, Layout, PromoType, SecurityStamp}, 3 | set::SetType, 4 | }; 5 | 6 | use static_assertions as sa; 7 | 8 | sa::assert_impl_all!(FrameEffect: Copy); 9 | sa::assert_impl_all!(Layout: Copy); 10 | sa::assert_impl_all!(SetType: Copy); 11 | sa::assert_impl_all!(PromoType: Copy); 12 | sa::assert_impl_all!(SecurityStamp: Copy); 13 | 14 | sa::assert_eq_size!(FrameEffect, u8); 15 | sa::assert_eq_size!(Layout, u8); 16 | sa::assert_eq_size!(SetType, u8); 17 | sa::assert_eq_size!(PromoType, u8); 18 | sa::assert_eq_size!(SecurityStamp, u8); 19 | 20 | #[allow(dead_code)] 21 | fn match_on_frame_effect(f: FrameEffect) { 22 | match f { 23 | FrameEffect::Legendary => todo!(), 24 | FrameEffect::Miracle => todo!(), 25 | FrameEffect::Nyxtouched => todo!(), 26 | FrameEffect::Draft => todo!(), 27 | FrameEffect::Devoid => todo!(), 28 | FrameEffect::Tombstone => todo!(), 29 | FrameEffect::Colorshifted => todo!(), 30 | FrameEffect::Inverted => todo!(), 31 | FrameEffect::SunMoonDfc => todo!(), 32 | FrameEffect::CompassLandDfc => todo!(), 33 | FrameEffect::OriginPwDfc => todo!(), 34 | FrameEffect::MoonEldraziDfc => todo!(), 35 | FrameEffect::WaxingAndWaningMoonDfc => todo!(), 36 | FrameEffect::Showcase => todo!(), 37 | FrameEffect::ExtendedArt => todo!(), 38 | FrameEffect::Companion => todo!(), 39 | FrameEffect::Etched => todo!(), 40 | FrameEffect::Snow => todo!(), 41 | FrameEffect::Lesson => todo!(), 42 | FrameEffect::ShatteredGlass => todo!(), 43 | FrameEffect::ConvertDfc => todo!(), 44 | FrameEffect::FanDfc => todo!(), 45 | FrameEffect::UpsideDownDfc => todo!(), 46 | FrameEffect::MoonReverseMoonDfc => todo!(), 47 | FrameEffect::Enchantment => todo!(), 48 | FrameEffect::FullArt => todo!(), 49 | FrameEffect::Nyxborn => todo!(), 50 | FrameEffect::Booster => todo!(), 51 | FrameEffect::Textless => todo!(), 52 | FrameEffect::StorySpotlight => todo!(), 53 | FrameEffect::Thick => todo!(), 54 | FrameEffect::Borderless => todo!(), 55 | FrameEffect::Vehicle => todo!(), 56 | FrameEffect::Spree => todo!(), 57 | FrameEffect::Unknown => todo!(), 58 | } 59 | } 60 | #[allow(dead_code)] 61 | fn match_on_layout(f: Layout) { 62 | match f { 63 | Layout::Normal => todo!(), 64 | Layout::Split => todo!(), 65 | Layout::Flip => todo!(), 66 | Layout::Transform => todo!(), 67 | Layout::ModalDfc => todo!(), 68 | Layout::Meld => todo!(), 69 | Layout::Leveler => todo!(), 70 | Layout::Class => todo!(), 71 | Layout::Saga => todo!(), 72 | Layout::Adventure => todo!(), 73 | Layout::Planar => todo!(), 74 | Layout::Scheme => todo!(), 75 | Layout::Vanguard => todo!(), 76 | Layout::Token => todo!(), 77 | Layout::DoubleFacedToken => todo!(), 78 | Layout::Emblem => todo!(), 79 | Layout::Augment => todo!(), 80 | Layout::Host => todo!(), 81 | Layout::ArtSeries => todo!(), 82 | Layout::ReversibleCard => todo!(), 83 | Layout::Prototype => todo!(), 84 | Layout::Mutate => todo!(), 85 | Layout::Case => todo!(), 86 | Layout::Unknown => todo!(), 87 | } 88 | } 89 | 90 | #[allow(dead_code)] 91 | fn match_on_set_type(f: SetType) { 92 | match f { 93 | SetType::Core => todo!(), 94 | SetType::Expansion => todo!(), 95 | SetType::Masters => todo!(), 96 | SetType::Masterpiece => todo!(), 97 | SetType::FromTheVault => todo!(), 98 | SetType::Spellbook => todo!(), 99 | SetType::PremiumDeck => todo!(), 100 | SetType::DuelDeck => todo!(), 101 | SetType::DraftInnovation => todo!(), 102 | SetType::TreasureChest => todo!(), 103 | SetType::Commander => todo!(), 104 | SetType::Planechase => todo!(), 105 | SetType::Archenemy => todo!(), 106 | SetType::Vanguard => todo!(), 107 | SetType::Funny => todo!(), 108 | SetType::Starter => todo!(), 109 | SetType::GiftBox => todo!(), 110 | SetType::Promo => todo!(), 111 | SetType::Token => todo!(), 112 | SetType::Memorabilia => todo!(), 113 | SetType::Alchemy => todo!(), 114 | SetType::Arsenal => todo!(), 115 | SetType::Minigame => todo!(), 116 | SetType::Unknown => todo!(), 117 | } 118 | } 119 | 120 | #[allow(dead_code)] 121 | fn match_on_promo_type(f: PromoType) { 122 | match f { 123 | PromoType::Alchemy => todo!(), 124 | PromoType::Arenaleague => todo!(), 125 | PromoType::Beginnerbox => todo!(), 126 | PromoType::Boosterfun => todo!(), 127 | PromoType::Boxtopper => todo!(), 128 | PromoType::Brawldeck => todo!(), 129 | PromoType::Bringafriend => todo!(), 130 | PromoType::Bundle => todo!(), 131 | PromoType::Buyabox => todo!(), 132 | PromoType::Commanderparty => todo!(), 133 | PromoType::Commanderpromo => todo!(), 134 | PromoType::Concept => todo!(), 135 | PromoType::Confettifoil => todo!(), 136 | PromoType::Convention => todo!(), 137 | PromoType::Datestamped => todo!(), 138 | PromoType::Dossier => todo!(), 139 | PromoType::Doubleexposure => todo!(), 140 | PromoType::Doublerainbow => todo!(), 141 | PromoType::Draculaseries => todo!(), 142 | PromoType::Draftweekend => todo!(), 143 | PromoType::DragonScaleFoil => todo!(), 144 | PromoType::Duels => todo!(), 145 | PromoType::Embossed => todo!(), 146 | PromoType::Event => todo!(), 147 | PromoType::FFI => todo!(), 148 | PromoType::FFII => todo!(), 149 | PromoType::FFIII => todo!(), 150 | PromoType::FFIV => todo!(), 151 | PromoType::FFV => todo!(), 152 | PromoType::FFVI => todo!(), 153 | PromoType::FFVII => todo!(), 154 | PromoType::FFVIII => todo!(), 155 | PromoType::FFIX => todo!(), 156 | PromoType::FFX => todo!(), 157 | PromoType::FFXI => todo!(), 158 | PromoType::FFXII => todo!(), 159 | PromoType::FFXIII => todo!(), 160 | PromoType::FFXIV => todo!(), 161 | PromoType::FFXV => todo!(), 162 | PromoType::FFXVI => todo!(), 163 | PromoType::FirstPlaceFoil => todo!(), 164 | PromoType::Fnm => todo!(), 165 | PromoType::Fracturefoil => todo!(), 166 | PromoType::Galaxyfoil => todo!(), 167 | PromoType::Gameday => todo!(), 168 | PromoType::Giftbox => todo!(), 169 | PromoType::Gilded => todo!(), 170 | PromoType::Glossy => todo!(), 171 | PromoType::Godzillaseries => todo!(), 172 | PromoType::Halofoil => todo!(), 173 | PromoType::Imagine => todo!(), 174 | PromoType::Instore => todo!(), 175 | PromoType::Intropack => todo!(), 176 | PromoType::Invisibleink => todo!(), 177 | PromoType::Jpwalker => todo!(), 178 | PromoType::Judgegift => todo!(), 179 | PromoType::League => todo!(), 180 | PromoType::Magnified => todo!(), 181 | PromoType::Manafoil => todo!(), 182 | PromoType::Mediainsert => todo!(), 183 | PromoType::Moonlitland => todo!(), 184 | PromoType::Neonink => todo!(), 185 | PromoType::Oilslick => todo!(), 186 | PromoType::Openhouse => todo!(), 187 | PromoType::Planeswalkerdeck => todo!(), 188 | PromoType::Plastic => todo!(), 189 | PromoType::Playerrewards => todo!(), 190 | PromoType::Playpromo => todo!(), 191 | PromoType::Playtest => todo!(), 192 | PromoType::Portrait => todo!(), 193 | PromoType::Poster => todo!(), 194 | PromoType::Premiereshop => todo!(), 195 | PromoType::Prerelease => todo!(), 196 | PromoType::Promopack => todo!(), 197 | PromoType::Rainbowfoil => todo!(), 198 | PromoType::Raisedfoil => todo!(), 199 | PromoType::Ravnicacity => todo!(), 200 | PromoType::Rebalanced => todo!(), 201 | PromoType::Release => todo!(), 202 | PromoType::Resale => todo!(), 203 | PromoType::Ripplefoil => todo!(), 204 | PromoType::Schinesealtart => todo!(), 205 | PromoType::Scroll => todo!(), 206 | PromoType::Serialized => todo!(), 207 | PromoType::Setextension => todo!(), 208 | PromoType::Setpromo => todo!(), 209 | PromoType::Silverfoil => todo!(), 210 | PromoType::Sldbonus => todo!(), 211 | PromoType::Stamped => todo!(), 212 | PromoType::Startercollection => todo!(), 213 | PromoType::Starterdeck => todo!(), 214 | PromoType::Stepandcompleat => todo!(), 215 | PromoType::Storechampionship => todo!(), 216 | PromoType::Surgefoil => todo!(), 217 | PromoType::Textured => todo!(), 218 | PromoType::Themepack => todo!(), 219 | PromoType::Thick => todo!(), 220 | PromoType::Tourney => todo!(), 221 | PromoType::Unknown => todo!(), 222 | PromoType::UpsideDown => todo!(), 223 | PromoType::UpsideDownBack => todo!(), 224 | PromoType::Vault => todo!(), 225 | PromoType::Wizardsplaynetwork => todo!(), 226 | } 227 | } 228 | 229 | #[allow(dead_code)] 230 | fn match_on_security_stamp(s: SecurityStamp) { 231 | match s { 232 | SecurityStamp::Oval => todo!(), 233 | SecurityStamp::Triangle => todo!(), 234 | SecurityStamp::Acorn => todo!(), 235 | SecurityStamp::Circle => todo!(), 236 | SecurityStamp::Arena => todo!(), 237 | SecurityStamp::Heart => todo!(), 238 | SecurityStamp::Unknown => todo!(), 239 | } 240 | } 241 | 242 | #[allow(dead_code)] 243 | fn match_on_finishes(f: Finishes) { 244 | match f { 245 | Finishes::Nonfoil => todo!(), 246 | Finishes::Foil => todo!(), 247 | Finishes::Etched => todo!(), 248 | Finishes::Unknown => todo!(), 249 | } 250 | } 251 | 252 | #[test] 253 | fn deserialize() { 254 | assert_eq!( 255 | serde_json::from_str::(r#""frontier""#).unwrap(), 256 | FrameEffect::Unknown, 257 | ); 258 | assert_eq!( 259 | serde_json::from_str::(r#""frontier""#).unwrap(), 260 | Layout::Unknown, 261 | ); 262 | assert_eq!( 263 | serde_json::from_str::(r#""frontier""#).unwrap(), 264 | SetType::Unknown, 265 | ); 266 | assert_eq!( 267 | serde_json::from_str::(r#""frontier""#).unwrap(), 268 | PromoType::Unknown, 269 | ); 270 | assert_eq!( 271 | serde_json::from_str::(r#""frontier""#).unwrap(), 272 | SecurityStamp::Unknown, 273 | ); 274 | assert_eq!( 275 | serde_json::from_str::(r#""foo""#).unwrap(), 276 | Finishes::Unknown, 277 | ); 278 | } 279 | --------------------------------------------------------------------------------