Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Use custom {de,}serialization for operation/response envelopes #5

Closed
wants to merge 7 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
43 changes: 37 additions & 6 deletions src/types/get_folder.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,18 +5,36 @@
use serde::Deserialize;
use xml_struct::XmlSerialize;

use crate::{BaseFolderId, Folder, FolderShape, ResponseClass};
use crate::{
types::sealed::EnvelopeBodyContents, 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 <https://learn.microsoft.com/en-us/exchange/client-developer/web-service-reference/getfolder>
#[derive(Debug, XmlSerialize)]
#[xml_struct(default_ns = MESSAGES_NS_URI)]
pub struct GetFolder {
/// A description of the information to be included in the response for each
/// retrieved folder.
pub folder_shape: FolderShape,

/// A list of IDs for which to retrieve folder information.
pub folder_ids: Vec<BaseFolderId>,
}

/// The response to a GetFolder request.
impl Operation for GetFolder {
type Response = GetFolderResponse;
}

impl EnvelopeBodyContents for GetFolder {
fn name() -> &'static str {
"GetFolder"
}
}

/// A response to a [`GetFolder`] request.
///
/// See <https://learn.microsoft.com/en-us/exchange/client-developer/web-service-reference/getfolderresponse>
#[derive(Debug, Deserialize)]
Expand All @@ -25,25 +43,38 @@ pub struct GetFolderResponse {
pub response_messages: ResponseMessages,
}

/// A collection of response messages from a GetFolder response.
impl OperationResponse for GetFolderResponse {}

impl EnvelopeBodyContents for GetFolderResponse {
fn name() -> &'static str {
"GetFolderResponse"
}
}

/// A collection of responses for individual entities within a request.
///
/// See <https://learn.microsoft.com/en-us/exchange/client-developer/web-service-reference/responsemessages>
#[derive(Debug, Deserialize)]
#[serde(rename_all = "PascalCase")]
pub struct ResponseMessages {
pub get_folder_response_message: Vec<GetFolderResponseMessage>,
}

/// A message in a GetFolder response.
/// A response to a request for an individual folder within a [`GetFolder`] operation.
///
/// See <https://learn.microsoft.com/en-us/exchange/client-developer/web-service-reference/getfolderresponsemessage>
#[derive(Debug, Deserialize)]
#[serde(rename_all = "PascalCase")]
pub struct GetFolderResponseMessage {
/// The success value of the corresponding request.
#[serde(rename = "@ResponseClass")]
pub response_class: ResponseClass,

/// A collection of the retrieved folders.
pub folders: Folders,
}

/// A list of folders in a GetFolder response message.
/// A collection of information on Exchange folders.
///
/// See <https://learn.microsoft.com/en-us/exchange/client-developer/web-service-reference/folders-ex15websvcsotherref>
#[derive(Debug, Deserialize)]
Expand Down
63 changes: 32 additions & 31 deletions src/types/operations.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,38 +5,39 @@
use serde::Deserialize;
use xml_struct::XmlSerialize;

use crate::{
get_folder::{GetFolder, GetFolderResponse},
sync_folder_hierarchy::{SyncFolderHierarchy, SyncFolderHierarchyResponse},
MESSAGES_NS_URI,
};

/// 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 <https://learn.microsoft.com/en-us/exchange/client-developer/web-service-reference/getfolder-operation#getfolder-request-example>
GetFolder(GetFolder),

/// Retrieve the latest changes in the folder hierarchy for this mailbox.
///
/// See <https://learn.microsoft.com/en-us/exchange/client-developer/web-service-reference/syncfolderhierarchy-operation#syncfolderhierarchy-request-example>
SyncFolderHierarchy(SyncFolderHierarchy),
/// A marker trait for EWS operations.
///
/// Types implementing this trait may appear in requests to EWS as the operation
/// to be performed.
///
/// # Usage
///
/// See [`Envelope`] for details.
///
/// [`Envelope`]: crate::soap::Envelope
pub trait Operation: XmlSerialize + sealed::EnvelopeBodyContents {
/// The structure returned by EWS in response to requests containing this
/// operation.
type Response: OperationResponse;
}

/// Responses to available operations.
#[derive(Debug, Deserialize)]
pub enum OperationResponse {
/// The response to a GetFolder operation.
///
/// See <https://learn.microsoft.com/en-us/exchange/client-developer/web-service-reference/getfolder-operation#getfolder-response-example>
GetFolderResponse(GetFolderResponse),
/// A marker trait for EWS operation responses.
///
/// Types implementing this trait may appear in responses from EWS after
/// requesting an operation be performed.
///
/// # Usage
///
/// See [`Envelope`] for details.
///
/// [`Envelope`]: crate::soap::Envelope
pub trait OperationResponse: for<'de> Deserialize<'de> + sealed::EnvelopeBodyContents {}

/// The response to a SyncFolderHierarchy operation.
///
/// See <https://learn.microsoft.com/en-us/exchange/client-developer/web-service-reference/syncfolderhierarchy-operation#successful-syncfolderhierarchy-response>
SyncFolderHierarchyResponse(SyncFolderHierarchyResponse),
pub(super) mod sealed {
/// A trait for structures which may appear in the body of a SOAP envelope.
pub trait EnvelopeBodyContents {
/// Gets the name of the element enclosing the contents of this
/// structure when represented in XML.
fn name() -> &'static str;
Comment on lines +38 to +41
Copy link
Collaborator Author

@leftmostcat leftmostcat Apr 16, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Deriving this would be pretty trivial and we probably want to do that soon. I just wanted to get it working.

For derivation, we could even potentially make it so that Operation and OperationResponse are the traits used for the macro(s) and getting EnvelopeBodyContents is a "hidden" bonus. It would just require that we add something like a #[response(GetFolderResponse)] attribute to the Operation derive. Thoughts on this are welcomed.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Perhaps something more specific to the project such as #[ews:node(GetFolderResponse)] ?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I believe the helper attribute needs to be a valid identifier, but that's something we can figure out later if we do want to add this derive functionality in that form.

}
}
83 changes: 55 additions & 28 deletions src/types/soap.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,15 @@ use quick_xml::{
events::{BytesDecl, BytesEnd, BytesStart, Event},
Reader, Writer,
};
use serde::Deserialize;
use xml_struct::XmlSerialize;

use crate::{Error, MessageXml, ResponseCode, SOAP_NS_URI, TYPES_NS_URI};
use crate::{
Error, MessageXml, Operation, OperationResponse, ResponseCode, SOAP_NS_URI, TYPES_NS_URI,
};

mod de;
use self::de::DeserializeEnvelope;

/// A SOAP envelope wrapping an EWS operation.
/// A SOAP envelope containing the body of an EWS operation or response.
///
/// See <https://www.w3.org/TR/2000/NOTE-SOAP-20000508/#_Toc478383494>
#[derive(Debug)]
Expand All @@ -21,10 +24,13 @@ pub struct Envelope<B> {

impl<B> Envelope<B>
where
B: XmlSerialize,
B: Operation,
{
/// Serializes the SOAP envelope as a complete XML document.
pub fn as_xml_document(&self) -> Result<Vec<u8>, Error> {
const SOAP_ENVELOPE: &str = "soap:Envelope";
const SOAP_BODY: &str = "soap:Body";

let mut writer = {
let inner: Vec<u8> = Default::default();
Writer::new(inner)
Expand All @@ -33,39 +39,30 @@ 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())
}
}

impl<B> Envelope<B>
where
B: for<'de> Deserialize<'de>,
B: OperationResponse,
{
/// Populates an [`Envelope`] from raw XML.
pub fn from_xml_document(document: &[u8]) -> Result<Self, Error> {
#[derive(Deserialize)]
#[serde(rename_all = "PascalCase")]
struct DummyEnvelope<T> {
body: DummyBody<T>,
}

#[derive(Deserialize)]
struct DummyBody<T> {
#[serde(rename = "$value")]
inner: T,
}

// The body of an envelope can contain a fault, indicating an error with
// a request. We want to parse that and return it as the `Err` portion
// of a result. However, Microsoft includes a field in their fault
Expand All @@ -80,10 +77,10 @@ where
return Err(Error::RequestFault(Box::new(fault)));
}

let envelope: DummyEnvelope<B> = quick_xml::de::from_reader(document)?;
let envelope: DeserializeEnvelope<B> = quick_xml::de::from_reader(document)?;

Ok(Envelope {
body: envelope.body.inner,
body: envelope.body,
})
}
}
Expand Down Expand Up @@ -391,7 +388,7 @@ pub struct FaultDetail {
mod tests {
use serde::Deserialize;

use crate::Error;
use crate::{types::sealed::EnvelopeBodyContents, Error, OperationResponse};

use super::Envelope;

Expand All @@ -405,6 +402,14 @@ mod tests {
_other_field: (),
}

impl OperationResponse for SomeStruct {}

impl EnvelopeBodyContents 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#"<?xml version="1.0" encoding="utf-8"?><s:Envelope xmlns:s="http://schemas.xmlsoap.org/soap/envelope/"><s:Body><foo:Foo><text>testing content</text><other_field/></foo:Foo></s:Body></s:Envelope>"#;
Expand All @@ -421,10 +426,21 @@ mod tests {

#[test]
fn deserialize_envelope_with_schema_fault() {
#[derive(Debug, Deserialize)]
struct Foo;

impl OperationResponse for Foo {}

impl EnvelopeBodyContents for Foo {
fn name() -> &'static str {
"Foo"
}
}

// This XML is drawn from testing data for `evolution-ews`.
let xml = r#"<?xml version="1.0" encoding="utf-8"?><s:Envelope xmlns:s="http://schemas.xmlsoap.org/soap/envelope/"><s:Body><s:Fault><faultcode xmlns:a="http://schemas.microsoft.com/exchange/services/2006/types">a:ErrorSchemaValidation</faultcode><faultstring xml:lang="en-US">The 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.</faultstring><detail><e:ResponseCode xmlns:e="http://schemas.microsoft.com/exchange/services/2006/errors">ErrorSchemaValidation</e:ResponseCode><e:Message xmlns:e="http://schemas.microsoft.com/exchange/services/2006/errors">The request failed schema validation.</e:Message><t:MessageXml xmlns:t="http://schemas.microsoft.com/exchange/services/2006/types"><t:LineNumber>2</t:LineNumber><t:LinePosition>630</t:LinePosition><t:Violation>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.</t:Violation></t:MessageXml></detail></s:Fault></s:Body></s:Envelope>"#;

let err = <Envelope<()>>::from_xml_document(xml.as_bytes())
let err = <Envelope<Foo>>::from_xml_document(xml.as_bytes())
.expect_err("should return error when body contains fault");

if let Error::RequestFault(fault) = err {
Expand Down Expand Up @@ -463,12 +479,23 @@ mod tests {

#[test]
fn deserialize_envelope_with_server_busy_fault() {
#[derive(Debug, Deserialize)]
struct Foo;

impl OperationResponse for Foo {}

impl EnvelopeBodyContents 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#"<?xml version="1.0" encoding="utf-8"?><s:Envelope xmlns:s="http://schemas.xmlsoap.org/soap/envelope/"><s:Body><s:Fault><faultcode xmlns:a="http://schemas.microsoft.com/exchange/services/2006/types">a:ErrorServerBusy</faultcode><faultstring xml:lang="en-US">I made this up because I don't have real testing data. 🙃</faultstring><detail><e:ResponseCode xmlns:e="http://schemas.microsoft.com/exchange/services/2006/errors">ErrorServerBusy</e:ResponseCode><e:Message xmlns:e="http://schemas.microsoft.com/exchange/services/2006/errors">Who really knows?</e:Message><t:MessageXml xmlns:t="http://schemas.microsoft.com/exchange/services/2006/types"><t:Value Name="BackOffMilliseconds">25</t:Value></t:MessageXml></detail></s:Fault></s:Body></s:Envelope>"#;

let err = <Envelope<()>>::from_xml_document(xml.as_bytes())
let err = <Envelope<Foo>>::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
Expand Down
Loading
Loading