openleadr_wire/
program.rs

1//! Types used for the `program/` endpoint
2
3use crate::{
4    event::EventPayloadDescriptor, interval::IntervalPeriod, report::ReportPayloadDescriptor,
5    target::TargetMap, Duration, IdentifierError,
6};
7use chrono::{DateTime, Utc};
8use serde::{Deserialize, Serialize};
9use serde_with::skip_serializing_none;
10use std::{fmt::Display, str::FromStr};
11use validator::Validate;
12
13use super::Identifier;
14
15pub type Programs = Vec<Program>;
16
17/// Provides program specific metadata from VTN to VEN.
18#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, Validate)]
19#[serde(rename_all = "camelCase")]
20pub struct Program {
21    /// VTN provisioned on object creation.
22    ///
23    /// URL safe VTN assigned object ID.
24    pub id: ProgramId,
25
26    /// VTN provisioned on object creation.
27    ///
28    /// datetime in ISO 8601 format
29    #[serde(with = "crate::serde_rfc3339")]
30    pub created_date_time: DateTime<Utc>,
31
32    /// VTN provisioned on object modification.
33    ///
34    /// datetime in ISO 8601 format
35    #[serde(with = "crate::serde_rfc3339")]
36    pub modification_date_time: DateTime<Utc>,
37
38    #[serde(flatten)]
39    #[validate(nested)]
40    pub content: ProgramContent,
41}
42
43#[skip_serializing_none]
44#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, Validate)]
45#[serde(rename_all = "camelCase", tag = "objectType", rename = "PROGRAM")]
46pub struct ProgramContent {
47    /// Short name to uniquely identify program.
48    #[serde(deserialize_with = "crate::string_within_range_inclusive::<1, 128, _>")]
49    pub program_name: String,
50    /// Long name of program for human readability.
51    pub program_long_name: Option<String>,
52    /// Short name of energy retailer providing the program.
53    pub retailer_name: Option<String>,
54    /// Long name of energy retailer for human readability.
55    pub retailer_long_name: Option<String>,
56    /// A program defined categorization.
57    pub program_type: Option<String>,
58    /// Alpha-2 code per ISO 3166-1.
59    pub country: Option<String>,
60    /// Coding per ISO 3166-2. E.g. state in US.
61    pub principal_subdivision: Option<String>,
62    /// duration in ISO 8601 format
63    ///
64    /// Number of hours different from UTC for the standard time applicable to the program.
65    // TODO: aaaaaah why???
66    pub time_zone_offset: Option<Duration>,
67    pub interval_period: Option<IntervalPeriod>,
68    /// A list of programDescriptions
69    #[validate(nested)]
70    pub program_descriptions: Option<Vec<ProgramDescription>>,
71    /// True if events are fixed once transmitted.
72    pub binding_events: Option<bool>,
73    /// True if events have been adapted from a grid event.
74    pub local_price: Option<bool>,
75    /// A list of payloadDescriptors.
76    pub payload_descriptors: Option<Vec<PayloadDescriptor>>,
77    /// A list of valuesMap objects.
78    pub targets: Option<TargetMap>,
79}
80
81impl ProgramContent {
82    pub fn new(name: impl ToString) -> ProgramContent {
83        ProgramContent {
84            program_name: name.to_string(),
85            program_long_name: Default::default(),
86            retailer_name: Default::default(),
87            retailer_long_name: Default::default(),
88            program_type: Default::default(),
89            country: Default::default(),
90            principal_subdivision: Default::default(),
91            time_zone_offset: Default::default(),
92            interval_period: Default::default(),
93            program_descriptions: Default::default(),
94            binding_events: Default::default(),
95            local_price: Default::default(),
96            payload_descriptors: Default::default(),
97            targets: Default::default(),
98        }
99    }
100}
101
102// example: object-999
103#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, Hash, Eq)]
104pub struct ProgramId(pub(crate) Identifier);
105
106impl Display for ProgramId {
107    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
108        write!(f, "{}", self.0)
109    }
110}
111
112impl ProgramId {
113    pub fn as_str(&self) -> &str {
114        self.0.as_str()
115    }
116
117    pub fn new(identifier: &str) -> Option<Self> {
118        Some(Self(identifier.parse().ok()?))
119    }
120}
121
122impl FromStr for ProgramId {
123    type Err = IdentifierError;
124
125    fn from_str(s: &str) -> Result<Self, Self::Err> {
126        Ok(Self(s.parse()?))
127    }
128}
129
130#[derive(Clone, Debug, PartialEq, Default, Serialize, Deserialize, Validate)]
131pub struct ProgramDescription {
132    /// A human or machine readable program description
133    #[serde(rename = "URL")]
134    #[validate(url)]
135    pub url: String,
136}
137
138#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
139#[serde(tag = "objectType", rename_all = "SCREAMING_SNAKE_CASE")]
140pub enum PayloadDescriptor {
141    EventPayloadDescriptor(EventPayloadDescriptor),
142    ReportPayloadDescriptor(ReportPayloadDescriptor),
143}
144
145#[cfg(test)]
146mod test {
147    use super::*;
148
149    #[test]
150    fn example_parses() {
151        let example = r#"[
152                  {
153                    "id": "object-999",
154                    "createdDateTime": "2023-06-15T09:30:00Z",
155                    "modificationDateTime": "2023-06-15T09:30:00Z",
156                    "objectType": "PROGRAM",
157                    "programName": "ResTOU",
158                    "programLongName": "Residential Time of Use-A",
159                    "retailerName": "ACME",
160                    "retailerLongName": "ACME Electric Inc.",
161                    "programType": "PRICING_TARIFF",
162                    "country": "US",
163                    "principalSubdivision": "CO",
164                    "timeZoneOffset": "PT1H",
165                    "intervalPeriod": {
166                      "start": "2023-06-15T09:30:00Z",
167                      "duration": "PT1H",
168                      "randomizeStart": "PT1H"
169                    },
170                    "programDescriptions": null,
171                    "bindingEvents": false,
172                    "localPrice": false,
173                    "payloadDescriptors": null,
174                    "targets": null
175                  }
176                ]"#;
177
178        let parsed = serde_json::from_str::<Programs>(example).unwrap();
179
180        let expected = vec![Program {
181            id: ProgramId("object-999".parse().unwrap()),
182            created_date_time: "2023-06-15T09:30:00Z".parse().unwrap(),
183            modification_date_time: "2023-06-15T09:30:00Z".parse().unwrap(),
184            content: ProgramContent {
185                program_name: "ResTOU".into(),
186                program_long_name: Some("Residential Time of Use-A".into()),
187                retailer_name: Some("ACME".into()),
188                retailer_long_name: Some("ACME Electric Inc.".into()),
189                program_type: Some("PRICING_TARIFF".into()),
190                country: Some("US".into()),
191                principal_subdivision: Some("CO".into()),
192                time_zone_offset: Some(Duration::PT1H),
193                interval_period: Some(IntervalPeriod {
194                    start: "2023-06-15T09:30:00Z".parse().unwrap(),
195                    duration: Some(Duration::PT1H),
196                    randomize_start: Some(Duration::PT1H),
197                }),
198                program_descriptions: None,
199                binding_events: Some(false),
200                local_price: Some(false),
201                payload_descriptors: None,
202                targets: None,
203            },
204        }];
205
206        assert_eq!(expected, parsed);
207    }
208
209    #[test]
210    fn parses_minimal() {
211        let example = r#"{"programName":"test"}"#;
212
213        assert_eq!(
214            serde_json::from_str::<ProgramContent>(example).unwrap(),
215            ProgramContent {
216                program_name: "test".to_string(),
217                program_long_name: None,
218                retailer_name: None,
219                retailer_long_name: None,
220                program_type: None,
221                country: None,
222                principal_subdivision: None,
223                time_zone_offset: None,
224                interval_period: None,
225                program_descriptions: None,
226                binding_events: None,
227                local_price: None,
228                payload_descriptors: None,
229                targets: None,
230            }
231        );
232    }
233}