Skip to main content

openleadr_wire/
resource.rs

1use chrono::{DateTime, Utc};
2use serde::{Deserialize, Serialize};
3use serde_with::{DefaultOnNull, serde_as, skip_serializing_none};
4use std::{fmt::Display, str::FromStr};
5use validator::Validate;
6
7use crate::{
8    ClientId, Identifier, IdentifierError, target::Target, values_map::ValuesMap, ven::VenId,
9};
10
11/// A resource is an energy device or system subject to control by a VEN.
12#[skip_serializing_none]
13#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, Validate)]
14#[serde(rename_all = "camelCase", tag = "objectType", rename = "RESOURCE")]
15pub struct Resource {
16    /// URL safe VTN assigned object ID.
17    pub id: ResourceId,
18    /// datetime in ISO 8601 format
19    #[serde(with = "crate::serde_rfc3339")]
20    pub created_date_time: DateTime<Utc>,
21    /// datetime in ISO 8601 format
22    #[serde(with = "crate::serde_rfc3339")]
23    pub modification_date_time: DateTime<Utc>,
24    /// The `clientID` is specified as a read-write field, but a duplicate of the `venID`.
25    /// Therefore, we include it as a read-only field here until it's completely removed.
26    // TODO remove as soon as https://github.com/oadr3-org/specification/pull/381 is resolved
27    #[serde(rename = "clientID")]
28    pub client_id: ClientId,
29    #[serde(flatten)]
30    #[validate(nested)]
31    pub content: BlResourceRequest,
32}
33
34#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
35#[serde(rename_all = "SCREAMING_SNAKE_CASE", tag = "objectType")]
36pub enum ResourceRequest {
37    BlResourceRequest(BlResourceRequest),
38    VenResourceRequest(VenResourceRequest),
39}
40
41impl Validate for ResourceRequest {
42    fn validate(&self) -> Result<(), validator::ValidationErrors> {
43        match self {
44            ResourceRequest::BlResourceRequest(x) => x.validate(),
45            ResourceRequest::VenResourceRequest(x) => x.validate(),
46        }
47    }
48}
49
50impl ResourceRequest {
51    pub fn resource_name(&self) -> &str {
52        match self {
53            ResourceRequest::BlResourceRequest(r) => &r.resource_name,
54            ResourceRequest::VenResourceRequest(r) => &r.resource_name,
55        }
56    }
57
58    pub fn attributes(&self) -> Option<&[ValuesMap]> {
59        match self {
60            ResourceRequest::BlResourceRequest(r) => r.attributes.as_deref(),
61            ResourceRequest::VenResourceRequest(r) => r.attributes.as_deref(),
62        }
63    }
64}
65
66#[skip_serializing_none]
67#[serde_as]
68#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, Validate)]
69#[serde(rename_all = "camelCase")]
70pub struct BlResourceRequest {
71    /// A list of targets.
72    #[serde(default)]
73    #[serde_as(deserialize_as = "DefaultOnNull")]
74    pub targets: Vec<Target>,
75    /// User generated identifier, resource may be configured with identifier out-of-band.
76    #[serde(deserialize_with = "crate::string_within_range_inclusive::<1, 128, _>")]
77    pub resource_name: String,
78    /// VTN provisioned on object creation based on the path, e.g., POST <>/ven/{venID}/resources.
79    #[serde(rename = "venID")]
80    pub ven_id: VenId,
81    /// A list of valuesMap objects describing attributes.
82    pub attributes: Option<Vec<ValuesMap>>,
83}
84
85#[skip_serializing_none]
86#[serde_as]
87#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, Validate)]
88#[serde(rename_all = "camelCase")]
89pub struct VenResourceRequest {
90    /// User generated identifier, resource may be configured with identifier out-of-band.
91    #[serde(deserialize_with = "crate::string_within_range_inclusive::<1, 128, _>")]
92    pub resource_name: String,
93    /// A list of valuesMap objects describing attributes.
94    pub attributes: Option<Vec<ValuesMap>>,
95}
96
97#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, Hash, Eq)]
98pub struct ResourceId(pub(crate) Identifier);
99
100impl Display for ResourceId {
101    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
102        write!(f, "{}", self.0)
103    }
104}
105
106impl ResourceId {
107    pub fn as_str(&self) -> &str {
108        self.0.as_str()
109    }
110
111    pub fn new(identifier: &str) -> Option<Self> {
112        Some(Self(identifier.parse().ok()?))
113    }
114}
115
116impl FromStr for ResourceId {
117    type Err = IdentifierError;
118
119    fn from_str(s: &str) -> Result<Self, Self::Err> {
120        Ok(Self(s.parse()?))
121    }
122}
123
124#[cfg(test)]
125mod test {
126    use super::{BlResourceRequest, Resource, ResourceId, ResourceRequest, VenResourceRequest};
127    use crate::ven::VenId;
128
129    #[test]
130    fn example_roundtrip() {
131        let example = r#"
132        {
133          "id": "test-resource",
134          "createdDateTime": "2023-06-15T09:30:00Z",
135          "modificationDateTime": "2023-06-15T09:30:00Z",
136          "resourceName": "RESOURCE_0999",
137          "objectType": "RESOURCE",
138          "clientID": "ven_client",
139          "venID": "0",
140          "targets": [
141            "resource_0999"
142          ]
143        }"#;
144
145        let parsed = serde_json::from_str::<Resource>(example).unwrap();
146
147        let expected = Resource {
148            id: ResourceId("test-resource".parse().unwrap()),
149            client_id: "ven_client".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: BlResourceRequest {
153                attributes: None,
154                targets: vec!["resource_0999".parse().unwrap()],
155                resource_name: "RESOURCE_0999".to_string(),
156                ven_id: VenId("0".parse().unwrap()),
157            },
158        };
159
160        assert_eq!(expected, parsed);
161
162        let serialized = serde_json::to_string_pretty(&expected).unwrap();
163
164        // Make sure the "objectType" discriminator is only printed once
165        let num_object_type = serialized
166            .as_bytes()
167            .windows("objectType".len())
168            .filter(|&w| w == "objectType".as_bytes())
169            .count();
170
171        assert_eq!(num_object_type, 1);
172
173        // Make sure the correct discriminator is printed
174        assert!(serialized.contains(r#""objectType": "RESOURCE""#));
175
176        let parsed = serde_json::from_str::<Resource>(&serialized).unwrap();
177        assert_eq!(expected, parsed);
178    }
179
180    #[test]
181    fn request_discriminator() {
182        let ven_request = r#"
183        {
184          "resourceName": "RESOURCE_0999",
185          "objectType": "VEN_RESOURCE_REQUEST",
186          "venID": "0"
187        }"#;
188
189        let parsed_ven_request = serde_json::from_str::<ResourceRequest>(ven_request).unwrap();
190
191        assert_eq!(
192            parsed_ven_request,
193            ResourceRequest::VenResourceRequest(VenResourceRequest {
194                resource_name: "RESOURCE_0999".to_string(),
195                attributes: None,
196            })
197        );
198
199        let bl_request = r#"
200        {
201          "resourceName": "RESOURCE_0999",
202          "objectType": "BL_RESOURCE_REQUEST",
203          "venID": "0",
204          "clientID": "ven_client"
205        }"#;
206
207        let parsed_bl_request = serde_json::from_str::<ResourceRequest>(bl_request).unwrap();
208
209        assert_eq!(
210            parsed_bl_request,
211            ResourceRequest::BlResourceRequest(BlResourceRequest {
212                targets: vec![],
213                resource_name: "RESOURCE_0999".to_string(),
214                ven_id: VenId("0".parse().unwrap()),
215                attributes: None,
216            })
217        );
218
219        let ven_request_with_bl_fields = r#"
220        {
221          "resourceName": "RESOURCE_0999",
222          "objectType": "VEN_RESOURCE_REQUEST",
223          "venID": "0",
224          "clientID": "ven_client"
225        }"#;
226
227        let parsed_ven_request_with_bl_fields =
228            serde_json::from_str::<ResourceRequest>(ven_request_with_bl_fields).unwrap();
229
230        assert_eq!(
231            parsed_ven_request_with_bl_fields,
232            ResourceRequest::VenResourceRequest(VenResourceRequest {
233                resource_name: "RESOURCE_0999".to_string(),
234                attributes: None,
235            })
236        );
237    }
238}