1use 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#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, Validate)]
19#[serde(rename_all = "camelCase")]
20pub struct Program {
21 pub id: ProgramId,
25
26 #[serde(with = "crate::serde_rfc3339")]
30 pub created_date_time: DateTime<Utc>,
31
32 #[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 #[serde(deserialize_with = "crate::string_within_range_inclusive::<1, 128, _>")]
49 pub program_name: String,
50 pub program_long_name: Option<String>,
52 pub retailer_name: Option<String>,
54 pub retailer_long_name: Option<String>,
56 pub program_type: Option<String>,
58 pub country: Option<String>,
60 pub principal_subdivision: Option<String>,
62 pub time_zone_offset: Option<Duration>,
67 pub interval_period: Option<IntervalPeriod>,
68 #[validate(nested)]
70 pub program_descriptions: Option<Vec<ProgramDescription>>,
71 pub binding_events: Option<bool>,
73 pub local_price: Option<bool>,
75 pub payload_descriptors: Option<Vec<PayloadDescriptor>>,
77 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#[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 #[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}