use crate::{
ClientId, Identifier, IdentifierError, Unit,
event::EventId,
interval::{Interval, IntervalPeriod},
target::Target,
};
use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use serde_with::skip_serializing_none;
use std::{
fmt::{Display, Formatter},
str::FromStr,
};
use validator::{Validate, ValidateRange};
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, Validate)]
#[serde(rename_all = "camelCase")]
pub struct Report {
pub id: ReportId,
#[serde(with = "crate::serde_rfc3339")]
pub created_date_time: DateTime<Utc>,
#[serde(with = "crate::serde_rfc3339")]
pub modification_date_time: DateTime<Utc>,
#[serde(flatten)]
#[validate(nested)]
pub content: ReportRequest,
#[serde(rename = "clientID")]
pub client_id: ClientId,
}
#[skip_serializing_none]
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, Validate)]
#[serde(rename_all = "camelCase", tag = "objectType", rename = "REPORT")]
pub struct ReportRequest {
#[serde(rename = "eventID")]
pub event_id: EventId,
#[serde(deserialize_with = "crate::string_within_range_inclusive::<1, 128, _>")]
pub client_name: String,
pub report_name: Option<String>,
#[validate(nested)]
pub payload_descriptors: Option<Vec<ReportPayloadDescriptor>>,
pub resources: Vec<ReportResource>,
}
impl ReportRequest {
pub fn with_client_name(mut self, client_name: &str) -> Self {
self.client_name = client_name.to_string();
self
}
pub fn with_name(mut self, name: &str) -> Self {
self.report_name = Some(name.to_string());
self
}
pub fn with_payload_descriptors(mut self, descriptors: Vec<ReportPayloadDescriptor>) -> Self {
self.payload_descriptors = Some(descriptors);
self
}
pub fn with_resources(mut self, resources: Vec<ReportResource>) -> Self {
self.resources = resources;
self
}
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, Hash, Eq)]
pub struct ReportId(pub(crate) Identifier);
impl ReportId {
pub fn as_str(&self) -> &str {
self.0.as_str()
}
}
impl Display for ReportId {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.0)
}
}
impl FromStr for ReportId {
type Err = IdentifierError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
Ok(Self(s.parse()?))
}
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ReportResource {
pub resource_name: ResourceName,
#[serde(skip_serializing_if = "Option::is_none")]
pub interval_period: Option<IntervalPeriod>,
pub intervals: Vec<Interval>,
}
#[skip_serializing_none]
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ReportDescriptor {
pub payload_type: ReportType,
pub reading_type: Option<ReadingType>,
pub units: Option<Unit>,
pub targets: Option<Vec<Target>>,
#[serde(default = "bool_false")]
pub aggregate: bool,
#[serde(default = "neg_one")]
pub start_interval: i32,
#[serde(default = "neg_one")]
pub num_intervals: i32,
#[serde(default = "bool_true")]
pub historical: bool,
#[serde(default = "neg_one")]
pub frequency: i32,
#[serde(default = "pos_one")]
pub repeat: i32,
#[serde(default)]
pub report_intervals: ReportIntervals,
}
impl ReportDescriptor {
pub fn new(payload_type: ReportType) -> Self {
Self {
payload_type,
reading_type: None,
units: None,
targets: None,
aggregate: false,
start_interval: -1,
num_intervals: -1,
historical: true,
frequency: -1,
repeat: 1,
report_intervals: Default::default(),
}
}
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, Default)]
#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
pub enum ReportIntervals {
#[default]
Intervals,
SubIntervals,
OpenIntervals,
}
fn bool_false() -> bool {
false
}
fn bool_true() -> bool {
true
}
fn neg_one() -> i32 {
-1
}
fn pos_one() -> i32 {
1
}
#[skip_serializing_none]
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, Validate)]
#[serde(rename_all = "camelCase")]
pub struct ReportPayloadDescriptor {
pub payload_type: ReportType,
#[serde(skip_serializing_if = "ReadingType::is_default", default)]
pub reading_type: ReadingType,
pub units: Option<Unit>,
pub accuracy: Option<f32>,
#[validate(range(min = Confidence(0), max = Confidence(100)))]
pub confidence: Option<Confidence>,
}
impl ReportPayloadDescriptor {
pub fn new(payload_type: ReportType) -> Self {
Self {
payload_type,
reading_type: Default::default(),
units: None,
accuracy: None,
confidence: None,
}
}
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, PartialOrd)]
pub struct Confidence(u8);
impl ValidateRange<Confidence> for Confidence {
fn greater_than(&self, _: Confidence) -> Option<bool> {
None
}
fn less_than(&self, _: Confidence) -> Option<bool> {
None
}
}
#[cfg(test)]
mod tests {
use crate::{
Duration,
values_map::{Value, ValueType, ValuesMap},
};
use super::*;
#[test]
fn test_report_type_serialization() {
assert_eq!(
serde_json::to_string(&ReportType::Baseline).unwrap(),
r#""BASELINE""#
);
assert_eq!(
serde_json::to_string(&ReportType::RegulationSetpoint).unwrap(),
r#""REGULATION_SETPOINT""#
);
assert_eq!(
serde_json::to_string(&ReportType::Private(String::from("something else"))).unwrap(),
r#""something else""#
);
assert_eq!(
serde_json::from_str::<ReportType>(r#""DEMAND""#).unwrap(),
ReportType::Demand
);
assert_eq!(
serde_json::from_str::<ReportType>(r#""EXPORT_RESERVATION_FEE""#).unwrap(),
ReportType::ExportReservationFee
);
assert_eq!(
serde_json::from_str::<ReportType>(r#""something else""#).unwrap(),
ReportType::Private(String::from("something else"))
);
assert!(serde_json::from_str::<ReportType>(r#""""#).is_err());
assert!(serde_json::from_str::<ReportType>(&format!("\"{}\"", "x".repeat(129))).is_err());
}
#[test]
fn test_reading_type_serialization() {
assert_eq!(
serde_json::to_string(&ReadingType::DirectRead).unwrap(),
r#""DIRECT_READ""#
);
assert_eq!(
serde_json::to_string(&ReadingType::Private(String::from("something else"))).unwrap(),
r#""something else""#
);
assert_eq!(
serde_json::from_str::<ReadingType>(r#""AVERAGE""#).unwrap(),
ReadingType::Average
);
assert_eq!(
serde_json::from_str::<ReadingType>(r#""something else""#).unwrap(),
ReadingType::Private(String::from("something else"))
);
}
#[test]
fn descriptor_parses_minimal() {
let json = r#"{"payloadType":"hello"}"#;
let expected = ReportDescriptor::new(ReportType::Private("hello".into()));
assert_eq!(
serde_json::from_str::<ReportDescriptor>(json).unwrap(),
expected
);
}
#[test]
fn parses_minimal_report() {
let example = r#"{"eventID":"e1","clientName":"c","resources":[]}"#;
let expected = ReportRequest {
event_id: EventId("e1".parse().unwrap()),
client_name: "c".to_string(),
report_name: None,
payload_descriptors: None,
resources: vec![],
};
assert_eq!(
serde_json::from_str::<ReportRequest>(example).unwrap(),
expected
);
}
#[test]
fn test_resource_name_serialization() {
assert_eq!(
serde_json::to_string(&ResourceName::AggregatedReport).unwrap(),
r#""AGGREGATED_REPORT""#
);
assert_eq!(
serde_json::to_string(&ResourceName::Private(String::from("something else"))).unwrap(),
r#""something else""#
);
assert_eq!(
serde_json::from_str::<ResourceName>(r#""AGGREGATED_REPORT""#).unwrap(),
ResourceName::AggregatedReport
);
assert_eq!(
serde_json::from_str::<ResourceName>(r#""something else""#).unwrap(),
ResourceName::Private(String::from("something else"))
);
assert!(serde_json::from_str::<ResourceName>(r#""""#).is_err());
assert!(serde_json::from_str::<ResourceName>(&format!("\"{}\"", "x".repeat(129))).is_err());
}
#[test]
fn parses_example() {
let example = r#"[{
"id": "object-999",
"createdDateTime": "2023-06-15T09:30:00Z",
"modificationDateTime": "2023-06-15T09:30:00Z",
"objectType": "REPORT",
"eventID": "object-999",
"clientName": "VEN-999",
"reportName": "Battery_usage_04112023",
"payloadDescriptors": null,
"resources": [
{
"resourceName": "RESOURCE-999",
"intervalPeriod": {
"start": "2023-06-15T09:30:00Z",
"duration": "PT1H",
"randomizeStart": "PT1H"
},
"intervals": [
{
"id": 0,
"intervalPeriod": {
"start": "2023-06-15T09:30:00Z",
"duration": "PT1H",
"randomizeStart": "PT1H"
},
"payloads": [
{
"type": "PRICE",
"values": [0.17]
}
]
}
]
}
],
"clientID": "249rj49jiej"
}]"#;
let expected = Report {
id: ReportId("object-999".parse().unwrap()),
created_date_time: "2023-06-15T09:30:00Z".parse().unwrap(),
modification_date_time: "2023-06-15T09:30:00Z".parse().unwrap(),
content: ReportRequest {
event_id: EventId("object-999".parse().unwrap()),
client_name: "VEN-999".into(),
report_name: Some("Battery_usage_04112023".into()),
payload_descriptors: None,
resources: vec![ReportResource {
resource_name: ResourceName::Private("RESOURCE-999".into()),
interval_period: Some(IntervalPeriod {
start: "2023-06-15T09:30:00Z".parse().unwrap(),
duration: Some(Duration::PT1H),
randomize_start: Some(Duration::PT1H),
}),
intervals: vec![Interval {
id: 0,
interval_period: Some(IntervalPeriod {
start: "2023-06-15T09:30:00Z".parse().unwrap(),
duration: Some(Duration::PT1H),
randomize_start: Some(Duration::PT1H),
}),
payloads: vec![ValuesMap {
value_type: ValueType("PRICE".into()),
values: vec![Value::Number(0.17)],
}],
}],
}],
},
client_id: ClientId::new("249rj49jiej").unwrap(),
};
assert_eq!(
serde_json::from_str::<Vec<Report>>(example).unwrap()[0],
expected
);
}
}
#[derive(Clone, Serialize, Deserialize, PartialEq, Eq, Debug)]
#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
pub enum ReportType {
Reading,
Usage,
Demand,
Setpoint,
DeltaUsage,
Baseline,
OperatingState,
UpRegulationAvailable,
DownRegulationAvailable,
RegulationSetpoint,
StorageUsableCapacity,
StorageChargeLevel,
StorageMaxDischargePower,
StorageMaxChargePower,
SimpleLevel,
UsageForecast,
StorageDispatchForecast,
LoadShedDeltaAvailable,
GenerationDeltaAvailable,
DataQuality,
ImportReservationCapacity,
ImportReservationFee,
ExportReservationCapacity,
ExportReservationFee,
#[serde(untagged)]
Private(
#[serde(deserialize_with = "crate::string_within_range_inclusive::<1, 128, _>")] String,
),
}
#[derive(Clone, Default, Serialize, Deserialize, PartialEq, Eq, Debug)]
#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
pub enum ReadingType {
#[default]
DirectRead,
Estimated,
Summed,
Mean,
Peak,
Forecast,
Average,
#[serde(untagged)]
Private(String),
}
impl ReadingType {
fn is_default(&self) -> bool {
*self == Self::default()
}
}
#[derive(Clone, Serialize, Deserialize, PartialEq, Eq, Debug)]
#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
pub enum ResourceName {
AggregatedReport,
#[serde(untagged)]
Private(
#[serde(deserialize_with = "crate::string_within_range_inclusive::<1, 128, _>")] String,
),
}