From b5d79c0dd8a0a0bada0a64d2177d665a721548c3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Se=C3=A1n=20de=20B=C3=BArca?= Date: Mon, 15 Apr 2024 15:41:22 -0700 Subject: [PATCH] Use custom {de,}serialization for operation/response envelopes This allows for creating a strongly-typed link between an operation and its associated response, rather than forcing consumers to match on enums when only a single variant is possible without error. --- src/types/get_folder.rs | 31 +++++-- src/types/operations.rs | 36 ++------ src/types/soap.rs | 128 ++++++++++++++++++++++++----- src/types/sync_folder_hierarchy.rs | 20 ++++- 4 files changed, 156 insertions(+), 59 deletions(-) diff --git a/src/types/get_folder.rs b/src/types/get_folder.rs index e53a3de..990d654 100644 --- a/src/types/get_folder.rs +++ b/src/types/get_folder.rs @@ -5,18 +5,29 @@ use serde::Deserialize; use xml_struct::XmlSerialize; -use crate::{BaseFolderId, Folder, FolderShape, ResponseClass}; +use crate::{ + BaseFolderId, Folder, FolderShape, Operation, OperationResponse, ResponseClass, MESSAGES_NS_URI, +}; -/// The request to get one or more folder(s). +/// A request to get information on one or more folders. /// /// See #[derive(Debug, XmlSerialize)] +#[xml_struct(default_ns = MESSAGES_NS_URI)] pub struct GetFolder { pub folder_shape: FolderShape, pub folder_ids: Vec, } -/// The response to a GetFolder request. +impl Operation for GetFolder { + type Response = GetFolderResponse; + + fn name() -> &'static str { + "GetFolder" + } +} + +/// A response to a [`GetFolder`] request. /// /// See #[derive(Debug, Deserialize)] @@ -25,14 +36,22 @@ pub struct GetFolderResponse { pub response_messages: ResponseMessages, } -/// A collection of response messages from a GetFolder response. +impl OperationResponse for GetFolderResponse { + fn name() -> &'static str { + "GetFolderResponse" + } +} + +/// A collection of responses for individual entities within a request. +/// +/// See #[derive(Debug, Deserialize)] #[serde(rename_all = "PascalCase")] pub struct ResponseMessages { pub get_folder_response_message: Vec, } -/// A message in a GetFolder response. +/// A response to a request for an individual folder within a [`GetFolder`] operation. /// /// See #[derive(Debug, Deserialize)] @@ -43,7 +62,7 @@ pub struct GetFolderResponseMessage { pub folders: Folders, } -/// A list of folders in a GetFolder response message. +/// A collection of information on Exchange folders. /// /// See #[derive(Debug, Deserialize)] diff --git a/src/types/operations.rs b/src/types/operations.rs index 97f8994..d538902 100644 --- a/src/types/operations.rs +++ b/src/types/operations.rs @@ -5,38 +5,12 @@ use serde::Deserialize; use xml_struct::XmlSerialize; -use crate::{ - get_folder::{GetFolder, GetFolderResponse}, - sync_folder_hierarchy::{SyncFolderHierarchy, SyncFolderHierarchyResponse}, - MESSAGES_NS_URI, -}; +pub trait Operation: XmlSerialize { + type Response: OperationResponse; -/// Available EWS operations (requests) that can be performed against an -/// Exchange server. -#[derive(Debug, XmlSerialize)] -#[xml_struct(default_ns = MESSAGES_NS_URI)] -pub enum Operation { - /// Retrieve information regarding one or more folder(s). - /// - /// See - GetFolder(GetFolder), - - /// Retrieve the latest changes in the folder hierarchy for this mailbox. - /// - /// See - SyncFolderHierarchy(SyncFolderHierarchy), + fn name() -> &'static str; } -/// Responses to available operations. -#[derive(Debug, Deserialize)] -pub enum OperationResponse { - /// The response to a GetFolder operation. - /// - /// See - GetFolderResponse(GetFolderResponse), - - /// The response to a SyncFolderHierarchy operation. - /// - /// See - SyncFolderHierarchyResponse(SyncFolderHierarchyResponse), +pub trait OperationResponse: for<'de> Deserialize<'de> { + fn name() -> &'static str; } diff --git a/src/types/soap.rs b/src/types/soap.rs index 1d89e19..a8c17b2 100644 --- a/src/types/soap.rs +++ b/src/types/soap.rs @@ -2,14 +2,17 @@ * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ +use std::marker::PhantomData; + use quick_xml::{ events::{BytesDecl, BytesEnd, BytesStart, Event}, Reader, Writer, }; -use serde::Deserialize; -use xml_struct::XmlSerialize; +use serde::{de::Visitor, Deserialize, Deserializer}; -use crate::{Error, MessageXml, ResponseCode, SOAP_NS_URI, TYPES_NS_URI}; +use crate::{ + Error, MessageXml, Operation, OperationResponse, ResponseCode, SOAP_NS_URI, TYPES_NS_URI, +}; /// A SOAP envelope wrapping an EWS operation. /// @@ -21,10 +24,13 @@ pub struct Envelope { impl Envelope where - B: XmlSerialize, + B: Operation, { /// Serializes the SOAP envelope as a complete XML document. pub fn as_xml_document(&self) -> Result, Error> { + const SOAP_ENVELOPE: &str = "soap:Envelope"; + const SOAP_BODY: &str = "soap:Body"; + let mut writer = { let inner: Vec = Default::default(); Writer::new(inner) @@ -33,16 +39,19 @@ where // All EWS examples use XML 1.0 with UTF-8, so stick to that for now. writer.write_event(Event::Decl(BytesDecl::new("1.0", Some("utf-8"), None)))?; - // To get around having to make `Envelope` itself implement - // `XmlSerialize` + // We manually write these elements in order to control the name we + // write the body with. writer.write_event(Event::Start( - BytesStart::new("soap:Envelope") + BytesStart::new(SOAP_ENVELOPE) .with_attributes([("xmlns:soap", SOAP_NS_URI), ("xmlns:t", TYPES_NS_URI)]), ))?; + writer.write_event(Event::Start(BytesStart::new(SOAP_BODY)))?; - self.body.serialize_as_element(&mut writer, "soap:Body")?; + // Write the operation itself. + self.body.serialize_as_element(&mut writer, B::name())?; - writer.write_event(Event::End(BytesEnd::new("soap:Envelope")))?; + writer.write_event(Event::End(BytesEnd::new(SOAP_BODY)))?; + writer.write_event(Event::End(BytesEnd::new(SOAP_ENVELOPE)))?; Ok(writer.into_inner()) } @@ -50,20 +59,73 @@ where impl Envelope where - B: for<'de> Deserialize<'de>, + B: OperationResponse, { /// Populates an [`Envelope`] from raw XML. pub fn from_xml_document(document: &[u8]) -> Result { - #[derive(Deserialize)] - #[serde(rename_all = "PascalCase")] - struct DummyEnvelope { - body: DummyBody, + struct BodyVisitor(PhantomData); + + impl<'de, T> Visitor<'de> for BodyVisitor + where + T: OperationResponse, + { + type Value = T; + + fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result { + formatter.write_str("EWS operation response body") + } + + fn visit_map(self, mut map: A) -> Result + where + A: serde::de::MapAccess<'de>, + { + let name: Option = map.next_key()?; + if let Some(name) = name { + let expected = T::name(); + if name.as_str() != expected { + return Err(serde::de::Error::custom(format_args!( + "unknown field `{}`, expected {}", + name, expected + ))); + } + + let value = map.next_value()?; + + // To satisfy quick-xml's serde impl, we need to consume the + // final `None` key value in order to successfully complete. + if let Some(name) = map.next_key::()? { + return Err(serde::de::Error::custom(format_args!( + "unexpected field `{}`", + name + ))); + } + + return Ok(value); + } + + Err(serde::de::Error::invalid_type( + serde::de::Unexpected::Map, + &self, + )) + } + } + + fn deserialize_body<'de, D, T>(body: D) -> Result + where + D: Deserializer<'de>, + T: OperationResponse, + { + body.deserialize_map(BodyVisitor::(PhantomData)) } #[derive(Deserialize)] - struct DummyBody { - #[serde(rename = "$value")] - inner: T, + #[serde(rename_all = "PascalCase")] + struct DummyEnvelope + where + T: OperationResponse, + { + #[serde(deserialize_with = "deserialize_body")] + body: T, } // The body of an envelope can contain a fault, indicating an error with @@ -83,7 +145,7 @@ where let envelope: DummyEnvelope = quick_xml::de::from_reader(document)?; Ok(Envelope { - body: envelope.body.inner, + body: envelope.body, }) } } @@ -391,7 +453,7 @@ pub struct FaultDetail { mod tests { use serde::Deserialize; - use crate::Error; + use crate::{Error, OperationResponse}; use super::Envelope; @@ -405,6 +467,12 @@ mod tests { _other_field: (), } + impl OperationResponse for SomeStruct { + fn name() -> &'static str { + "Foo" + } + } + // This XML is contrived, with a custom structure defined in order to // test the generic behavior of the interface. let xml = r#"testing content"#; @@ -421,10 +489,19 @@ mod tests { #[test] fn deserialize_envelope_with_schema_fault() { + #[derive(Debug, Deserialize)] + struct Foo; + + impl OperationResponse for Foo { + fn name() -> &'static str { + "Foo" + } + } + // This XML is drawn from testing data for `evolution-ews`. let xml = r#"a:ErrorSchemaValidationThe request failed schema validation: The 'Id' attribute is invalid - The value 'invalidparentid' is invalid according to its datatype 'http://schemas.microsoft.com/exchange/services/2006/types:DistinguishedFolderIdNameType' - The Enumeration constraint failed.ErrorSchemaValidationThe request failed schema validation.2630The 'Id' attribute is invalid - The value 'invalidparentid' is invalid according to its datatype 'http://schemas.microsoft.com/exchange/services/2006/types:DistinguishedFolderIdNameType' - The Enumeration constraint failed."#; - let err = >::from_xml_document(xml.as_bytes()) + let err = >::from_xml_document(xml.as_bytes()) .expect_err("should return error when body contains fault"); if let Error::RequestFault(fault) = err { @@ -463,12 +540,21 @@ mod tests { #[test] fn deserialize_envelope_with_server_busy_fault() { + #[derive(Debug, Deserialize)] + struct Foo; + + impl OperationResponse for Foo { + fn name() -> &'static str { + "Foo" + } + } + // This XML is contrived based on what's known of the shape of // `ErrorServerBusy` responses. It should be replaced when we have // real-life examples. let xml = r#"a:ErrorServerBusyI made this up because I don't have real testing data. 🙃ErrorServerBusyWho really knows?25"#; - let err = >::from_xml_document(xml.as_bytes()) + let err = >::from_xml_document(xml.as_bytes()) .expect_err("should return error when body contains fault"); // The testing here isn't as thorough as the invalid schema test due to diff --git a/src/types/sync_folder_hierarchy.rs b/src/types/sync_folder_hierarchy.rs index b97c06e..722e8d2 100644 --- a/src/types/sync_folder_hierarchy.rs +++ b/src/types/sync_folder_hierarchy.rs @@ -5,18 +5,30 @@ use serde::Deserialize; use xml_struct::XmlSerialize; -use crate::{BaseFolderId, Folder, FolderId, FolderShape, ResponseClass}; +use crate::{ + BaseFolderId, Folder, FolderId, FolderShape, Operation, OperationResponse, ResponseClass, + MESSAGES_NS_URI, +}; /// The request for update regarding the folder hierarchy in a mailbox. /// /// See #[derive(Debug, XmlSerialize)] +#[xml_struct(default_ns = MESSAGES_NS_URI)] pub struct SyncFolderHierarchy { pub folder_shape: FolderShape, pub sync_folder_id: Option, pub sync_state: Option, } +impl Operation for SyncFolderHierarchy { + type Response = SyncFolderHierarchyResponse; + + fn name() -> &'static str { + "SyncFolderHierarchy" + } +} + /// The response to a SyncFolderHierarchy request. /// /// See @@ -26,6 +38,12 @@ pub struct SyncFolderHierarchyResponse { pub response_messages: ResponseMessages, } +impl OperationResponse for SyncFolderHierarchyResponse { + fn name() -> &'static str { + "SyncFolderHierarchyResponse" + } +} + /// A collection of response messages from a SyncFolderHierarchy response. #[derive(Debug, Deserialize)] #[serde(rename_all = "PascalCase")]