1use bon::Builder;
12use serde::{Deserialize, Serialize};
13
14use crate::ocpi_enum;
15use crate::ocpi_open_enum;
16use crate::types::validate_fields;
17use crate::types::{CiString, DateTime, DisplayText, Extensions, Url, Validate, Validator, ViolationCode};
18
19use super::tokens::Token;
20
21#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, Builder)]
29#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
30#[builder(on(_, into))]
31pub struct StartSession {
32 pub response_url: Url,
37 pub token: Token,
39 pub location_id: CiString<36>,
41 #[serde(default, skip_serializing_if = "Option::is_none")]
43 pub evse_uid: Option<CiString<36>>,
44 #[serde(default, skip_serializing_if = "Option::is_none")]
51 pub connector_id: Option<CiString<36>>,
52 #[serde(default, skip_serializing_if = "Option::is_none")]
54 pub authorization_reference: Option<CiString<36>>,
55 #[serde(flatten, default, skip_serializing_if = "Extensions::is_empty")]
57 #[builder(default)]
58 pub extensions: Extensions,
59}
60
61impl Validate for StartSession {
62 fn validate_in(&self, v: &mut Validator) {
63 validate_fields!(
64 self,
65 v,
66 response_url,
67 token,
68 location_id,
69 evse_uid,
70 connector_id,
71 authorization_reference,
72 );
73 if self.connector_id.is_some() && self.evse_uid.is_none() {
75 v.report_at(
76 "evse_uid",
77 ViolationCode::MissingConditional,
78 "is required when `connector_id` is set",
79 );
80 }
81 }
82}
83
84#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, Builder)]
88#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
89#[builder(on(_, into))]
90pub struct StopSession {
91 pub response_url: Url,
93 pub session_id: CiString<36>,
95 #[serde(flatten, default, skip_serializing_if = "Extensions::is_empty")]
97 #[builder(default)]
98 pub extensions: Extensions,
99}
100
101impl Validate for StopSession {
102 fn validate_in(&self, v: &mut Validator) {
103 validate_fields!(self, v, response_url, session_id);
104 }
105}
106
107#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, Builder)]
115#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
116#[builder(on(_, into))]
117pub struct ReserveNow {
118 pub response_url: Url,
120 pub token: Token,
122 pub expiry_date: DateTime,
124 pub reservation_id: CiString<36>,
130 pub location_id: CiString<36>,
132 #[serde(default, skip_serializing_if = "Option::is_none")]
137 pub evse_uid: Option<CiString<36>>,
138 #[serde(default, skip_serializing_if = "Option::is_none")]
140 pub authorization_reference: Option<CiString<36>>,
141 #[serde(flatten, default, skip_serializing_if = "Extensions::is_empty")]
143 #[builder(default)]
144 pub extensions: Extensions,
145}
146
147impl Validate for ReserveNow {
148 fn validate_in(&self, v: &mut Validator) {
149 validate_fields!(
150 self,
151 v,
152 response_url,
153 token,
154 expiry_date,
155 reservation_id,
156 location_id,
157 evse_uid,
158 authorization_reference,
159 );
160 }
161}
162
163#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, Builder)]
170#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
171#[builder(on(_, into))]
172pub struct CancelReservation {
173 pub response_url: Url,
175 pub reservation_id: CiString<36>,
177 #[serde(flatten, default, skip_serializing_if = "Extensions::is_empty")]
179 #[builder(default)]
180 pub extensions: Extensions,
181}
182
183impl Validate for CancelReservation {
184 fn validate_in(&self, v: &mut Validator) {
185 validate_fields!(self, v, response_url, reservation_id);
186 }
187}
188
189#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, Builder)]
196#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
197#[builder(on(_, into))]
198pub struct UnlockConnector {
199 pub response_url: Url,
201 pub location_id: CiString<36>,
203 pub evse_uid: CiString<36>,
205 pub connector_id: CiString<36>,
207 #[serde(flatten, default, skip_serializing_if = "Extensions::is_empty")]
209 #[builder(default)]
210 pub extensions: Extensions,
211}
212
213impl Validate for UnlockConnector {
214 fn validate_in(&self, v: &mut Validator) {
215 validate_fields!(self, v, response_url, location_id, evse_uid, connector_id);
216 }
217}
218
219#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, Builder)]
227#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
228#[builder(on(_, into))]
229pub struct CommandResponse {
230 pub result: CommandResponseType,
232 pub timeout: u32,
237 #[serde(default, skip_serializing_if = "Vec::is_empty")]
239 #[builder(default)]
240 pub message: Vec<DisplayText>,
241 #[serde(flatten, default, skip_serializing_if = "Extensions::is_empty")]
243 #[builder(default)]
244 pub extensions: Extensions,
245}
246
247impl CommandResponse {
248 #[must_use]
250 pub const fn timeout_duration(&self) -> std::time::Duration {
251 std::time::Duration::from_secs(self.timeout as u64)
252 }
253
254 #[must_use]
259 pub fn expects_result(&self) -> bool {
260 self.result == CommandResponseType::Accepted
261 }
262}
263
264impl Validate for CommandResponse {
265 fn validate_in(&self, v: &mut Validator) {
266 validate_fields!(self, v, result, message);
267 if self.result == CommandResponseType::Accepted && self.timeout == 0 {
268 v.report_at(
269 "timeout",
270 ViolationCode::OutOfRange,
271 "an accepted command needs a non-zero timeout for the eMSP to wait on",
272 );
273 }
274 }
275}
276
277#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, Builder)]
281#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
282#[builder(on(_, into))]
283pub struct CommandResult {
284 pub result: CommandResultType,
286 #[serde(default, skip_serializing_if = "Vec::is_empty")]
288 #[builder(default)]
289 pub message: Vec<DisplayText>,
290 #[serde(flatten, default, skip_serializing_if = "Extensions::is_empty")]
292 #[builder(default)]
293 pub extensions: Extensions,
294}
295
296impl Validate for CommandResult {
297 fn validate_in(&self, v: &mut Validator) {
298 validate_fields!(self, v, result, message);
299 }
300}
301
302#[derive(Clone, Debug, PartialEq)]
308#[non_exhaustive]
309pub enum Command {
310 CancelReservation(CancelReservation),
312 ReserveNow(Box<ReserveNow>),
314 StartSession(Box<StartSession>),
316 StopSession(StopSession),
318 UnlockConnector(UnlockConnector),
320}
321
322impl Command {
323 #[must_use]
325 pub fn command_type(&self) -> CommandType {
326 match self {
327 Self::CancelReservation(_) => CommandType::CancelReservation,
328 Self::ReserveNow(_) => CommandType::ReserveNow,
329 Self::StartSession(_) => CommandType::StartSession,
330 Self::StopSession(_) => CommandType::StopSession,
331 Self::UnlockConnector(_) => CommandType::UnlockConnector,
332 }
333 }
334
335 #[must_use]
337 pub fn response_url(&self) -> &Url {
338 match self {
339 Self::CancelReservation(c) => &c.response_url,
340 Self::ReserveNow(c) => &c.response_url,
341 Self::StartSession(c) => &c.response_url,
342 Self::StopSession(c) => &c.response_url,
343 Self::UnlockConnector(c) => &c.response_url,
344 }
345 }
346}
347
348impl Validate for Command {
349 fn validate_in(&self, v: &mut Validator) {
350 match self {
351 Self::CancelReservation(c) => c.validate_in(v),
352 Self::ReserveNow(c) => c.validate_in(v),
353 Self::StartSession(c) => c.validate_in(v),
354 Self::StopSession(c) => c.validate_in(v),
355 Self::UnlockConnector(c) => c.validate_in(v),
356 }
357 }
358}
359
360ocpi_enum! {
361 pub enum CommandResponseType {
365 NotSupported = "NOT_SUPPORTED",
367 Rejected = "REJECTED",
369 Accepted = "ACCEPTED",
371 UnknownSession = "UNKNOWN_SESSION",
373 }
374}
375
376ocpi_enum! {
377 pub enum CommandResultType {
384 Accepted = "ACCEPTED",
386 CanceledReservation = "CANCELED_RESERVATION",
388 EvseOccupied = "EVSE_OCCUPIED",
390 EvseInoperative = "EVSE_INOPERATIVE",
392 Failed = "FAILED",
394 NotSupported = "NOT_SUPPORTED",
396 Rejected = "REJECTED",
398 Timeout = "TIMEOUT",
400 UnknownReservation = "UNKNOWN_RESERVATION",
402 }
403}
404
405ocpi_open_enum! {
406 pub enum CommandType {
410 CancelReservation = "CANCEL_RESERVATION",
412 ReserveNow = "RESERVE_NOW",
414 StartSession = "START_SESSION",
416 StopSession = "STOP_SESSION",
418 UnlockConnector = "UNLOCK_CONNECTOR",
420 }
421}
422
423#[cfg(test)]
424mod tests {
425 use super::*;
426 use crate::types::Url;
427
428 fn url() -> Url {
429 Url::new("https://msp.example.com/ocpi/emsp/2.3.0/commands/START_SESSION/1234").unwrap()
430 }
431
432 #[test]
433 fn a_connector_id_without_an_evse_uid_is_incomplete() {
434 let token = crate::v2_3_0::tokens::Token::builder()
435 .country_code("NL")
436 .party_id("TNM")
437 .uid("012345678")
438 .token_type(crate::v2_3_0::tokens::TokenType::AppUser)
439 .contract_id("NL-TNM-C12345678-X")
440 .issuer("TheNewMotion")
441 .valid(true)
442 .whitelist(crate::v2_3_0::tokens::WhitelistType::Never)
443 .last_updated("2024-01-01T00:00:00Z".parse::<DateTime>().unwrap())
444 .build();
445 let cmd = StartSession::builder()
446 .response_url(url())
447 .token(token)
448 .location_id("LOC1")
449 .connector_id("1")
450 .build();
451 let err = cmd.validate().unwrap_err();
452 assert_eq!(err.as_slice()[0].pointer, "/evse_uid");
453 }
454
455 #[test]
456 fn only_an_accepted_response_promises_a_result() {
457 let accepted =
458 CommandResponse::builder().result(CommandResponseType::Accepted).timeout(30u32).build();
459 assert!(accepted.expects_result());
460 assert_eq!(accepted.timeout_duration(), std::time::Duration::from_secs(30));
461 assert!(accepted.validate().is_ok());
462
463 let rejected = CommandResponse::builder().result(CommandResponseType::Rejected).timeout(0u32).build();
464 assert!(!rejected.expects_result());
465 assert!(rejected.validate().is_ok(), "a rejected command needs no timeout");
466
467 let bad = CommandResponse::builder().result(CommandResponseType::Accepted).timeout(0u32).build();
468 assert_eq!(bad.validate().unwrap_err().as_slice()[0].pointer, "/timeout");
469 }
470
471 #[test]
472 fn response_and_result_enums_stay_distinct() {
473 assert!("CANCELED_RESERVATION".parse::<CommandResponseType>().is_err());
474 assert!("UNKNOWN_SESSION".parse::<CommandResultType>().is_err());
475 assert_eq!(
476 "CANCELED_RESERVATION".parse::<CommandResultType>().unwrap(),
477 CommandResultType::CanceledReservation
478 );
479 }
480
481 #[test]
482 fn the_command_enum_names_its_own_type_and_callback() {
483 let cmd = Command::StopSession(StopSession::builder().response_url(url()).session_id("101").build());
484 assert_eq!(cmd.command_type(), CommandType::StopSession);
485 assert_eq!(cmd.response_url(), &url());
486 }
487}