Skip to main content

openleadr_wire/
resource_group.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    Identifier, IdentifierError, resource::ResourceId, target::Target, values_map::ValuesMap,
9};
10
11/// A [`ResourceGroup`] is a grouping of nested resource groups or VEN resources, which is managed
12/// by the Business Logic. The [`ResourceGroup`] can provide a more accurate representation of the
13/// grid topology and how e.g. capacity bottlenecks affect downstream resources.
14#[skip_serializing_none]
15#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, Validate)]
16#[serde(
17    rename_all = "camelCase",
18    tag = "objectType",
19    rename = "RESOURCE_GROUP"
20)]
21pub struct ResourceGroup {
22    /// URL safe VTN assigned object ID.
23    pub id: ResourceGroupId,
24    /// datetime in ISO 8601 format
25    #[serde(with = "crate::serde_rfc3339")]
26    pub created_date_time: DateTime<Utc>,
27    /// datetime in ISO 8601 format
28    #[serde(with = "crate::serde_rfc3339")]
29    pub modification_date_time: DateTime<Utc>,
30
31    #[serde(flatten)]
32    #[validate(nested)]
33    pub content: BlResourceGroupRequest,
34}
35
36#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
37#[serde(tag = "type", content = "id", rename_all = "snake_case")]
38pub enum ResourceGroupChild {
39    ResourceGroup(ResourceGroupId),
40    VenResource(ResourceId),
41}
42
43impl ResourceGroupChild {
44    pub fn as_str(&self) -> &str {
45        match self {
46            ResourceGroupChild::VenResource(id) => id.as_str(),
47            ResourceGroupChild::ResourceGroup(id) => id.as_str(),
48        }
49    }
50}
51
52#[skip_serializing_none]
53#[serde_as]
54#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, Validate)]
55#[serde(rename_all = "camelCase")]
56pub struct BlResourceGroupRequest {
57    /// A list of targets.
58    #[serde(default)]
59    #[serde_as(deserialize_as = "DefaultOnNull")]
60    pub targets: Vec<Target>,
61    /// User generated identifier, resource may be configured with identifier out-of-band.
62    #[serde(deserialize_with = "crate::string_within_range_inclusive::<1, 128, _>")]
63    pub resource_group_name: String,
64    /// A list of valuesMap objects describing attributes.
65    pub attributes: Option<Vec<ValuesMap>>,
66
67    /// A list of [`ResourceGroupChild`] that contains child [`ResourceId`]s and/or child
68    /// [`ResourceGroupId`]s
69    #[serde(default)]
70    #[serde_as(deserialize_as = "DefaultOnNull")]
71    pub children: Vec<ResourceGroupChild>,
72}
73
74#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, Hash, Eq)]
75pub struct ResourceGroupId(pub(crate) Identifier);
76
77impl Display for ResourceGroupId {
78    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
79        write!(f, "{}", self.0)
80    }
81}
82
83impl ResourceGroupId {
84    pub fn as_str(&self) -> &str {
85        self.0.as_str()
86    }
87
88    pub fn new(identifier: &str) -> Option<Self> {
89        Some(Self(identifier.parse().ok()?))
90    }
91}
92
93impl FromStr for ResourceGroupId {
94    type Err = IdentifierError;
95
96    fn from_str(s: &str) -> Result<Self, Self::Err> {
97        Ok(Self(s.parse()?))
98    }
99}
100
101#[cfg(test)]
102mod test {
103    use crate::{resource::ResourceId, resource_group::ResourceGroupChild};
104
105    use super::{BlResourceGroupRequest, ResourceGroup, ResourceGroupId};
106
107    #[test]
108    fn example_roundtrip() {
109        let example = r#"
110        {
111          "id": "test-resource-group",
112          "createdDateTime": "2023-06-15T09:30:00Z",
113          "modificationDateTime": "2023-06-15T09:30:00Z",
114          "resourceGroupName": "RESOURCE_GROUP_0999",
115          "objectType": "RESOURCE_GROUP",
116          "targets": [
117            "resource_group_0999"
118          ],
119          "children": [
120                {
121                    "type":"resource_group",
122                    "id":"resource-group-1"
123                },
124                {
125                    "type":"ven_resource",
126                    "id":"ven-1"
127                }
128          ]
129        }"#;
130
131        let parsed = serde_json::from_str::<ResourceGroup>(example).unwrap();
132
133        let expected = ResourceGroup {
134            id: ResourceGroupId("test-resource-group".parse().unwrap()),
135            created_date_time: "2023-06-15T09:30:00Z".parse().unwrap(),
136            modification_date_time: "2023-06-15T09:30:00Z".parse().unwrap(),
137            content: BlResourceGroupRequest {
138                attributes: None,
139                targets: vec!["resource_group_0999".parse().unwrap()],
140                resource_group_name: "RESOURCE_GROUP_0999".to_string(),
141                children: vec![
142                    ResourceGroupChild::ResourceGroup(
143                        ResourceGroupId::new("resource-group-1").unwrap(),
144                    ),
145                    ResourceGroupChild::VenResource(ResourceId::new("ven-1").unwrap()),
146                ],
147            },
148        };
149
150        assert_eq!(expected, parsed);
151
152        let serialized = serde_json::to_string_pretty(&expected).unwrap();
153
154        // Make sure the "objectType" discriminator is only printed once
155        let num_object_type = serialized
156            .as_bytes()
157            .windows("objectType".len())
158            .filter(|&w| w == "objectType".as_bytes())
159            .count();
160
161        assert_eq!(num_object_type, 1);
162
163        // Make sure the correct discriminator is printed
164        assert!(serialized.contains(r#""objectType": "RESOURCE_GROUP""#));
165
166        let parsed = serde_json::from_str::<ResourceGroup>(&serialized).unwrap();
167        assert_eq!(expected, parsed);
168    }
169
170    #[test]
171    fn request_discriminator() {
172        let bl_request = r#"
173        {
174          "resourceGroupName": "RESOURCE_0999",
175          "objectType": "BL_RESOURCE_GROUP_REQUEST",
176          "targets": []
177        }"#;
178
179        let parsed_bl_request = serde_json::from_str::<BlResourceGroupRequest>(bl_request).unwrap();
180
181        assert_eq!(
182            parsed_bl_request,
183            BlResourceGroupRequest {
184                targets: vec![],
185                resource_group_name: "RESOURCE_0999".to_string(),
186                attributes: None,
187                children: vec![],
188            }
189        );
190    }
191}