1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
//! The game Marvel Snap allows sharing decks through the use of encoded strings.
//! This simple crate supports both encoding and decoding of that data to support
//! building other tools on top of the deck information.

#![forbid(unsafe_code)]
#![warn(missing_docs, missing_debug_implementations, rust_2018_idioms)]

use base64::DecodeError;
use base64::{engine::general_purpose, Engine as _};
use serde_derive::Deserialize;
use serde_derive::Serialize;
use thiserror::Error;

/// List of errors returned from a Result
#[derive(Debug, Error)]
pub enum DeckListError {
    /// This error should not occur and likley points to an underlying
    /// issue with string encoding. Make sure passed in data is valid.
    #[error("Failed to encode bytes")]
    EncodingError,

    /// Likely a bad code, this is a common error and should fail gracefully
    #[error("Failed to decode data as base64")]
    DecodingError(#[from] DecodeError),

    /// Likely a bad code, this is a common error and should fail gracefully
    #[error("Invalid data")]
    InvalidDeckInput,
}

/// The game Marvel Snap allows sharing decks through the use of encoded strings.
/// This simple crate supports both encoding and decoding of that data to support
/// building other tools on top of the deck information.
///
/// It does not include actual card data to keep this library simple as cards are
/// added to the pool frequently enough that this would get stale.
///
/// # Example (To Share)
///
/// ```rust
/// use marvelsnapdeck::DeckList;
///
/// let mut list = DeckList::new();
/// list.set_name("Thanos".to_string());
/// list.set_cards(&["AntMan", "Agent13", "Quinjet", "Angela",
/// "Okoye", "Armor", "Falcon", "Mystique", "Lockjaw",
/// "KaZar", "DevilDinosaur", "Thanos"]);
/// let code = list.into_code().unwrap();
/// ```
///
/// # Example (From Code)
///
/// ```no_run
/// use marvelsnapdeck::DeckList;
///
/// let clipboard = "...";
/// let mut list = DeckList::from_code(clipboard);
/// ```
#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct DeckList {
    #[serde(rename = "Name")]
    name: String,
    #[serde(rename = "Cards")]
    cards: Vec<Card>,
}

/// An individual card
#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Card {
    #[serde(rename = "CardDefId")]
    name: String,
}

impl DeckList {
    /// Create an empty DeckList to prepare
    pub fn new() -> Self {
        Self {
            name: Default::default(),
            cards: Default::default(),
        }
    }

    /// Set the deck name visible to the player in game
    ///
    /// # Example
    ///
    /// ```rust
    /// use marvelsnapdeck::DeckList;
    ///
    /// let mut list = DeckList::new();
    /// list.set_name("Thanos".into());
    ///
    /// assert_eq!(list.name(), "Thanos");
    /// ```
    pub fn set_name(&mut self, name: String) {
        self.name = name;
    }

    /// Gets the deck name visible to the player in game
    ///
    /// # Example
    ///
    /// ```rust
    /// use marvelsnapdeck::DeckList;
    ///
    /// let mut list = DeckList::new();
    /// list.set_name("Thanos".into());
    ///
    /// assert_eq!(list.name(), "Thanos");
    /// ```
    pub fn name(&self) -> &str {
        self.name.as_str()
    }

    /// Set the list of cards.
    ///  
    /// # Example
    ///
    /// ```rust
    /// use marvelsnapdeck::DeckList;
    ///
    /// let mut list = DeckList::new();
    /// list.set_cards(&["AntMan"]);
    ///
    /// let output = list.cards();
    ///
    /// assert_eq!(output[0], "AntMan");
    /// ```
    pub fn set_cards<T: AsRef<str> + std::fmt::Display>(&mut self, cards: &[T]) {
        let list = cards
            .iter()
            .map(|name| Card {
                name: name.to_string(),
            })
            .collect();
        self.cards = list;
    }

    /// Get list of cards as a vector of strings
    ///
    /// # Example
    ///
    /// ```rust
    /// use marvelsnapdeck::DeckList;
    ///
    /// let mut list = DeckList::new();
    /// list.set_cards(&["AntMan"]);
    ///
    /// let output = list.cards();
    ///
    /// assert_eq!(output[0], "AntMan");
    /// ```
    pub fn cards(&self) -> Vec<String> {
        self.cards.iter().map(|card| card.name.clone()).collect()
    }

    /// Convert a string copied from Marvel Snap into a DeckList.
    ///
    /// # Panics
    ///
    /// Panics if the code cannot be resolved into a valid DeckList struct.
    pub fn from_code<T: AsRef<[u8]>>(code: T) -> Result<Self, DeckListError> {
        let value = general_purpose::STANDARD_NO_PAD
            .decode(code)
            .map_err(DeckListError::DecodingError)?;

        let json: DeckList = serde_json::from_slice(value.as_slice())
            .map_err(|_| DeckListError::InvalidDeckInput)?;

        Ok(json)
    }

    /// Converts DeckList into a string for pasting into Marvel Snap
    ///
    /// For a complete deck, make sure to set both the deck name and include 12 valid cards.
    /// For simplicity, this library does not validate if the cards exist in the game.
    ///
    /// # Example
    /// ```rust
    /// use marvelsnapdeck::DeckList;
    ///
    /// let mut list = DeckList::new();
    /// list.set_name("Thanos".to_string());
    /// list.set_cards(&["AntMan", "Agent13", "Quinjet", "Angela",
    /// "Okoye", "Armor", "Falcon", "Mystique", "Lockjaw",
    /// "KaZar", "DevilDinosaur", "Thanos"]);
    /// let code = list.into_code().unwrap();
    /// ```
    ///
    /// # Panics
    ///
    /// Panics if the underlying card list fails to encode as a string
    pub fn into_code(&self) -> Result<String, DeckListError> {
        let data = serde_json::to_string(self).map_err(|_| DeckListError::EncodingError)?;

        let code = general_purpose::STANDARD_NO_PAD.encode(data);

        Ok(code)
    }
}

#[cfg(test)]
mod tests {
    use crate::DeckList;

    const VALID_CODE: &'static str = "eyJOYW1lIjoiVGhhbm9zIiwiQ2FyZHMiOlt7IkNhcmREZWZJZCI6IkFudE1hbiJ9LHsiQ2FyZERlZklkIjoiQWdlbnQxMyJ9LHsiQ2FyZERlZklkIjoiUXVpbmpldCJ9LHsiQ2FyZERlZklkIjoiQW5nZWxhIn0seyJDYXJkRGVmSWQiOiJPa295ZSJ9LHsiQ2FyZERlZklkIjoiQXJtb3IifSx7IkNhcmREZWZJZCI6IkZhbGNvbiJ9LHsiQ2FyZERlZklkIjoiTXlzdGlxdWUifSx7IkNhcmREZWZJZCI6IkxvY2tqYXcifSx7IkNhcmREZWZJZCI6IkthWmFyIn0seyJDYXJkRGVmSWQiOiJEZXZpbERpbm9zYXVyIn0seyJDYXJkRGVmSWQiOiJUaGFub3MifV19";

    #[test]
    fn decode_is_valid() {
        let list = DeckList::from_code(&VALID_CODE.to_string()).unwrap();
        assert_eq!(list.name(), "Thanos");
        assert_eq!(list.cards.len(), 12);
    }

    #[test]
    fn decode_cards() {
        let list = DeckList::from_code(&VALID_CODE.to_string()).unwrap();
        let cards = list.cards();

        assert_eq!(cards.len(), 12);
        assert_eq!(cards[0], "AntMan");
    }

    #[test]
    fn encode_cards() {
        let mut list = DeckList::new();
        list.set_name("Thanos".to_string());
        list.set_cards(&[
            "AntMan",
            "Agent13",
            "Quinjet",
            "Angela",
            "Okoye",
            "Armor",
            "Falcon",
            "Mystique",
            "Lockjaw",
            "KaZar",
            "DevilDinosaur",
            "Thanos",
        ]);
        let code = list.into_code().unwrap();
        assert_eq!(code, VALID_CODE.to_string());
    }
}