1#![deny(missing_docs)]
2
3use rgb::RGB8;
12#[cfg(feature = "serde")]
13use serde::{Deserialize, Serialize};
14use url::Url;
15
16#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
21#[derive(Debug, Clone, PartialEq, Eq, Hash)]
22pub struct Metadata {
23 pub image: Url,
25 pub external_url: Option<Url>,
27 pub description: String,
29 pub name: String,
31 #[cfg_attr(feature = "serde", serde(default))]
33 pub attributes: Vec<AttributeEntry>,
34 #[cfg_attr(feature = "serde", serde(with = "rgb8_fromhex_opt", default))]
37 pub background_color: Option<RGB8>,
38 pub animation_url: Option<Url>,
40 pub youtube_url: Option<Url>,
42}
43
44#[cfg_attr(
46 feature = "serde",
47 derive(Serialize, Deserialize),
48 serde(rename_all = "snake_case"),
49 serde(untagged)
50)]
51#[derive(Debug, Clone, PartialEq, Eq, Hash)]
52pub enum AttributeEntry {
53 String {
55 trait_type: String,
57 value: String,
59 },
60 Number {
62 trait_type: String,
64 value: u64,
66 display_type: Option<DisplayType>,
68 },
69}
70
71#[cfg_attr(
73 feature = "serde",
74 derive(Serialize, Deserialize),
75 serde(rename_all = "snake_case")
76)]
77#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
78pub enum DisplayType {
79 Number,
81 BoostPercentage,
83 BoostNumber,
85 Date,
87}
88
89#[cfg(feature = "serde")]
90mod rgb8_fromhex_opt {
91 use rgb::{ComponentSlice, RGB8};
92 use serde::{de::Error, Deserializer, Serialize, Serializer};
93
94 pub fn deserialize<'de, D>(deserializer: D) -> Result<Option<RGB8>, D::Error>
95 where
96 D: Deserializer<'de>,
97 {
98 match hex::deserialize(deserializer)
99 .map::<Option<Vec<_>>, _>(Some)
100 .unwrap_or_default()
101 {
102 Some(s) => {
103 if s.len() != 3 {
104 Err(D::Error::custom("expected color hex string"))
105 } else {
106 Ok(Some(RGB8 {
107 r: s[0],
108 g: s[1],
109 b: s[2],
110 }))
111 }
112 }
113 None => Ok(None),
114 }
115 }
116
117 pub fn serialize<S>(value: &Option<RGB8>, serializer: S) -> Result<S::Ok, S::Error>
118 where
119 S: Serializer,
120 {
121 match value {
122 Some(value) => hex::serialize(value.as_slice().to_vec(), serializer),
123 None => None::<()>.serialize(serializer),
124 }
125 }
126
127 #[cfg(test)]
128 mod tests {
129 use rgb::RGB8;
130 use serde::{Deserialize, Serialize};
131
132 #[derive(Serialize, Deserialize, Debug)]
133 struct Target {
134 #[serde(with = "crate::rgb8_fromhex_opt")]
135 color: Option<RGB8>,
136 }
137
138 #[test]
139 fn from_json() {
140 let s = r#"{ "color": "f2f2f2" }"#;
141 let target: Target = serde_json::from_str(s).unwrap();
142 assert_eq!(
143 target.color,
144 Some(RGB8 {
145 r: 242,
146 g: 242,
147 b: 242
148 })
149 );
150 }
151
152 #[test]
153 fn to_json() {
154 let target = Target {
155 color: Some(RGB8 {
156 r: 242,
157 g: 242,
158 b: 242,
159 }),
160 };
161 let s = serde_json::to_string(&target).unwrap();
162 assert_eq!(s, r#"{"color":"f2f2f2"}"#)
163 }
164
165 #[test]
166 fn from_notcolor_json() {
167 let s = r#"{ "color": "f2f2f2f2" }"#;
168 let target = serde_json::from_str::<Target>(s);
169 assert!(target.is_err());
170 }
171
172 #[test]
173 fn from_null_json() {
174 let s = r#"{ "color": null }"#;
175 let target = serde_json::from_str::<Target>(s).unwrap();
176 assert!(target.color.is_none());
177 }
178 }
179}
180
181#[cfg(feature = "serde")]
182#[cfg(test)]
183mod tests {
184 use crate::Metadata;
185
186 const PLANETPASS_ITEM: &str = r#"
187 {
188 "image": "https://assets.wanderers.ai/file/planetpass/vid/0/0.mp4",
189 "description": "Visit this planet and get a free Rocketeer NFT from Alucard.eth!",
190 "name": "Rocketeer X",
191 "attributes": [
192 {
193 "trait_type": "Core",
194 "value": "Vortex"
195 },
196 {
197 "trait_type": "Satellite",
198 "value": "Protoplanets"
199 },
200 {
201 "trait_type": "Feature",
202 "value": "Icy"
203 },
204 {
205 "trait_type": "Ship",
206 "value": "Docking"
207 },
208 {
209 "trait_type": "Space",
210 "value": "Green Sun"
211 },
212 {
213 "trait_type": "Terrain",
214 "value": "Layers"
215 },
216 {
217 "trait_type": "Faction",
218 "value": "Coalition for Uncorrupted Biology"
219 },
220 {
221 "trait_type": "Atmosphere",
222 "value": "Alpen Glow"
223 }
224 ]
225 }
226 "#;
227
228 #[test]
229 pub fn planetpass() {
230 let metadata = serde_json::from_str::<Metadata>(PLANETPASS_ITEM);
231 assert!(metadata.is_ok());
232 }
233}