Skip to main content

openleadr_wire/
program.rs

1//! Types used for the `program/` endpoint
2
3use crate::{
4    Identifier, IdentifierError, event::EventPayloadDescriptor, interval::IntervalPeriod,
5    report::ReportPayloadDescriptor, target::Target, values_map::ValuesMap,
6};
7use chrono::{DateTime, Utc};
8use serde::{Deserialize, Serialize};
9use serde_with::{DefaultOnNull, serde_as, skip_serializing_none};
10use std::{fmt::Display, str::FromStr};
11use validator::Validate;
12
13pub type Programs = Vec<Program>;
14
15/// Provides program specific metadata from VTN to VEN.
16#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, Validate)]
17#[serde(rename_all = "camelCase")]
18pub struct Program {
19    /// VTN provisioned on object creation.
20    ///
21    /// URL safe VTN assigned object ID.
22    pub id: ProgramId,
23
24    /// VTN provisioned on object creation.
25    ///
26    /// datetime in ISO 8601 format
27    #[serde(with = "crate::serde_rfc3339")]
28    pub created_date_time: DateTime<Utc>,
29
30    /// VTN provisioned on object modification.
31    ///
32    /// datetime in ISO 8601 format
33    #[serde(with = "crate::serde_rfc3339")]
34    pub modification_date_time: DateTime<Utc>,
35
36    #[serde(flatten)]
37    #[validate(nested)]
38    pub content: ProgramRequest,
39}
40
41#[skip_serializing_none]
42#[serde_as]
43#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, Validate)]
44#[serde(rename_all = "camelCase", tag = "objectType", rename = "PROGRAM")]
45pub struct ProgramRequest {
46    /// Short name to uniquely identify program.
47    #[serde(deserialize_with = "crate::string_within_range_inclusive::<1, 128, _>")]
48    pub program_name: String,
49    /// The temporal span of the program, which could be years-long.
50    pub interval_period: Option<IntervalPeriod>,
51    /// A list of programDescriptions
52    #[validate(nested)]
53    pub program_descriptions: Option<Vec<ProgramDescription>>,
54    /// A list of payloadDescriptors.
55    pub payload_descriptors: Option<Vec<PayloadDescriptor>>,
56    pub attributes: Option<Vec<ValuesMap>>,
57    /// A list of targets.
58    #[serde(default)]
59    #[serde_as(deserialize_as = "DefaultOnNull")]
60    pub targets: Vec<Target>,
61}
62
63impl ProgramRequest {
64    pub fn new(name: impl ToString) -> ProgramRequest {
65        ProgramRequest {
66            program_name: name.to_string(),
67            interval_period: Default::default(),
68            program_descriptions: Default::default(),
69            payload_descriptors: Default::default(),
70            attributes: Default::default(),
71            targets: Default::default(),
72        }
73    }
74}
75
76// example: object-999
77#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, Hash, Eq)]
78pub struct ProgramId(pub(crate) Identifier);
79
80impl Display for ProgramId {
81    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
82        write!(f, "{}", self.0)
83    }
84}
85
86impl ProgramId {
87    pub fn as_str(&self) -> &str {
88        self.0.as_str()
89    }
90
91    pub fn new(identifier: &str) -> Option<Self> {
92        Some(Self(identifier.parse().ok()?))
93    }
94}
95
96impl FromStr for ProgramId {
97    type Err = IdentifierError;
98
99    fn from_str(s: &str) -> Result<Self, Self::Err> {
100        Ok(Self(s.parse()?))
101    }
102}
103
104#[derive(Clone, Debug, PartialEq, Default, Serialize, Deserialize, Validate)]
105pub struct ProgramDescription {
106    /// A human or machine readable program description
107    #[serde(rename = "URL")]
108    #[validate(url)]
109    pub url: String,
110}
111
112#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
113#[serde(tag = "objectType", rename_all = "SCREAMING_SNAKE_CASE")]
114pub enum PayloadDescriptor {
115    EventPayloadDescriptor(EventPayloadDescriptor),
116    ReportPayloadDescriptor(ReportPayloadDescriptor),
117}
118
119#[cfg(test)]
120mod test {
121    use crate::Duration;
122
123    use super::*;
124
125    #[test]
126    fn example_parses() {
127        let example = r#"[
128                  {
129                    "id": "object-999",
130                    "createdDateTime": "2023-06-15T09:30:00Z",
131                    "modificationDateTime": "2023-06-15T09:30:00Z",
132                    "objectType": "PROGRAM",
133                    "programName": "ResTOU",
134                    "intervalPeriod": {
135                      "start": "2023-06-15T09:30:00Z",
136                      "duration": "PT1H",
137                      "randomizeStart": "PT1H"
138                    },
139                    "programDescriptions": null,
140                    "payloadDescriptors": null,
141                    "attributes": null,
142                    "targets": null
143                  }
144                ]"#;
145
146        let parsed = serde_json::from_str::<Programs>(example).unwrap();
147
148        let expected = vec![Program {
149            id: ProgramId("object-999".parse().unwrap()),
150            created_date_time: "2023-06-15T09:30:00Z".parse().unwrap(),
151            modification_date_time: "2023-06-15T09:30:00Z".parse().unwrap(),
152            content: ProgramRequest {
153                program_name: "ResTOU".into(),
154                interval_period: Some(IntervalPeriod {
155                    start: "2023-06-15T09:30:00Z".parse().unwrap(),
156                    duration: Some(Duration::PT1H),
157                    randomize_start: Some(Duration::PT1H),
158                }),
159                program_descriptions: None,
160                payload_descriptors: None,
161                attributes: None,
162                targets: vec![],
163            },
164        }];
165
166        assert_eq!(expected, parsed);
167    }
168
169    #[test]
170    fn parses_minimal() {
171        let example = r#"{"programName":"test"}"#;
172
173        assert_eq!(
174            serde_json::from_str::<ProgramRequest>(example).unwrap(),
175            ProgramRequest {
176                program_name: "test".to_string(),
177                interval_period: None,
178                program_descriptions: None,
179                payload_descriptors: None,
180                attributes: None,
181                targets: vec![],
182            }
183        );
184    }
185}