Skip to main content

openleadr_wire/
lib.rs

1#![deny(rustdoc::broken_intra_doc_links)]
2#![deny(rustdoc::private_intra_doc_links)]
3
4//! Wire format definitions for OpenADR endpoints
5//!
6//! The types in this module model the messages sent over the wire in OpenADR 3.0.
7//! Most types are originally generated from the OpenAPI specification of OpenADR
8//! and manually modified to be more idiomatic.
9
10pub use event::Event;
11pub use program::Program;
12pub use report::Report;
13use serde::{Deserialize, Deserializer, Serialize, Serializer, de::Unexpected};
14use std::{fmt::Display, str::FromStr};
15pub use ven::Ven;
16
17pub mod event;
18pub mod interval;
19pub mod oauth;
20pub mod problem;
21pub mod program;
22pub mod report;
23pub mod resource;
24pub mod resource_group;
25pub mod subscription;
26pub mod target;
27pub mod values_map;
28pub mod ven;
29
30pub mod serde_rfc3339 {
31    use super::*;
32
33    use chrono::{DateTime, TimeZone, Utc};
34
35    pub fn serialize<S, Tz>(time: &DateTime<Tz>, serializer: S) -> Result<S::Ok, S::Error>
36    where
37        S: Serializer,
38        Tz: TimeZone,
39    {
40        serializer.serialize_str(&time.to_rfc3339())
41    }
42
43    pub fn deserialize<'de, D>(deserializer: D) -> Result<DateTime<Utc>, D::Error>
44    where
45        D: Deserializer<'de>,
46    {
47        let rfc_str = <String as Deserialize>::deserialize(deserializer)?;
48
49        match DateTime::parse_from_rfc3339(&rfc_str) {
50            Ok(datetime) => Ok(datetime.into()),
51            Err(_) => Err(serde::de::Error::invalid_value(
52                Unexpected::Str(&rfc_str),
53                &"Invalid RFC3339 string",
54            )),
55        }
56    }
57}
58
59pub fn string_within_range_inclusive<'de, const MIN: usize, const MAX: usize, D>(
60    deserializer: D,
61) -> Result<String, D::Error>
62where
63    D: Deserializer<'de>,
64{
65    let string = <String as Deserialize>::deserialize(deserializer)?;
66    let len = string.len();
67
68    if (MIN..=MAX).contains(&len) {
69        Ok(string.to_string())
70    } else {
71        Err(serde::de::Error::invalid_value(
72            Unexpected::Str(&string),
73            &IdentifierError::InvalidLength(len).to_string().as_str(),
74        ))
75    }
76}
77
78/// A string that matches `/^[a-zA-Z0-9_-]*$/` with length in 1..=128
79#[derive(Debug, Clone, Serialize, PartialEq, Eq, Hash, PartialOrd, Ord, sqlx::Type)]
80#[sqlx(transparent)]
81pub struct Identifier(#[serde(deserialize_with = "identifier")] String);
82
83impl<'de> Deserialize<'de> for Identifier {
84    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
85    where
86        D: Deserializer<'de>,
87    {
88        let s: String = Deserialize::deserialize(deserializer)?;
89
90        match Self::validate(&s) {
91            Ok(()) => Ok(Identifier(s)),
92            Err(e) => Err(serde::de::Error::invalid_value(
93                Unexpected::Str(&s),
94                &e.to_string().as_str(),
95            )),
96        }
97    }
98}
99
100#[derive(thiserror::Error, Debug)]
101pub enum IdentifierError {
102    #[error("string length {0} outside of allowed range 1..=128")]
103    InvalidLength(usize),
104    #[error("identifier contains characters besides [a-zA-Z0-9_-]: {0}")]
105    InvalidCharacter(String),
106    #[error("this identifier name is not allowed: {0}")]
107    ForbiddenName(String),
108}
109
110const FORBIDDEN_NAMES: &[&str] = &["null"];
111
112impl FromStr for Identifier {
113    type Err = IdentifierError;
114
115    fn from_str(s: &str) -> Result<Self, Self::Err> {
116        Self::validate(s).map(|()| Identifier(s.to_string()))
117    }
118}
119
120impl Identifier {
121    fn validate(s: &str) -> Result<(), IdentifierError> {
122        let is_valid_character = |b: u8| b.is_ascii_alphanumeric() || b == b'_' || b == b'-';
123
124        if !(1..=128).contains(&s.len()) {
125            Err(IdentifierError::InvalidLength(s.len()))
126        } else if !s.bytes().all(is_valid_character) {
127            Err(IdentifierError::InvalidCharacter(s.to_string()))
128        } else if FORBIDDEN_NAMES.contains(&s.to_ascii_lowercase().as_str()) {
129            Err(IdentifierError::ForbiddenName(s.to_string()))
130        } else {
131            Ok(())
132        }
133    }
134
135    pub fn as_str(&self) -> &str {
136        &self.0
137    }
138}
139
140impl Display for Identifier {
141    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
142        write!(f, "{}", self.0)
143    }
144}
145
146#[derive(Copy, Clone, Debug, PartialEq, Serialize, Deserialize)]
147#[serde(rename_all = "UPPERCASE")]
148pub enum ObjectType {
149    Program,
150    Event,
151    Report,
152    Subscription,
153    Ven,
154    Resource,
155    ResourceGroup,
156}
157
158impl ObjectType {
159    pub fn as_str(self) -> &'static str {
160        match self {
161            ObjectType::Program => "PROGRAM",
162            ObjectType::Event => "EVENT",
163            ObjectType::Report => "REPORT",
164            ObjectType::Subscription => "SUBSCRIPTION",
165            ObjectType::Ven => "VEN",
166            ObjectType::Resource => "RESOURCE",
167            ObjectType::ResourceGroup => "RESOURCE_GROUP",
168        }
169    }
170}
171
172/// An ISO 8601 formatted duration
173#[derive(Clone, Debug, PartialEq)]
174pub struct Duration(iso8601_duration::Duration);
175
176impl<'de> Deserialize<'de> for Duration {
177    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
178    where
179        D: Deserializer<'de>,
180    {
181        let raw = String::deserialize(deserializer)?;
182        let duration = raw
183            .parse::<iso8601_duration::Duration>()
184            .map_err(|_| "iso8601_duration::ParseDurationError")
185            .map_err(serde::de::Error::custom)?;
186
187        Ok(Self(duration))
188    }
189}
190
191impl Serialize for Duration {
192    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
193    where
194        S: Serializer,
195    {
196        self.to_string().serialize(serializer)
197    }
198}
199
200impl Duration {
201    /// Because iso8601 durations can include months and years, they don't independently have a
202    /// fixed duration. Their real duration (in real units like seconds) can only be determined
203    /// when a starting time is given.
204    ///
205    /// NOTE: does not consider leap seconds!
206    pub fn to_chrono_at_datetime<Tz: chrono::TimeZone>(
207        &self,
208        at: chrono::DateTime<Tz>,
209    ) -> chrono::Duration {
210        self.0.to_chrono_at_datetime(at)
211    }
212
213    /// One (1) hour
214    pub const PT1H: Self = Self(iso8601_duration::Duration {
215        year: 0.0,
216        month: 0.0,
217        day: 0.0,
218        hour: 1.0,
219        minute: 0.0,
220        second: 0.0,
221    });
222
223    /// Indicates that an event's intervals continue indefinitely into the future until the event is
224    /// deleted or modified. This effectively represents an infinite duration.
225    pub const P999Y: Self = Self(iso8601_duration::Duration {
226        year: 9999.0,
227        month: 0.0,
228        day: 0.0,
229        hour: 0.0,
230        minute: 0.0,
231        second: 0.0,
232    });
233
234    pub const PT0S: Self = Self(iso8601_duration::Duration {
235        year: 0.0,
236        month: 0.0,
237        day: 0.0,
238        hour: 0.0,
239        minute: 0.0,
240        second: 0.0,
241    });
242
243    pub const fn hours(hour: f32) -> Self {
244        Self(iso8601_duration::Duration {
245            year: 0.0,
246            month: 0.0,
247            day: 0.0,
248            hour,
249            minute: 0.0,
250            second: 0.0,
251        })
252    }
253}
254
255impl std::str::FromStr for Duration {
256    type Err = iso8601_duration::ParseDurationError;
257
258    fn from_str(s: &str) -> Result<Self, Self::Err> {
259        let duration = s.parse::<iso8601_duration::Duration>()?;
260        Ok(Self(duration))
261    }
262}
263
264impl Display for Duration {
265    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
266        let iso8601_duration::Duration {
267            year,
268            month,
269            day,
270            hour,
271            minute,
272            second,
273        } = self.0;
274
275        f.write_fmt(format_args!(
276            "P{year}Y{month}M{day}DT{hour}H{minute}M{second}S",
277        ))
278    }
279}
280
281#[derive(Serialize, Deserialize, PartialEq, Eq, Debug, Clone)]
282#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
283pub enum OperatingState {
284    Normal,
285    Error,
286    IdleNormal,
287    RunningNormal,
288    RunningCurtailed,
289    RunningHeightened,
290    IdleCurtailed,
291    #[serde(rename = "SGD_ERROR_CONDITION")]
292    SGDErrorCondition,
293    IdleHeightened,
294    IdleOptedOut,
295    RunningOptedOut,
296    #[serde(untagged)]
297    Private(String),
298}
299
300#[derive(Clone, Serialize, Deserialize, PartialEq, Eq, Debug)]
301#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
302pub enum DataQuality {
303    /// No known reasons to doubt the data.
304    Ok,
305    /// The data item is currently unavailable.
306    Missing,
307    /// The data item has been estimated from other available information.
308    Estimated,
309    /// The data item is suspected to be bad or is known to be.
310    Bad,
311    /// An application specific privately defined data quality setting.
312    #[serde(untagged)]
313    Private(String),
314}
315
316#[derive(Clone, Serialize, Deserialize, PartialEq, Eq, Debug)]
317#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
318pub enum Attribute {
319    /// Describes a single geographic point. Values contains 2 floats, generally
320    /// representing longitude and latitude. Demand Response programs may define
321    /// their own use of these fields.
322    Location,
323    /// Describes a geographic area. Application specific data. Demand Response
324    /// programs may define their own use of these fields, such as GeoJSON
325    /// polygon data.
326    Area,
327    /// The maximum consumption as a float, in kiloWatts.
328    MaxPowerConsumption,
329    /// The maximum power the device can export as a float, in kiloWatts.
330    MaxPowerExport,
331    /// A free-form short description of a VEN or resource.
332    Description,
333    /// An application specific privately defined attribute.
334    #[serde(untagged)]
335    Private(String),
336}
337
338#[derive(Clone, Serialize, Deserialize, PartialEq, Eq, Debug)]
339#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
340pub enum Unit {
341    /// Kilowatt-hours (kWh)
342    #[serde(rename = "KWH")]
343    KWH,
344    /// Greenhouse gas emissions (g/kWh)
345    #[serde(rename = "GHG")]
346    GHG,
347    /// Voltage (V)
348    Volts,
349    /// Current (A)
350    Amps,
351    /// Temperature (C)
352    Celcius,
353    /// Temperature (F)
354    Fahrenheit,
355    /// Percentage (%)
356    Percent,
357    /// Kilowatts
358    #[serde(rename = "KW")]
359    KW,
360    /// Kilovolt-ampere hours (kVAh)
361    #[serde(rename = "KVAH")]
362    KVAH,
363    /// Kilovolt-amperes reactive hours (kVARh)
364    #[serde(rename = "KVARH")]
365    KVARH,
366    /// Kilovolt-amperes (kVA)
367    #[serde(rename = "KVA")]
368    KVA,
369    /// Kilovolt-amperes reactive (kVAR)
370    #[serde(rename = "KVAR")]
371    KVAR,
372    /// An application specific privately defined unit.
373    #[serde(untagged)]
374    Private(String),
375}
376
377// example: 249rj49jiej
378#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, Hash, Eq, sqlx::Type)]
379#[sqlx(transparent)]
380pub struct ClientId(pub(crate) Identifier);
381
382impl Display for ClientId {
383    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
384        write!(f, "{}", self.0)
385    }
386}
387
388impl FromStr for ClientId {
389    type Err = IdentifierError;
390
391    fn from_str(s: &str) -> Result<Self, Self::Err> {
392        Ok(Self(s.parse()?))
393    }
394}
395
396impl ClientId {
397    pub fn as_str(&self) -> &str {
398        self.0.as_str()
399    }
400
401    pub fn new(identifier: &str) -> Option<Self> {
402        Some(Self(identifier.parse().ok()?))
403    }
404}
405
406#[cfg(test)]
407mod tests {
408    use crate::{Attribute, DataQuality, Identifier, OperatingState, Unit};
409    use chrono::{DateTime, Utc};
410
411    #[test]
412    fn test_operating_state_serialization() {
413        assert_eq!(
414            serde_json::to_string(&OperatingState::SGDErrorCondition).unwrap(),
415            r#""SGD_ERROR_CONDITION""#
416        );
417        assert_eq!(
418            serde_json::to_string(&OperatingState::Error).unwrap(),
419            r#""ERROR""#
420        );
421        assert_eq!(
422            serde_json::to_string(&OperatingState::Private(String::from("something else")))
423                .unwrap(),
424            r#""something else""#
425        );
426        assert_eq!(
427            serde_json::from_str::<OperatingState>(r#""NORMAL""#).unwrap(),
428            OperatingState::Normal
429        );
430        assert_eq!(
431            serde_json::from_str::<OperatingState>(r#""something else""#).unwrap(),
432            OperatingState::Private(String::from("something else"))
433        );
434    }
435
436    #[test]
437    fn test_data_quality_serialization() {
438        assert_eq!(serde_json::to_string(&DataQuality::Ok).unwrap(), r#""OK""#);
439        assert_eq!(
440            serde_json::to_string(&DataQuality::Private(String::from("something else"))).unwrap(),
441            r#""something else""#
442        );
443        assert_eq!(
444            serde_json::from_str::<DataQuality>(r#""MISSING""#).unwrap(),
445            DataQuality::Missing
446        );
447        assert_eq!(
448            serde_json::from_str::<DataQuality>(r#""something else""#).unwrap(),
449            DataQuality::Private(String::from("something else"))
450        );
451    }
452
453    #[test]
454    fn test_attribute_serialization() {
455        assert_eq!(
456            serde_json::to_string(&Attribute::Area).unwrap(),
457            r#""AREA""#
458        );
459        assert_eq!(
460            serde_json::to_string(&Attribute::Private(String::from("something else"))).unwrap(),
461            r#""something else""#
462        );
463        assert_eq!(
464            serde_json::from_str::<Attribute>(r#""MAX_POWER_EXPORT""#).unwrap(),
465            Attribute::MaxPowerExport
466        );
467        assert_eq!(
468            serde_json::from_str::<Attribute>(r#""something else""#).unwrap(),
469            Attribute::Private(String::from("something else"))
470        );
471    }
472
473    #[test]
474    fn test_unit_serialization() {
475        assert_eq!(serde_json::to_string(&Unit::KVARH).unwrap(), r#""KVARH""#);
476        assert_eq!(
477            serde_json::to_string(&Unit::Private(String::from("something else"))).unwrap(),
478            r#""something else""#
479        );
480        assert_eq!(
481            serde_json::from_str::<Unit>(r#""CELCIUS""#).unwrap(),
482            Unit::Celcius
483        );
484        assert_eq!(
485            serde_json::from_str::<Unit>(r#""something else""#).unwrap(),
486            Unit::Private(String::from("something else"))
487        );
488    }
489
490    impl quickcheck::Arbitrary for super::Duration {
491        fn arbitrary(g: &mut quickcheck::Gen) -> Self {
492            // the iso8601_duration library uses an f32 to store the values, which starts losing
493            // precision at 24-bit integers.
494            super::Duration(iso8601_duration::Duration {
495                year: (<u32 as quickcheck::Arbitrary>::arbitrary(g) & 0x00FF_FFFF) as f32,
496                month: (<u32 as quickcheck::Arbitrary>::arbitrary(g) & 0x00FF_FFFF) as f32,
497                day: (<u32 as quickcheck::Arbitrary>::arbitrary(g) & 0x00FF_FFFF) as f32,
498                hour: (<u32 as quickcheck::Arbitrary>::arbitrary(g) & 0x00FF_FFFF) as f32,
499                minute: (<u32 as quickcheck::Arbitrary>::arbitrary(g) & 0x00FF_FFFF) as f32,
500                second: (<u32 as quickcheck::Arbitrary>::arbitrary(g) & 0x00FF_FFFF) as f32,
501            })
502        }
503    }
504
505    #[test]
506    fn duration_to_string_from_str_roundtrip() {
507        quickcheck::quickcheck(test as fn(_) -> bool);
508
509        fn test(input: super::Duration) -> bool {
510            let roundtrip = input.to_string().parse::<super::Duration>().unwrap();
511
512            assert_eq!(input.0, roundtrip.0);
513
514            input.0 == roundtrip.0
515        }
516    }
517
518    #[test]
519    fn deserialize_identifier() {
520        assert_eq!(
521            serde_json::from_str::<Identifier>(r#""example-999""#).unwrap(),
522            Identifier("example-999".to_string())
523        );
524        assert!(
525            serde_json::from_str::<Identifier>(r#""þingvellir-999""#)
526                .unwrap_err()
527                .to_string()
528                .contains("identifier contains characters besides")
529        );
530
531        let long = "x".repeat(128);
532        assert_eq!(
533            serde_json::from_str::<Identifier>(&format!("\"{long}\"")).unwrap(),
534            Identifier(long)
535        );
536
537        let too_long = "x".repeat(129);
538        assert!(
539            serde_json::from_str::<Identifier>(&format!("\"{too_long}\""))
540                .unwrap_err()
541                .to_string()
542                .contains("string length 129 outside of allowed range 1..=128")
543        );
544
545        assert!(
546            serde_json::from_str::<Identifier>("\"\"")
547                .unwrap_err()
548                .to_string()
549                .contains("string length 0 outside of allowed range 1..=128")
550        );
551    }
552
553    #[test]
554    fn deserialize_string_within_range_inclusive() {
555        use serde::Deserialize;
556
557        #[derive(Debug, Deserialize, PartialEq, Eq)]
558        struct Test(
559            #[serde(deserialize_with = "super::string_within_range_inclusive::<1, 128, _>")] String,
560        );
561
562        let long = "x".repeat(128);
563        assert_eq!(
564            serde_json::from_str::<Test>(&format!("\"{long}\"")).unwrap(),
565            Test(long)
566        );
567
568        let too_long = "x".repeat(129);
569        assert!(
570            serde_json::from_str::<Test>(&format!("\"{too_long}\""))
571                .unwrap_err()
572                .to_string()
573                .contains("string length 129 outside of allowed range 1..=128")
574        );
575
576        assert!(
577            serde_json::from_str::<Test>("\"\"")
578                .unwrap_err()
579                .to_string()
580                .contains("string length 0 outside of allowed range 1..=128")
581        );
582    }
583
584    #[test]
585    fn deserialize_datetime() {
586        use serde::Deserialize;
587        // Thanks to https://tc39.es/proposal-uniform-interchange-date-parsing/cases.html
588
589        #[derive(Debug, Deserialize, PartialEq, Eq)]
590        struct Test(#[serde(with = "super::serde_rfc3339")] DateTime<Utc>);
591
592        let valid_dates = [
593            "1972-06-30T23:59:60Z",
594            "2019-03-26T14:00:00.9Z",
595            "2019-03-26T14:00:00.4999Z",
596            "1969-03-26T14:00:00.4999Z",
597        ];
598
599        for valid in valid_dates {
600            assert_eq!(
601                serde_json::from_str::<Test>(&format!("\"{valid}\"")).unwrap(),
602                Test(valid.parse().unwrap())
603            );
604        }
605
606        let invalid_dates = [
607            "2019-03-26T14:00:00,999Z",
608            "2019-03-26T10:00-04",
609            "2019-03-26T14:00.9Z",
610            "20190326T1400Z",
611            "2019-02-30",
612            "2019-03-25T24:01Z",
613            "2019-03-26T14:00+24:00",
614            "2019-03-26Z",
615            "2019-03-26+01:00",
616            "2019-03-26-04:00",
617            "2019-03-26T10:00-0400",
618            "+0002019-03-26T14:00Z",
619            "+2019-03-26T14:00Z",
620            "002019-03-26T14:00Z",
621            "019-03-26T14:00Z",
622            "2019-03-26T10:00Q",
623            "2019-03-26T10:00T",
624            "2019-03-26Q",
625            "2019-03-26T",
626            "2019-03-26 14:00Z",
627            "2019-03-26T14:00:00.",
628        ];
629
630        for invalid in invalid_dates {
631            assert!(
632                serde_json::from_str::<Test>(&format!("\"{invalid}\""))
633                    .unwrap_err()
634                    .to_string()
635                    .contains("Invalid RFC3339 string")
636            );
637        }
638    }
639}