streamdeck_rs/
lib.rs

1#[cfg(feature = "logging")]
2pub mod logging;
3pub mod registration;
4pub mod socket;
5
6pub use crate::registration::RegistrationInfo;
7pub use crate::socket::StreamDeckSocket;
8
9use serde::{de, ser};
10use serde_derive::{Deserialize, Serialize};
11use serde_repr::{Deserialize_repr, Serialize_repr};
12use std::fmt;
13
14/// A message received from the Stream Deck software.
15///
16/// - `G` represents the global settings that are persisted within the Stream Deck software.
17/// - `S` represents the settings that are persisted within the Stream Deck software.
18/// - `M` represents the messages that are received from the property inspector.
19///
20/// [Official Documentation](https://developer.elgato.com/documentation/stream-deck/sdk/events-received/)
21#[derive(Debug, Deserialize, Serialize)]
22#[serde(tag = "event", rename_all = "camelCase")]
23pub enum Message<G, S, M> {
24    /// A key has been pressed.
25    ///
26    /// [Official Documentation](https://developer.elgato.com/documentation/stream-deck/sdk/events-received/#keydown)
27    #[serde(rename_all = "camelCase")]
28    KeyDown {
29        /// The uuid of the action.
30        action: String,
31        /// The instance of the action (key or part of a multiaction).
32        context: String,
33        /// The device where the key was pressed.
34        device: String,
35        /// Additional information about the key press.
36        payload: KeyPayload<S>,
37    },
38    /// A key has been released.
39    ///
40    /// [Official Documentation](https://developer.elgato.com/documentation/stream-deck/sdk/events-received/#keyup)
41    #[serde(rename_all = "camelCase")]
42    KeyUp {
43        /// The uuid of the action.
44        action: String,
45        /// The instance of the action (key or part of a multiaction).
46        context: String,
47        /// The device where the key was pressed.
48        device: String,
49        /// Additional information about the key press.
50        payload: KeyPayload<S>,
51    },
52    /// An instance of the action has been added to the display.
53    ///
54    /// [Official Documentation](https://developer.elgato.com/documentation/stream-deck/sdk/events-received/#willappear)
55    #[serde(rename_all = "camelCase")]
56    WillAppear {
57        /// The uuid of the action.
58        action: String,
59        /// The instance of the action (key or part of a multiaction).
60        context: String,
61        /// The device where the action will appear, or None if it does not appear on a device.
62        device: Option<String>,
63        /// Additional information about the action's appearance.
64        payload: VisibilityPayload<S>,
65    },
66    /// An instance of the action has been removed from the display.
67    ///
68    /// [Official Documentation](https://developer.elgato.com/documentation/stream-deck/sdk/events-received/#willdisappear)
69    #[serde(rename_all = "camelCase")]
70    WillDisappear {
71        /// The uuid of the action.
72        action: String,
73        /// The instance of the action (key or part of a multiaction).
74        context: String,
75        /// The device where the action was visible, or None if it was not on a device.
76        device: Option<String>,
77        /// Additional information about the action's appearance.
78        payload: VisibilityPayload<S>,
79    },
80    /// The title has changed for an instance of an action.
81    ///
82    /// [Official Documentation](https://developer.elgato.com/documentation/stream-deck/sdk/events-received/#titleparametersdidchange)
83    #[serde(rename_all = "camelCase")]
84    TitleParametersDidChange {
85        /// The uuid of the action.
86        action: String,
87        /// The instance of the action (key or part of a multiaction).
88        context: String,
89        /// The device where the action is visible, or None if it is not on a device.
90        device: Option<String>,
91        /// Additional information about the new title.
92        payload: TitleParametersPayload<S>,
93    },
94    /// A device has connected.
95    ///
96    /// [Official Documentation](https://developer.elgato.com/documentation/stream-deck/sdk/events-received/#devicedidconnect)
97    #[serde(rename_all = "camelCase")]
98    DeviceDidConnect {
99        /// The ID of the device that has connected.
100        device: String,
101        /// Information about the device.
102        device_info: DeviceInfo,
103    },
104    /// A device has disconnected.
105    ///
106    /// [Official Documentation](https://developer.elgato.com/documentation/stream-deck/sdk/events-received/#devicediddisconnect)
107    #[serde(rename_all = "camelCase")]
108    DeviceDidDisconnect {
109        /// The ID of the device that has disconnected.
110        device: String,
111    },
112    /// An application monitored by the manifest file has launched.
113    ///
114    /// [Official Documentation](https://developer.elgato.com/documentation/stream-deck/sdk/events-received/#applicationdidlaunch)
115    #[serde(rename_all = "camelCase")]
116    ApplicationDidLaunch {
117        /// Information about the launched application.
118        payload: ApplicationPayload,
119    },
120    /// An application monitored by the manifest file has terminated.
121    ///
122    /// [Official Documentation](https://developer.elgato.com/documentation/stream-deck/sdk/events-received/#applicationdidterminate)
123    #[serde(rename_all = "camelCase")]
124    ApplicationDidTerminate {
125        /// Information about the terminated application.
126        payload: ApplicationPayload,
127    },
128    /// The property inspector has sent data.
129    ///
130    /// [Official Documentation](https://developer.elgato.com/documentation/stream-deck/sdk/events-received/#sendtoplugin)
131    #[serde(rename_all = "camelCase")]
132    SendToPlugin {
133        /// The uuid of the action.
134        action: String,
135        /// The instance of the action (key or part of a multiaction).
136        context: String,
137        /// Information sent from the property inspector.
138        payload: M,
139    },
140    /// The application has sent settings for an action.
141    ///
142    /// This message is sent in response to GetSettings, but also after the
143    /// property inspector changes the settings.
144    ///
145    /// [Official Documentation](https://developer.elgato.com/documentation/stream-deck/sdk/events-received/#didreceivesettings)
146    #[serde(rename_all = "camelCase")]
147    DidReceiveSettings {
148        /// The uuid of the action.
149        action: String,
150        /// The instance of the action (key or part of a multiaction).
151        context: String,
152        /// The device where the action exists.
153        device: String,
154        /// The current settings for the action.
155        payload: KeyPayload<S>,
156    },
157    /// The property inspector for an action has become visible.
158    ///
159    /// [Official Documentation](https://developer.elgato.com/documentation/stream-deck/sdk/events-received/#propertyinspectordidappear)
160    #[serde(rename_all = "camelCase")]
161    PropertyInspectorDidAppear {
162        /// The uuid of the action.
163        action: String,
164        /// The instance of the action (key or part of a multiaction).
165        context: String,
166        /// The device where the action exists.
167        device: String,
168    },
169    /// The property inspector for an action is no longer visible.
170    ///
171    /// [Official Documentation](https://developer.elgato.com/documentation/stream-deck/sdk/events-received/#propertyinspectordiddisappear)
172    #[serde(rename_all = "camelCase")]
173    PropertyInspectorDidDisappear {
174        /// The uuid of the action.
175        action: String,
176        /// The instance of the action (key or part of a multiaction).
177        context: String,
178        /// The device where the action exists.
179        device: String,
180    },
181    /// The application has sent settings for an action.
182    ///
183    /// This message is sent in response to GetGlobalSettings, but also after
184    /// the property inspector changes the settings.
185    ///
186    /// [Official Documentation](https://developer.elgato.com/documentation/stream-deck/sdk/events-received/#didreceiveglobalsettings)
187    #[serde(rename_all = "camelCase")]
188    DidReceiveGlobalSettings {
189        /// The current settings for the action.
190        payload: GlobalSettingsPayload<G>,
191    },
192    /// The computer has resumed from sleep.
193    ///
194    /// Added in Stream Deck software version 4.3.
195    ///
196    /// [Official Documentation](https://developer.elgato.com/documentation/stream-deck/sdk/events-received/#systemdidwakeup)
197    SystemDidWakeUp,
198    /// An event from an unsupported version of the Stream Deck software.
199    ///
200    /// This occurs when the Stream Deck software sends an event that is not
201    /// understood. Usually this will be because the Stream Deck software is
202    /// newer than the plugin, and it should be safe to ignore these.
203    #[serde(other)]
204    Unknown,
205}
206
207/// A message to be sent to the Stream Deck software.
208///
209/// - `G` represents the global settings that are persisted within the Stream Deck software.
210/// - `S` represents the action settings that are persisted within the Stream Deck software.
211/// - `M` represents the messages that are sent to the property inspector.
212///
213/// [Official Documentation](https://developer.elgato.com/documentation/stream-deck/sdk/events-sent/)
214#[derive(Debug, Deserialize, Serialize)]
215#[serde(tag = "event", rename_all = "camelCase")]
216pub enum MessageOut<G, S, M> {
217    /// Set the title of an action instance.
218    ///
219    /// [Official Documentation](https://developer.elgato.com/documentation/stream-deck/sdk/events-sent/#settitle)
220    #[serde(rename_all = "camelCase")]
221    SetTitle {
222        /// The instance of the action (key or part of a multiaction).
223        context: String,
224        /// The title to set.
225        payload: TitlePayload,
226    },
227    /// Set the image of an action instance.
228    ///
229    /// [Official Documentation](https://developer.elgato.com/documentation/stream-deck/sdk/events-sent/#setimage)
230    #[serde(rename_all = "camelCase")]
231    SetImage {
232        /// The instance of the action (key or part of a multiaction).
233        context: String,
234        /// The image to set.
235        payload: ImagePayload,
236    },
237    /// Temporarily overlay the key image with an alert icon.
238    ///
239    /// [Official Documentation](https://developer.elgato.com/documentation/stream-deck/sdk/events-sent/#showalert)
240    #[serde(rename_all = "camelCase")]
241    ShowAlert {
242        /// The instance of the action (key or part of a multiaction).
243        context: String,
244    },
245    /// Temporarily overlay the key image with a checkmark.
246    ///
247    /// [Official Documentation](https://developer.elgato.com/documentation/stream-deck/sdk/events-sent/#showok)
248    #[serde(rename_all = "camelCase")]
249    ShowOk {
250        /// The instance of the action (key or part of a multiaction).
251        context: String,
252    },
253    /// Retrieve settings for an instance of an action via DidReceiveSettings.
254    ///
255    /// [Official Documentation](https://developer.elgato.com/documentation/stream-deck/sdk/events-sent/#getsettings)
256    #[serde(rename_all = "camelCase")]
257    GetSettings {
258        /// The instance of the action (key or part of a multiaction).
259        context: String,
260    },
261    /// Store settings for an instance of an action.
262    ///
263    /// [Official Documentation](https://developer.elgato.com/documentation/stream-deck/sdk/events-sent/#setsettings)
264    #[serde(rename_all = "camelCase")]
265    SetSettings {
266        /// The instance of the action (key or part of a multiaction).
267        context: String,
268        /// The settings to save.
269        payload: S,
270    },
271    /// Set the state of an action.
272    ///
273    /// Normally, Stream Deck changes the state of an action automatically when the key is pressed.
274    ///
275    /// [Official Documentation](https://developer.elgato.com/documentation/stream-deck/sdk/events-sent/#setstate)
276    #[serde(rename_all = "camelCase")]
277    SetState {
278        /// The instance of the action (key or part of a multiaction).
279        context: String,
280        /// The desired state.
281        payload: StatePayload,
282    },
283    /// Send data to the property inspector.
284    ///
285    /// [Official Documentation](https://developer.elgato.com/documentation/stream-deck/sdk/events-sent/#sendtopropertyinspector)
286    #[serde(rename_all = "camelCase")]
287    SendToPropertyInspector {
288        /// The uuid of the action.
289        action: String,
290        /// The instance of the action (key or part of a multiaction).
291        context: String,
292        /// The message to send.
293        payload: M,
294    },
295    /// Select a new profile.
296    ///
297    /// [Official Documentation](https://developer.elgato.com/documentation/stream-deck/sdk/events-sent/#switchtoprofile)
298    #[serde(rename_all = "camelCase")]
299    SwitchToProfile {
300        /// The instance of the action (key or part of a multiaction).
301        context: String,
302        /// The device to change the profile of.
303        device: String,
304        /// The profile to activate.
305        payload: ProfilePayload,
306    },
307    /// Open a URL in the default browser.
308    ///
309    /// [Official Documentation](https://developer.elgato.com/documentation/stream-deck/sdk/events-sent/#openurl)
310    #[serde(rename_all = "camelCase")]
311    OpenUrl {
312        /// The url to open.
313        payload: UrlPayload,
314    },
315    /// Retrieve plugin settings for via DidReceiveGlobalSettings.
316    ///
317    /// [Official Documentation](https://developer.elgato.com/documentation/stream-deck/sdk/events-sent/#getglobalsettings)
318    #[serde(rename_all = "camelCase")]
319    GetGlobalSettings {
320        /// The instance of the action (key or part of a multiaction).
321        context: String,
322    },
323    /// Store plugin settings.
324    ///
325    /// [Official Documentation](https://developer.elgato.com/documentation/stream-deck/sdk/events-sent/#setglobalsettings)
326    #[serde(rename_all = "camelCase")]
327    SetGlobalSettings {
328        /// The instance of the action (key or part of a multiaction).
329        context: String,
330        /// The settings to save.
331        payload: G,
332    },
333    /// Write to the log.
334    ///
335    /// [Official Documentation](https://developer.elgato.com/documentation/stream-deck/sdk/events-sent/#logmessage)
336    #[serde(rename_all = "camelCase")]
337    LogMessage {
338        /// The message to log.
339        payload: LogMessagePayload,
340    },
341}
342
343/// The target of a command.
344#[derive(Debug, Deserialize_repr, Serialize_repr)]
345#[repr(u8)]
346pub enum Target {
347    /// Both the device and a the display within the Stream Deck software.
348    Both = 0,
349    /// Only the device.
350    Hardware = 1,
351    /// Only the display within the Stream Deck software.
352    Software = 2,
353}
354
355/// The title to set as part of a [SetTitle](enum.MessageOut.html#variant.SetTitle) message.
356///
357/// [Official Documentation](https://developer.elgato.com/documentation/stream-deck/sdk/events-sent/#settitle)
358#[derive(Debug, Deserialize, Serialize)]
359#[serde(rename_all = "camelCase")]
360pub struct TitlePayload {
361    /// The new title.
362    pub title: Option<String>,
363    /// The target displays.
364    pub target: Target,
365    /// The state to set the title for. If not set, it is set for all states.
366    #[serde(skip_serializing_if = "Option::is_none")]
367    pub state: Option<u8>,
368}
369
370/// The image to set as part of a [SetImage](enum.MessageOut.html#variant.SetImage) message.
371///
372/// [Official Documentation](https://developer.elgato.com/documentation/stream-deck/sdk/events-sent/#setimage)
373#[derive(Debug, Deserialize, Serialize)]
374#[serde(rename_all = "camelCase")]
375pub struct ImagePayload {
376    /// An image in the form of a data URI.
377    pub image: Option<String>,
378    /// The target displays.
379    pub target: Target,
380    /// The state to set the image for. If not set, it is set for all states.
381    #[serde(skip_serializing_if = "Option::is_none")]
382    pub state: Option<u8>,
383}
384
385/// The state to set as part of a [SetState](enum.MessageOut.html#variant.SetState) message.
386///
387/// [Official Documentation](https://developer.elgato.com/documentation/stream-deck/sdk/events-sent/#setstate)
388#[derive(Debug, Deserialize, Serialize)]
389#[serde(rename_all = "camelCase")]
390pub struct StatePayload {
391    /// The new state.
392    pub state: u8,
393}
394
395/// The profile to activate as part of a [SwitchToProfile](enum.MessageOut.html#variant.SwitchToProfile) message.
396///
397/// [Official Documentation](https://developer.elgato.com/documentation/stream-deck/sdk/events-sent/#SwitchToProfile)
398#[derive(Debug, Deserialize, Serialize)]
399#[serde(rename_all = "camelCase")]
400pub struct ProfilePayload {
401    /// The name of the profile to activate.
402    pub profile: String,
403}
404
405/// The URL to launch as part of a [OpenUrl](enum.MessageOut.html#variant.OpenUrl) message.
406///
407/// [Official Documentation](https://developer.elgato.com/documentation/stream-deck/sdk/events-sent/#openurl)
408#[derive(Debug, Deserialize, Serialize)]
409#[serde(rename_all = "camelCase")]
410pub struct UrlPayload {
411    /// The URL to launch.
412    pub url: String,
413}
414
415/// Additional information about the key pressed.
416#[derive(Debug, Deserialize, Serialize)]
417#[serde(rename_all = "camelCase")]
418pub struct KeyPayload<S> {
419    /// The stored settings for the action instance.
420    pub settings: S,
421    /// The location of the key that was pressed, or None if this action instance is part of a multi action.
422    pub coordinates: Option<Coordinates>,
423    /// The current state of the action instance.
424    pub state: Option<u8>,
425    /// The desired state of the action instance (if this instance is part of a multi action).
426    pub user_desired_state: Option<u8>,
427    //TODO: is_in_multi_action ignored. replace coordinates with enum Location { Coordinates, MultiAction }.
428}
429
430/// Additional information about a key's appearance.
431#[derive(Debug, Deserialize, Serialize)]
432#[serde(rename_all = "camelCase")]
433pub struct VisibilityPayload<S> {
434    /// The stored settings for the action instance.
435    pub settings: S,
436    /// The location of the key, or None if this action instance is part of a multi action.
437    pub coordinates: Option<Coordinates>,
438    /// The state of the action instance.
439    pub state: Option<u8>,
440    //TODO: is_in_multi_action ignored. replace coordinates with enum Location { Coordinates, MultiAction }.
441}
442
443/// The new title of a key.
444#[derive(Debug, Deserialize, Serialize)]
445#[serde(rename_all = "camelCase")]
446pub struct TitleParametersPayload<S> {
447    /// The stored settings for the action instance.
448    pub settings: S,
449    /// The location of the key, or None if this action instance is part of a multi action.
450    pub coordinates: Coordinates,
451    /// The state of the action instance.
452    pub state: Option<u8>,
453    /// The new title.
454    pub title: String,
455    /// Additional parameters for the display of the title.
456    pub title_parameters: TitleParameters,
457}
458
459/// The new global settings.
460#[derive(Debug, Deserialize, Serialize)]
461#[serde(rename_all = "camelCase")]
462pub struct GlobalSettingsPayload<G> {
463    /// The stored settings for the plugin.
464    pub settings: G,
465}
466
467/// A log message.
468#[derive(Debug, Deserialize, Serialize)]
469#[serde(rename_all = "camelCase")]
470pub struct LogMessagePayload {
471    /// The log message text.
472    pub message: String,
473}
474
475/// Information about a hardware device.
476///
477/// [Official Documentation](https://developer.elgato.com/documentation/stream-deck/sdk/events-received/#devicedidconnect)
478#[derive(Debug, Deserialize, Serialize)]
479#[serde(rename_all = "camelCase")]
480pub struct DeviceInfo {
481    /// The user-provided name of the device.
482    ///
483    /// Added in Stream Deck software version 4.3.
484    pub name: Option<String>,
485    /// The size of the device.
486    pub size: DeviceSize,
487    /// The type of the device, or None if the Stream Deck software is running with no device attached.
488    #[serde(rename = "type")]
489    pub _type: Option<DeviceType>,
490}
491
492/// Information about a monitored application that has launched or terminated.
493#[derive(Debug, Deserialize, Serialize)]
494#[serde(rename_all = "camelCase")]
495pub struct ApplicationPayload {
496    /// The name of the application.
497    pub application: String,
498}
499
500/// The location of a key on a device.
501///
502/// Locations are specified using zero-indexed values starting from the top left corner of the device.
503#[derive(Debug, Deserialize, Serialize)]
504#[serde(rename_all = "camelCase")]
505pub struct Coordinates {
506    /// The x coordinate of the key.
507    pub column: u8,
508    /// The y-coordinate of the key.
509    pub row: u8,
510}
511
512/// The vertical alignment of a title.
513///
514/// Titles are always centered horizontally.
515#[derive(Debug, Deserialize, Serialize)]
516#[serde(rename_all = "camelCase")]
517pub enum Alignment {
518    /// The title should appear at the top of the key.
519    Top,
520    /// The title should appear in the middle of the key.
521    Middle,
522    /// The title should appear at the bottom of the key.
523    Bottom,
524}
525
526/// Style information for a title.
527///
528/// [Official Documentation](https://developer.elgato.com/documentation/stream-deck/sdk/events-received/#titleparametersdidchange)
529#[derive(Debug, Deserialize, Serialize)]
530#[serde(rename_all = "camelCase")]
531pub struct TitleParameters {
532    /// The name of the font family.
533    pub font_family: String,
534    /// The font size.
535    pub font_size: u8,
536    /// Whether the font is bold and/or italic.
537    pub font_style: String,
538    /// Whether the font is underlined.
539    pub font_underline: bool,
540    /// Whether the title is displayed.
541    pub show_title: bool,
542    /// The vertical alignment of the title.
543    pub title_alignment: Alignment,
544    /// The color of the title.
545    pub title_color: String,
546}
547
548/// The size of a device in keys.
549#[derive(Debug, Deserialize, Serialize)]
550#[serde(rename_all = "camelCase")]
551pub struct DeviceSize {
552    /// The number of key columns on the device.
553    pub columns: u8,
554    /// The number of key rows on the device.
555    pub rows: u8,
556}
557
558/// The type of connected hardware device.
559///
560/// [Official Documentation](https://developer.elgato.com/documentation/stream-deck/sdk/manifest/#profiles)
561#[derive(Debug)]
562pub enum DeviceType {
563    /// The [Stream Deck](https://www.elgato.com/en/gaming/stream-deck).
564    StreamDeck, // 0
565    /// The [Stream Deck Mini](https://www.elgato.com/en/gaming/stream-deck-mini).
566    StreamDeckMini, // 1
567    /// The [Stream Deck XL](https://www.elgato.com/en/gaming/stream-deck-xl).
568    ///
569    /// Added in Stream Deck software version 4.3.
570    StreamDeckXl, // 2
571    /// The [Stream Deck Mobile](https://www.elgato.com/en/gaming/stream-deck-mobile) app.
572    ///
573    /// Added in Stream Deck software version 4.3.
574    StreamDeckMobile, // 3
575    /// The G-keys in Corsair keyboards
576    ///
577    /// Added in Stream Deck software version 4.7
578    CorsairGKeys, // 4
579    /// The [Stream Deck Pedal](https://www.elgato.com/en/stream-deck-pedal).
580    ///
581    /// Added in Stream Deck software version 5.2
582    StreamDeckPedal, // 5
583    /// The [Corsair Voyager Streaming Laptop](https://www.corsair.com/us/en/voyager-a1600-gaming-streaming-pc-laptop).
584    ///
585    /// Added in Stream Deck software version 5.3
586    CorsairVoyager, // 6
587    /// The [Stream Deck +](https://www.elgato.com/en/stream-deck-plus)
588    ///
589    /// Added in Stream Deck software version 6.0
590    StreamDeckPlus, // 7
591    /// A device not documented in the 6.0 SDK.
592    Unknown(u64),
593}
594
595impl ser::Serialize for DeviceType {
596    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
597    where
598        S: ser::Serializer,
599    {
600        serializer.serialize_u64(match self {
601            DeviceType::StreamDeck => 0,
602            DeviceType::StreamDeckMini => 1,
603            DeviceType::StreamDeckXl => 2,
604            DeviceType::StreamDeckMobile => 3,
605            DeviceType::CorsairGKeys => 4,
606            DeviceType::StreamDeckPedal => 5,
607            DeviceType::CorsairVoyager => 6,
608            DeviceType::StreamDeckPlus => 7,
609            DeviceType::Unknown(value) => *value,
610        })
611    }
612}
613
614impl<'de> de::Deserialize<'de> for DeviceType {
615    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
616    where
617        D: de::Deserializer<'de>,
618    {
619        struct Visitor;
620
621        impl<'de> de::Visitor<'de> for Visitor {
622            type Value = DeviceType;
623
624            fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
625                formatter.write_str("an integer")
626            }
627
628            fn visit_u64<E>(self, value: u64) -> Result<DeviceType, E>
629            where
630                E: de::Error,
631            {
632                Ok(match value {
633                    0 => DeviceType::StreamDeck,
634                    1 => DeviceType::StreamDeckMini,
635                    2 => DeviceType::StreamDeckXl,
636                    3 => DeviceType::StreamDeckMobile,
637                    4 => DeviceType::CorsairGKeys,
638                    5 => DeviceType::StreamDeckPedal,
639                    6 => DeviceType::CorsairVoyager,
640                    7 => DeviceType::StreamDeckPlus,
641                    value => DeviceType::Unknown(value),
642                })
643            }
644        }
645
646        deserializer.deserialize_u64(Visitor)
647    }
648}
649
650#[derive(Clone, Debug, Eq, PartialEq)]
651pub struct Color {
652    r: u8,
653    g: u8,
654    b: u8,
655}
656
657impl ser::Serialize for Color {
658    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
659    where
660        S: ser::Serializer,
661    {
662        let html_color = format!("#{:02x}{:02x}{:02x}", self.r, self.g, self.b);
663        serializer.serialize_str(&html_color)
664    }
665}
666
667impl<'de> de::Deserialize<'de> for Color {
668    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
669    where
670        D: de::Deserializer<'de>,
671    {
672        struct Visitor;
673
674        impl<'de> de::Visitor<'de> for Visitor {
675            type Value = Color;
676
677            fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
678                formatter.write_str("a hex color")
679            }
680
681            fn visit_str<E>(self, value: &str) -> Result<Color, E>
682            where
683                E: de::Error,
684            {
685                if value.len() != 7 {
686                    return Err(E::invalid_length(value.len(), &self));
687                }
688
689                if &value[0..1] != "#" {
690                    return Err(E::custom("expected string to begin with '#'"));
691                }
692
693                let r = u8::from_str_radix(&value[1..3], 16)
694                    .map_err(|_| E::invalid_value(de::Unexpected::Str(value), &self))?;
695                let g = u8::from_str_radix(&value[3..5], 16)
696                    .map_err(|_| E::invalid_value(de::Unexpected::Str(value), &self))?;
697                let b = u8::from_str_radix(&value[5..7], 16)
698                    .map_err(|_| E::invalid_value(de::Unexpected::Str(value), &self))?;
699
700                Ok(Color { r, g, b })
701            }
702        }
703
704        deserializer.deserialize_str(Visitor)
705    }
706}
707
708#[cfg(test)]
709mod test {
710    use super::Color;
711
712    #[test]
713    fn color() {
714        let color_a = Color {
715            r: 0x12,
716            g: 0x34,
717            b: 0x56,
718        };
719        let color_b = Color {
720            r: 0x12,
721            g: 0x12,
722            b: 0x12,
723        };
724
725        let as_json = r##"["#123456","#121212"]"##;
726        let colors: Vec<Color> = serde_json::from_str(as_json).expect("array of colors");
727
728        assert_eq!(2, colors.len());
729        assert_eq!(color_a, colors[0]);
730        assert_eq!(color_b, colors[1]);
731
732        let json_str: String = serde_json::to_string(&vec![color_a, color_b]).expect("JSON array");
733        assert_eq!(as_json, json_str);
734    }
735}