1use std::collections::BTreeMap;
2
3use serde::{Deserialize, Serialize};
4use serde_json::value::Value;
5
6use crate::sensors::Sensors;
7
8type Extensions = BTreeMap<String, Value>;
9
10#[derive(Serialize, Deserialize, Default, Debug, Clone, PartialEq)]
11pub struct Location {
12 #[serde(skip_serializing_if = "Option::is_none")]
13 pub address: Option<String>,
14 #[serde(skip_serializing_if = "Option::is_none")]
18 pub osm_link: Option<String>,
19 pub lat: f64,
20 pub lon: f64,
21 #[serde(skip_serializing_if = "Option::is_none")]
22 pub timezone: Option<String>,
23}
24
25#[derive(Serialize, Deserialize, Default, Debug, Clone, PartialEq, Eq)]
26pub struct Spacefed {
27 pub spacenet: bool,
28 pub spacesaml: bool,
29 pub spacephone: Option<bool>,
30}
31
32impl Spacefed {
33 fn verify(&self, version: StatusBuilderVersion) -> Result<(), String> {
34 if version == StatusBuilderVersion::V14 {
35 if self.spacephone.is_some() {
36 return Err("spacefed.spacephone key was removed".into());
37 }
38 } else if self.spacephone.is_none() {
39 return Err("spacefed.spacephone must be present".into());
40 }
41 Ok(())
42 }
43}
44
45#[derive(Serialize, Deserialize, Default, Debug, Clone, PartialEq, Eq)]
46pub struct Icon {
47 pub open: String,
48 pub closed: String,
49}
50
51#[derive(Serialize, Deserialize, Default, Debug, Clone, PartialEq, Eq)]
52pub struct State {
53 pub open: Option<bool>,
54 #[serde(skip_serializing_if = "Option::is_none")]
55 pub lastchange: Option<u64>,
56 #[serde(skip_serializing_if = "Option::is_none")]
57 pub trigger_person: Option<String>,
58 #[serde(skip_serializing_if = "Option::is_none")]
59 pub message: Option<String>,
60 #[serde(skip_serializing_if = "Option::is_none")]
61 pub icon: Option<Icon>,
62}
63
64impl State {
65 fn verify(&self, _version: StatusBuilderVersion) -> Result<(), String> {
66 Ok(())
67 }
68}
69
70#[derive(Serialize, Deserialize, Default, Debug, Clone, PartialEq, Eq)]
71pub struct Event {
72 pub name: String,
73 #[serde(rename = "type")]
74 pub type_: String,
75 pub timestamp: u64,
76 #[serde(skip_serializing_if = "Option::is_none")]
77 pub extra: Option<String>,
78}
79
80#[derive(Serialize, Deserialize, Default, Debug, Clone, PartialEq, Eq)]
81pub struct Keymaster {
82 #[serde(skip_serializing_if = "Option::is_none")]
83 pub name: Option<String>,
84 #[serde(skip_serializing_if = "Option::is_none")]
85 pub irc_nick: Option<String>,
86 #[serde(skip_serializing_if = "Option::is_none")]
87 pub phone: Option<String>,
88 #[serde(skip_serializing_if = "Option::is_none")]
89 pub email: Option<String>,
90 #[serde(skip_serializing_if = "Option::is_none")]
91 pub twitter: Option<String>,
92 #[serde(skip_serializing_if = "Option::is_none")]
93 pub xmpp: Option<String>,
94 #[serde(skip_serializing_if = "Option::is_none")]
95 pub mastodon: Option<String>,
96}
97
98#[derive(Serialize, Deserialize, Default, Debug, Clone, PartialEq, Eq)]
99pub struct GoogleContact {
100 #[serde(skip_serializing_if = "Option::is_none")]
101 pub plus: Option<String>,
102}
103
104#[derive(Serialize, Deserialize, Default, Debug, Clone, PartialEq, Eq)]
105pub struct Contact {
106 #[serde(skip_serializing_if = "Option::is_none")]
107 pub phone: Option<String>,
108 #[serde(skip_serializing_if = "Option::is_none")]
109 pub sip: Option<String>,
110 #[serde(skip_serializing_if = "Option::is_none")]
111 pub keymasters: Option<Vec<Keymaster>>,
112 #[serde(skip_serializing_if = "Option::is_none")]
113 pub irc: Option<String>,
114 #[serde(skip_serializing_if = "Option::is_none")]
115 pub twitter: Option<String>,
116 #[serde(skip_serializing_if = "Option::is_none")]
117 pub facebook: Option<String>,
118 #[serde(skip_serializing_if = "Option::is_none")]
119 pub google: Option<GoogleContact>,
120 #[serde(skip_serializing_if = "Option::is_none")]
121 pub identica: Option<String>,
122 #[serde(skip_serializing_if = "Option::is_none")]
123 pub foursquare: Option<String>,
124 #[serde(skip_serializing_if = "Option::is_none")]
125 pub email: Option<String>,
126 #[serde(skip_serializing_if = "Option::is_none")]
127 pub ml: Option<String>,
128 #[serde(skip_serializing_if = "Option::is_none")]
129 pub jabber: Option<String>,
130 #[serde(skip_serializing_if = "Option::is_none")]
131 pub xmpp: Option<String>,
132 #[serde(skip_serializing_if = "Option::is_none")]
133 pub issue_mail: Option<String>,
134 #[serde(skip_serializing_if = "Option::is_none")]
135 pub mumble: Option<String>,
136 #[serde(skip_serializing_if = "Option::is_none")]
137 pub matrix: Option<String>,
138 #[serde(skip_serializing_if = "Option::is_none")]
139 pub mastodon: Option<String>,
140 #[serde(skip_serializing_if = "Option::is_none")]
141 pub gopher: Option<String>,
142}
143
144#[derive(Serialize, Deserialize, Default, Debug, Clone, PartialEq, Eq)]
145pub struct Feed {
146 #[serde(rename = "type", skip_serializing_if = "Option::is_none")]
147 pub type_: Option<String>,
148 pub url: String,
149}
150
151#[derive(Serialize, Deserialize, Default, Debug, Clone, PartialEq, Eq)]
152pub struct Feeds {
153 #[serde(skip_serializing_if = "Option::is_none")]
154 pub blog: Option<Feed>,
155 #[serde(skip_serializing_if = "Option::is_none")]
156 pub wiki: Option<Feed>,
157 #[serde(skip_serializing_if = "Option::is_none")]
158 pub calendar: Option<Feed>,
159 #[serde(skip_serializing_if = "Option::is_none")]
160 pub flickr: Option<Feed>,
161}
162
163#[derive(Serialize, Deserialize, Default, Debug, Clone, PartialEq, Eq)]
164pub struct Cache {
165 pub schedule: String,
166}
167
168#[derive(Serialize, Deserialize, Default, Debug, Clone, PartialEq, Eq)]
169pub struct RadioShow {
170 pub name: String,
171 pub url: String,
172 #[serde(rename = "type")]
173 pub type_: String,
174 pub start: String,
175 pub end: String,
176}
177
178#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)]
179#[serde(rename_all = "snake_case")]
180pub enum IssueReportChannel {
181 Email,
182 IssueMail,
183 Twitter,
184 Ml,
185}
186
187#[derive(Serialize, Deserialize, Default, Debug, Clone, PartialEq, Eq)]
188pub struct Stream {
189 #[serde(skip_serializing_if = "Option::is_none")]
190 pub m4: Option<String>,
191 #[serde(skip_serializing_if = "Option::is_none")]
192 pub mjpeg: Option<String>,
193 #[serde(skip_serializing_if = "Option::is_none")]
194 pub ustream: Option<String>,
195}
196
197#[derive(Serialize, Deserialize, Default, Debug, Clone, PartialEq, Eq)]
198pub struct Link {
199 pub name: String,
200 #[serde(skip_serializing_if = "Option::is_none")]
201 pub description: Option<String>,
202 pub url: String,
203}
204
205#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)]
206#[serde(rename_all = "snake_case")]
207pub enum BillingInterval {
208 Yearly,
209 Monthly,
210 Weekly,
211 Daily,
212 Hourly,
213 Other,
214}
215
216impl Default for BillingInterval {
217 fn default() -> Self {
218 BillingInterval::Monthly
219 }
220}
221
222#[derive(Serialize, Deserialize, Default, Debug, Clone, PartialEq)]
223pub struct MembershipPlan {
224 pub name: String,
225 pub value: f64,
226 pub currency: String,
227 pub billing_interval: BillingInterval,
228 #[serde(skip_serializing_if = "Option::is_none")]
229 pub description: Option<String>,
230}
231
232#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)]
233pub enum ApiVersion {
234 #[serde(rename = "14")]
235 V14,
236}
237
238#[derive(Serialize, Deserialize, Default, Debug, Clone, PartialEq)]
240pub struct Status {
241 #[serde(skip_serializing_if = "Option::is_none")]
243 pub api: Option<String>,
244
245 #[serde(skip_serializing_if = "Option::is_none")]
246 pub api_compatibility: Option<Vec<ApiVersion>>,
247
248 pub space: String,
249 pub logo: String,
250 pub url: String,
251 pub location: Location,
252 pub contact: Contact,
253
254 #[serde(skip_serializing_if = "Option::is_none")]
256 pub spacefed: Option<Spacefed>,
257 #[serde(skip_serializing_if = "Option::is_none")]
258 pub projects: Option<Vec<String>>,
259 #[serde(skip_serializing_if = "Option::is_none")]
260 pub cam: Option<Vec<String>>,
261 #[serde(skip_serializing_if = "Option::is_none")]
262 pub stream: Option<Stream>,
263 #[serde(skip_serializing_if = "Option::is_none")]
264 pub feeds: Option<Feeds>,
265 #[serde(skip_serializing_if = "Option::is_none")]
266 pub events: Option<Vec<Event>>,
267 #[serde(skip_serializing_if = "Option::is_none")]
268 pub radio_show: Option<Vec<RadioShow>>,
269 #[serde(skip_serializing_if = "Option::is_none")]
270 pub links: Option<Vec<Link>>,
271 #[serde(skip_serializing_if = "Option::is_none")]
272 pub membership_plans: Option<Vec<MembershipPlan>>,
273
274 #[serde(skip_serializing_if = "Option::is_none")]
276 pub cache: Option<Cache>,
277 #[serde(skip_serializing_if = "Vec::is_empty", default)]
278 pub issue_report_channels: Vec<IssueReportChannel>,
279
280 #[serde(skip_serializing_if = "Option::is_none")]
282 pub state: Option<State>,
283 #[serde(skip_serializing_if = "Option::is_none")]
284 pub sensors: Option<Sensors>,
285
286 #[serde(flatten)]
288 pub extensions: Extensions,
289}
290
291impl Status {
292 #[deprecated(
294 since = "0.5.0",
295 note = "Please use the `StatusBuilder` or a struct expression instead"
296 )]
297 pub fn new<S: Into<String>>(
298 space: S,
299 logo: S,
300 url: S,
301 location: Location,
302 contact: Contact,
303 issue_report_channels: Vec<IssueReportChannel>,
304 ) -> Status {
305 Status {
306 api: Some("0.13".into()),
307 space: space.into(),
308 logo: logo.into(),
309 url: url.into(),
310 location,
311 contact,
312 issue_report_channels,
313 ..Default::default()
314 }
315 }
316}
317
318#[derive(Debug, Copy, Clone, Eq, PartialEq)]
319enum StatusBuilderVersion {
320 V0_13,
321 V14,
322 Mixed,
323}
324
325impl Default for StatusBuilderVersion {
326 fn default() -> StatusBuilderVersion {
327 StatusBuilderVersion::V0_13
328 }
329}
330
331#[derive(Default, Debug, Clone)]
333pub struct StatusBuilder {
334 version: StatusBuilderVersion,
335 space: String,
336 logo: Option<String>,
337 url: Option<String>,
338 location: Option<Location>,
339 contact: Option<Contact>,
340 spacefed: Option<Spacefed>,
341 projects: Option<Vec<String>>,
342 cam: Option<Vec<String>>,
343 feeds: Option<Feeds>,
344 events: Option<Vec<Event>>,
345 radio_show: Option<Vec<RadioShow>>,
346 links: Option<Vec<Link>>,
347 membership_plans: Option<Vec<MembershipPlan>>,
348 issue_report_channels: Vec<IssueReportChannel>,
349 extensions: Extensions,
350 state: Option<State>,
351}
352
353impl StatusBuilder {
354 pub fn new<S: Into<String>>(space_name: S) -> StatusBuilder {
355 StatusBuilder {
356 space: space_name.into(),
357 ..Default::default()
358 }
359 }
360
361 pub fn v0_13<S: Into<String>>(space_name: S) -> StatusBuilder {
362 StatusBuilder {
363 space: space_name.into(),
364 version: StatusBuilderVersion::V0_13,
365 ..Default::default()
366 }
367 }
368
369 pub fn v14<S: Into<String>>(space_name: S) -> StatusBuilder {
370 StatusBuilder {
371 space: space_name.into(),
372 version: StatusBuilderVersion::V14,
373 ..Default::default()
374 }
375 }
376
377 pub fn mixed<S: Into<String>>(space_name: S) -> StatusBuilder {
378 StatusBuilder {
379 space: space_name.into(),
380 version: StatusBuilderVersion::Mixed,
381 ..Default::default()
382 }
383 }
384
385 pub fn state(mut self, state: State) -> Self {
386 self.state = Some(state);
387 self
388 }
389
390 pub fn logo<S: Into<String>>(mut self, logo: S) -> Self {
391 self.logo = Some(logo.into());
392 self
393 }
394
395 pub fn url<S: Into<String>>(mut self, url: S) -> Self {
396 self.url = Some(url.into());
397 self
398 }
399
400 pub fn location(mut self, location: Location) -> Self {
401 self.location = Some(location);
402 self
403 }
404
405 pub fn contact(mut self, contact: Contact) -> Self {
406 self.contact = Some(contact);
407 self
408 }
409
410 pub fn spacefed(mut self, spacefed: Spacefed) -> Self {
411 self.spacefed = Some(spacefed);
412 self
413 }
414
415 pub fn add_event(mut self, event: Event) -> Self {
416 self.events.get_or_insert(vec![]).push(event);
417 self
418 }
419
420 pub fn add_cam<S: Into<String>>(mut self, cam: S) -> Self {
421 self.cam.get_or_insert(vec![]).push(cam.into());
422 self
423 }
424
425 pub fn feeds(mut self, feeds: Feeds) -> Self {
426 self.feeds = Some(feeds);
427 self
428 }
429
430 pub fn add_radio_show(mut self, radio_show: RadioShow) -> Self {
431 self.radio_show.get_or_insert(vec![]).push(radio_show);
432 self
433 }
434
435 pub fn add_link(mut self, link: Link) -> Self {
436 self.links.get_or_insert(vec![]).push(link);
437 self
438 }
439
440 pub fn add_membership_plan(mut self, membership_plan: MembershipPlan) -> Self {
441 self.membership_plans.get_or_insert(vec![]).push(membership_plan);
442 self
443 }
444
445 pub fn add_project<S: Into<String>>(mut self, project: S) -> Self {
446 self.projects.get_or_insert(vec![]).push(project.into());
447 self
448 }
449
450 pub fn add_issue_report_channel(mut self, report_channel: IssueReportChannel) -> Self {
451 self.issue_report_channels.push(report_channel);
452 self
453 }
454
455 pub fn add_extension<V: Into<Value>>(mut self, name: &str, value: V) -> Self {
459 if name.starts_with("ext_") {
460 self.extensions.insert(name.to_owned(), value.into());
461 } else {
462 self.extensions.insert(format!("ext_{}", name), value.into());
463 }
464 self
465 }
466
467 pub fn build(self) -> Result<Status, String> {
468 let api = match self.version {
469 StatusBuilderVersion::V0_13 | StatusBuilderVersion::Mixed => Some("0.13".to_owned()),
470 _ => None,
471 };
472 let api_compatibility = match self.version {
473 StatusBuilderVersion::V14 | StatusBuilderVersion::Mixed => Some(vec![ApiVersion::V14]),
474 _ => None,
475 };
476
477 let contact = self.contact.ok_or("contact missing")?;
478 if let Some(spacefed) = &self.spacefed {
479 spacefed.verify(self.version)?;
480 }
481 if let Some(state) = &self.state {
482 state.verify(self.version)?;
483 }
484
485 if self.version == StatusBuilderVersion::V14 {
486 if contact.jabber.is_some() {
487 return Err("jabber key under contact was renamed to xmpp".into());
488 }
489 if contact.google.is_some() {
490 return Err("google key under contact was removed".into());
491 }
492 if self.radio_show.is_some() {
493 return Err("radio_show key was removed".into());
494 }
495
496 if !self.issue_report_channels.is_empty() {
497 return Err("issue_report_channels key was removed".into());
498 }
499 } else {
500 if self.issue_report_channels.is_empty() {
501 return Err("issue_report_channels must not be empty".into());
502 }
503 if self.state.is_none() {
504 return Err("state must be present in v0.13".into());
505 }
506 if let Some(ref location) = self.location {
507 if location.timezone.is_some() {
508 return Err("location.timezone is only present in v0.14 and above".into());
509 }
510 }
511 if self.links.is_some() {
512 return Err("links is only present in v0.14 and above".into());
513 }
514 if self.membership_plans.is_some() {
515 return Err("membership_plans is only present in v0.14 and above".into());
516 }
517 }
518
519 Ok(Status {
520 api,
521 api_compatibility,
522 space: self.space,
523 logo: self.logo.ok_or("logo missing")?,
524 url: self.url.ok_or("url missing")?,
525 location: self.location.ok_or("location missing")?,
526 contact,
527 spacefed: self.spacefed,
528 projects: self.projects,
529 cam: self.cam,
530 feeds: self.feeds,
531 events: self.events,
532 radio_show: self.radio_show,
533 links: self.links,
534 membership_plans: self.membership_plans,
535 issue_report_channels: self.issue_report_channels,
536 state: self.state,
537 extensions: self.extensions,
538 ..Default::default()
539 })
540 }
541}
542
543#[cfg(test)]
544mod test {
545 use super::*;
546 use serde_json::{from_str, to_string};
547
548 #[test]
549 fn serialize_deserialize_cache() {
550 let a = Cache {
551 schedule: "bla".into(),
552 };
553 let b: Cache = from_str(&to_string(&a).unwrap()).unwrap();
554 assert_eq!(a.schedule, b.schedule);
555 }
556
557 #[test]
558 fn serialize_deserialize_simple_contact() {
559 let a: Contact = Contact {
560 keymasters: Some(vec![Keymaster {
561 name: Some("Joe".into()),
562 email: Some("joe@example.com".into()),
563 ..Keymaster::default()
564 }]),
565 irc: Some("bla".into()),
566 google: Some(GoogleContact {
567 plus: Some("http://gplus/profile".into()),
568 }),
569 email: Some("bli@bla".into()),
570 ..Default::default()
571 };
572 let b: Contact = from_str(&to_string(&a).unwrap()).unwrap();
573
574 assert_eq!(a, b);
575 }
576
577 #[test]
578 fn test_builder_v13_fail_on_location_timezone() {
579 let status = StatusBuilder::v0_13("foo")
580 .logo("bar")
581 .url("foobar")
582 .location(Location {
583 timezone: Some("Europe/London".into()),
584 ..Default::default()
585 })
586 .contact(Contact::default())
587 .add_issue_report_channel(IssueReportChannel::Email)
588 .state(State {
589 open: Some(false),
590 ..State::default()
591 })
592 .build();
593 assert!(status.is_err());
594 assert_eq!(
595 status.err().unwrap(),
596 "location.timezone is only present in v0.14 and above"
597 );
598 }
599
600 #[test]
601 fn test_builder_v13_fail_on_links() {
602 let status = StatusBuilder::v0_13("foo")
603 .logo("bar")
604 .url("foobar")
605 .contact(Contact::default())
606 .add_issue_report_channel(IssueReportChannel::Email)
607 .state(State {
608 open: Some(false),
609 ..State::default()
610 })
611 .add_link(Link::default())
612 .build();
613 assert!(status.is_err());
614 assert_eq!(status.err().unwrap(), "links is only present in v0.14 and above");
615 }
616
617 #[test]
618 fn test_builder_v13_fail_on_membership_plans() {
619 let status = StatusBuilder::v0_13("foo")
620 .logo("bar")
621 .url("foobar")
622 .contact(Contact::default())
623 .add_issue_report_channel(IssueReportChannel::Email)
624 .state(State {
625 open: Some(false),
626 ..State::default()
627 })
628 .add_membership_plan(MembershipPlan::default())
629 .build();
630 assert!(status.is_err());
631 assert_eq!(
632 status.err().unwrap(),
633 "membership_plans is only present in v0.14 and above"
634 );
635 }
636
637 #[test]
638 fn test_builder_v14() {
639 let status = StatusBuilder::v14("foo")
640 .logo("bar")
641 .url("foobar")
642 .location(Location::default())
643 .contact(Contact::default())
644 .add_link(Link::default())
645 .add_membership_plan(MembershipPlan::default())
646 .build()
647 .unwrap();
648 assert_eq!(
649 status,
650 Status {
651 api: None,
652 api_compatibility: Some(vec![ApiVersion::V14]),
653 space: "foo".into(),
654 logo: "bar".into(),
655 url: "foobar".into(),
656 issue_report_channels: vec![],
657 links: Some(vec![Link::default()]),
658 membership_plans: Some(vec![MembershipPlan::default()]),
659 ..Status::default()
660 }
661 );
662 }
663
664 #[test]
665 fn test_builder_v14_fail_on_jabber() {
666 let status = StatusBuilder::v14("foo")
667 .logo("bar")
668 .url("foobar")
669 .location(Location::default())
670 .contact(Contact {
671 jabber: Some("jabber".into()),
672 ..Contact::default()
673 })
674 .build();
675 assert!(status.is_err());
676 }
677
678 #[test]
679 fn test_builder_v14_fail_on_google() {
680 let status = StatusBuilder::v14("foo")
681 .logo("bar")
682 .url("foobar")
683 .location(Location::default())
684 .contact(Contact {
685 google: Some(GoogleContact::default()),
686 ..Contact::default()
687 })
688 .build();
689 assert!(status.is_err());
690 }
691
692 #[test]
693 fn test_builder_v14_fail_on_radio_show() {
694 let status = StatusBuilder::v14("foo")
695 .logo("bar")
696 .url("foobar")
697 .location(Location::default())
698 .contact(Contact::default())
699 .add_radio_show(RadioShow::default())
700 .build();
701 assert!(status.is_err());
702 }
703
704 #[test]
705 fn test_builder_v14_fail_on_spacephone() {
706 let status = StatusBuilder::v14("foo")
707 .logo("bar")
708 .url("foobar")
709 .location(Location::default())
710 .contact(Contact::default())
711 .spacefed(Spacefed {
712 spacephone: Some(true),
713 ..Spacefed::default()
714 })
715 .build();
716 assert!(status.is_err());
717 }
718
719 #[test]
720 fn test_builder_mixed() {
721 let status = StatusBuilder::mixed("foo")
722 .logo("bar")
723 .url("foobar")
724 .state(State::default())
725 .location(Location::default())
726 .contact(Contact::default())
727 .add_issue_report_channel(IssueReportChannel::Email)
728 .build()
729 .unwrap();
730 assert_eq!(
731 status,
732 Status {
733 api: Some("0.13".into()),
734 api_compatibility: Some(vec![ApiVersion::V14]),
735 space: "foo".into(),
736 logo: "bar".into(),
737 url: "foobar".into(),
738 state: Some(State {
739 open: None,
740 ..State::default()
741 }),
742 issue_report_channels: vec![IssueReportChannel::Email],
743 ..Status::default()
744 }
745 );
746 }
747
748 #[test]
749 fn test_builder() {
750 let status = StatusBuilder::new("foo")
751 .logo("bar")
752 .url("foobar")
753 .state(State {
754 open: Some(false),
755 ..State::default()
756 })
757 .location(Location::default())
758 .contact(Contact::default())
759 .spacefed(Spacefed {
760 spacephone: Some(false),
761 ..Spacefed::default()
762 })
763 .feeds(Feeds::default())
764 .add_project("spaceapi-rs")
765 .add_cam("cam1")
766 .add_cam("cam2".to_string())
767 .add_event(Event::default())
768 .add_issue_report_channel(IssueReportChannel::Email)
769 .build()
770 .unwrap();
771 assert_eq!(status.api, Some("0.13".into()));
772 assert_eq!(status.space, "foo");
773 assert_eq!(status.logo, "bar");
774 assert_eq!(status.url, "foobar");
775 assert_eq!(status.location, Location::default());
776 assert_eq!(status.contact, Contact::default());
777 assert_eq!(
778 status.spacefed,
779 Some(Spacefed {
780 spacephone: Some(false),
781 ..Spacefed::default()
782 })
783 );
784 assert_eq!(status.feeds, Some(Feeds::default()));
785 assert_eq!(status.projects, Some(vec!["spaceapi-rs".to_string()]));
786 assert_eq!(status.cam, Some(vec!["cam1".to_string(), "cam2".to_string()]));
787 assert_eq!(status.events, Some(vec![Event::default()]));
788 assert_eq!(status.issue_report_channels, vec![IssueReportChannel::Email]);
789 }
790
791 #[test]
792 fn serialize_skip_none() {
793 let f1 = Feed {
794 type_: Some("rss".to_string()),
795 url: "https://some/rss.xml".to_string(),
796 };
797 let f2 = Feed {
798 type_: None,
799 url: "https://some/rss.xml".to_string(),
800 };
801 assert_eq!(
802 to_string(&f1).unwrap(),
803 "{\"type\":\"rss\",\"url\":\"https://some/rss.xml\"}".to_string()
804 );
805 assert_eq!(
806 to_string(&f2).unwrap(),
807 "{\"url\":\"https://some/rss.xml\"}".to_string()
808 );
809 }
810
811 #[test]
812 fn serialize_deserialize_full() {
813 let status = StatusBuilder::new("foo")
814 .logo("bar")
815 .url("foobar")
816 .state(State {
817 open: Some(false),
818 ..State::default()
819 })
820 .location(Location::default())
821 .contact(Contact::default())
822 .add_extension("aaa", Value::Array(vec![Value::Null, Value::from(42)]))
823 .add_issue_report_channel(IssueReportChannel::Email)
824 .build()
825 .unwrap();
826 let serialized = to_string(&status).unwrap();
827 let deserialized = from_str::<Status>(&serialized).unwrap();
828 assert_eq!(status, deserialized);
829 }
830
831 #[test]
832 fn serialize_extension_fields_empty() {
833 let status = StatusBuilder::new("a")
834 .logo("b")
835 .url("c")
836 .state(State {
837 open: Some(false),
838 ..State::default()
839 })
840 .location(Location::default())
841 .contact(Contact::default())
842 .add_issue_report_channel(IssueReportChannel::Email)
843 .build();
844 assert!(status.is_ok());
845 assert_eq!(
846 &to_string(&status.unwrap()).unwrap(),
847 "{\"api\":\"0.13\",\"space\":\"a\",\"logo\":\"b\",\"url\":\"c\",\
848 \"location\":{\"lat\":0.0,\"lon\":0.0},\"contact\":{},\"issue_report_channels\":[\"email\"],\
849 \"state\":{\"open\":false}}"
850 );
851 }
852
853 #[test]
854 fn serialize_extension_fields() {
855 let status = StatusBuilder::new("a")
856 .logo("b")
857 .url("c")
858 .state(State {
859 open: Some(false),
860 ..State::default()
861 })
862 .location(Location::default())
863 .contact(Contact::default())
864 .add_issue_report_channel(IssueReportChannel::Email)
865 .add_extension("aaa", Value::String("xxx".into()))
866 .add_extension("bbb", Value::Array(vec![Value::Null, Value::from(42)]))
867 .build();
868 assert!(status.is_ok());
869 assert_eq!(
870 &to_string(&status.unwrap()).unwrap(),
871 "{\"api\":\"0.13\",\"space\":\"a\",\"logo\":\"b\",\"url\":\"c\",\
872 \"location\":{\"lat\":0.0,\"lon\":0.0},\"contact\":{},\"issue_report_channels\":[\"email\"],\
873 \"state\":{\"open\":false},\"ext_aaa\":\"xxx\",\"ext_bbb\":[null,42]}"
874 );
875 }
876
877 #[test]
878 fn serialize_deserialize_full_with_optional() {
879 let mut status = StatusBuilder::new("foo")
880 .logo("bar")
881 .url("foobar")
882 .state(State {
883 open: Some(false),
884 ..State::default()
885 })
886 .location(Location::default())
887 .contact(Contact::default())
888 .add_issue_report_channel(IssueReportChannel::Email)
889 .build()
890 .unwrap();
891 status.spacefed = Some(Spacefed::default());
892 status.feeds = Some(Feeds::default());
893 status.projects = Some(vec![]);
894 status.cam = Some(vec![]);
895
896 let serialized = to_string(&status).unwrap();
897 let deserialized = from_str::<Status>(&serialized).unwrap();
898 assert_eq!(status, deserialized);
899 }
900
901 #[test]
906 fn prepend_ext_to_extension_field_names() {
907 let status = StatusBuilder::new("a")
908 .logo("b")
909 .url("c")
910 .state(State {
911 open: Some(false),
912 ..State::default()
913 })
914 .location(Location::default())
915 .contact(Contact::default())
916 .add_issue_report_channel(IssueReportChannel::Email)
917 .add_extension("aaa", Value::String("xxx".into()))
918 .add_extension("ext_aaa", Value::String("yyy".into()))
919 .add_extension("bbb", Value::Null)
920 .add_extension("ext_ccc", Value::Null)
921 .build();
922 assert!(status.is_ok());
923 let serialized = to_string(&status.unwrap()).unwrap();
924 assert!(serialized.contains("\"ext_aaa\":\"yyy\""));
925 assert!(serialized.contains("\"ext_bbb\":null"));
926 assert!(serialized.contains("\"ext_ccc\":null"));
927 }
928
929 #[test]
930 fn deserialize_status() {
931 let data = r#"{
932 "api": "0.13",
933 "space": "a",
934 "logo": "b",
935 "url": "c",
936 "location": {
937 "lat": 0.0,
938 "lon": 0.0
939 },
940 "contact": {},
941 "issue_report_channels": [],
942 "state": {
943 "open": null,
944 "icon": {
945 "open": "d",
946 "closed": "e"
947 }
948 },
949 "ext_aaa": "xxx",
950 "ext_bbb": [null,42]
951 }"#;
952 let deserialized: Status = from_str(data).unwrap();
953 assert_eq!(deserialized.api, Some("0.13".into()));
954 let keys = deserialized.extensions.keys();
955 assert_eq!(keys.len(), 2)
956 }
957
958 mod serialize {
959 use super::*;
960
961 macro_rules! test_serialize {
963 ($test_name:ident, $value:expr, $expected:expr) => {
964 #[test]
965 fn $test_name() {
966 let serialized = to_string(&$value).unwrap();
967 assert_eq!(serialized, $expected);
968 }
969 };
970 }
971
972 test_serialize!(issue_report_channel_email, IssueReportChannel::Email, "\"email\"");
973 test_serialize!(
974 issue_report_channel_issue_mail,
975 IssueReportChannel::IssueMail,
976 "\"issue_mail\""
977 );
978 test_serialize!(
979 issue_report_channel_twitter,
980 IssueReportChannel::Twitter,
981 "\"twitter\""
982 );
983 test_serialize!(issue_report_channel_ml, IssueReportChannel::Ml, "\"ml\"");
984
985 test_serialize!(stream_default, Stream::default(), "{}");
986
987 test_serialize!(
988 stream_m4,
989 Stream {
990 m4: Some("http://example.org/stream.mpg".to_string()),
991 ..Stream::default()
992 },
993 r#"{"m4":"http://example.org/stream.mpg"}"#
994 );
995
996 test_serialize!(
997 stream_mjpeg,
998 Stream {
999 mjpeg: Some("http://example.org/stream.mjpeg".to_string()),
1000 ..Stream::default()
1001 },
1002 r#"{"mjpeg":"http://example.org/stream.mjpeg"}"#
1003 );
1004
1005 test_serialize!(
1006 stream_ustream,
1007 Stream {
1008 ustream: Some("http://www.ustream.tv/channel/hackspsps".to_string()),
1009 ..Stream::default()
1010 },
1011 r#"{"ustream":"http://www.ustream.tv/channel/hackspsps"}"#
1012 );
1013 }
1014
1015 mod deserialize {
1016 use super::*;
1017
1018 macro_rules! test_deserialize {
1020 ($test_name:ident, $value:expr, $type:ty, $expected:expr) => {
1021 #[test]
1022 fn $test_name() {
1023 let deserialized = from_str::<$type>(&$value).unwrap();
1024 assert_eq!(deserialized, $expected);
1025 }
1026 };
1027 }
1028
1029 test_deserialize!(
1030 issue_report_channel_email,
1031 "\"email\"",
1032 IssueReportChannel,
1033 IssueReportChannel::Email
1034 );
1035 test_deserialize!(
1036 issue_report_channel_issue_mail,
1037 "\"issue_mail\"",
1038 IssueReportChannel,
1039 IssueReportChannel::IssueMail
1040 );
1041 test_deserialize!(
1042 issue_report_channel_twitter,
1043 "\"twitter\"",
1044 IssueReportChannel,
1045 IssueReportChannel::Twitter
1046 );
1047 test_deserialize!(
1048 issue_report_channel_ml,
1049 "\"ml\"",
1050 IssueReportChannel,
1051 IssueReportChannel::Ml
1052 );
1053 }
1054}