From b5d79c0dd8a0a0bada0a64d2177d665a721548c3 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Se=C3=A1n=20de=20B=C3=BArca?= <leftmostcat@gmail.com>
Date: Mon, 15 Apr 2024 15:41:22 -0700
Subject: [PATCH 1/7] 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 <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 {
     pub folder_shape: FolderShape,
     pub folder_ids: Vec<BaseFolderId>,
 }
 
-/// 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 <https://learn.microsoft.com/en-us/exchange/client-developer/web-service-reference/getfolderresponse>
 #[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 <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)]
@@ -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 <https://learn.microsoft.com/en-us/exchange/client-developer/web-service-reference/folders-ex15websvcsotherref>
 #[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 <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),
+    fn name() -> &'static str;
 }
 
-/// 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),
-
-    /// 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 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<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)
@@ -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<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>,
+        struct BodyVisitor<T>(PhantomData<T>);
+
+        impl<'de, T> Visitor<'de> for BodyVisitor<T>
+        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<A>(self, mut map: A) -> Result<Self::Value, A::Error>
+            where
+                A: serde::de::MapAccess<'de>,
+            {
+                let name: Option<String> = 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::<String>()? {
+                        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<T, D::Error>
+        where
+            D: Deserializer<'de>,
+            T: OperationResponse,
+        {
+            body.deserialize_map(BodyVisitor::<T>(PhantomData))
         }
 
         #[derive(Deserialize)]
-        struct DummyBody<T> {
-            #[serde(rename = "$value")]
-            inner: T,
+        #[serde(rename_all = "PascalCase")]
+        struct DummyEnvelope<T>
+        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<B> = 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#"<?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>"#;
@@ -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#"<?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 {
@@ -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#"<?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
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 <https://learn.microsoft.com/en-us/exchange/client-developer/web-service-reference/syncfolderhierarchy>
 #[derive(Debug, XmlSerialize)]
+#[xml_struct(default_ns = MESSAGES_NS_URI)]
 pub struct SyncFolderHierarchy {
     pub folder_shape: FolderShape,
     pub sync_folder_id: Option<BaseFolderId>,
     pub sync_state: Option<String>,
 }
 
+impl Operation for SyncFolderHierarchy {
+    type Response = SyncFolderHierarchyResponse;
+
+    fn name() -> &'static str {
+        "SyncFolderHierarchy"
+    }
+}
+
 /// The response to a SyncFolderHierarchy request.
 ///
 /// See <https://learn.microsoft.com/en-us/exchange/client-developer/web-service-reference/syncfolderhierarchyresponse>
@@ -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")]

From 4635133dcaa27c97d51430e086997103b0c5ee39 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Se=C3=A1n=20de=20B=C3=BArca?= <leftmostcat@gmail.com>
Date: Mon, 15 Apr 2024 15:57:38 -0700
Subject: [PATCH 2/7] Seal naming trait

---
 src/types/get_folder.rs            |  9 +++++++--
 src/types/operations.rs            | 12 +++++++-----
 src/types/soap.rs                  | 14 ++++++++++----
 src/types/sync_folder_hierarchy.rs | 10 +++++++---
 4 files changed, 31 insertions(+), 14 deletions(-)

diff --git a/src/types/get_folder.rs b/src/types/get_folder.rs
index 990d654..b739b93 100644
--- a/src/types/get_folder.rs
+++ b/src/types/get_folder.rs
@@ -6,7 +6,8 @@ use serde::Deserialize;
 use xml_struct::XmlSerialize;
 
 use crate::{
-    BaseFolderId, Folder, FolderShape, Operation, OperationResponse, ResponseClass, MESSAGES_NS_URI,
+    types::sealed::NamedStructure, BaseFolderId, Folder, FolderShape, Operation, OperationResponse,
+    ResponseClass, MESSAGES_NS_URI,
 };
 
 /// A request to get information on one or more folders.
@@ -21,7 +22,9 @@ pub struct GetFolder {
 
 impl Operation for GetFolder {
     type Response = GetFolderResponse;
+}
 
+impl NamedStructure for GetFolder {
     fn name() -> &'static str {
         "GetFolder"
     }
@@ -36,7 +39,9 @@ pub struct GetFolderResponse {
     pub response_messages: ResponseMessages,
 }
 
-impl OperationResponse for GetFolderResponse {
+impl OperationResponse for GetFolderResponse {}
+
+impl NamedStructure for GetFolderResponse {
     fn name() -> &'static str {
         "GetFolderResponse"
     }
diff --git a/src/types/operations.rs b/src/types/operations.rs
index d538902..b967f58 100644
--- a/src/types/operations.rs
+++ b/src/types/operations.rs
@@ -5,12 +5,14 @@
 use serde::Deserialize;
 use xml_struct::XmlSerialize;
 
-pub trait Operation: XmlSerialize {
+pub trait Operation: XmlSerialize + sealed::NamedStructure {
     type Response: OperationResponse;
-
-    fn name() -> &'static str;
 }
 
-pub trait OperationResponse: for<'de> Deserialize<'de> {
-    fn name() -> &'static str;
+pub trait OperationResponse: for<'de> Deserialize<'de> + sealed::NamedStructure {}
+
+pub(super) mod sealed {
+    pub trait NamedStructure {
+        fn name() -> &'static str;
+    }
 }
diff --git a/src/types/soap.rs b/src/types/soap.rs
index a8c17b2..362a5f5 100644
--- a/src/types/soap.rs
+++ b/src/types/soap.rs
@@ -453,7 +453,7 @@ pub struct FaultDetail {
 mod tests {
     use serde::Deserialize;
 
-    use crate::{Error, OperationResponse};
+    use crate::{types::sealed::NamedStructure, Error, OperationResponse};
 
     use super::Envelope;
 
@@ -467,7 +467,9 @@ mod tests {
             _other_field: (),
         }
 
-        impl OperationResponse for SomeStruct {
+        impl OperationResponse for SomeStruct {}
+
+        impl NamedStructure for SomeStruct {
             fn name() -> &'static str {
                 "Foo"
             }
@@ -492,7 +494,9 @@ mod tests {
         #[derive(Debug, Deserialize)]
         struct Foo;
 
-        impl OperationResponse for Foo {
+        impl OperationResponse for Foo {}
+
+        impl NamedStructure for Foo {
             fn name() -> &'static str {
                 "Foo"
             }
@@ -543,7 +547,9 @@ mod tests {
         #[derive(Debug, Deserialize)]
         struct Foo;
 
-        impl OperationResponse for Foo {
+        impl OperationResponse for Foo {}
+
+        impl NamedStructure for Foo {
             fn name() -> &'static str {
                 "Foo"
             }
diff --git a/src/types/sync_folder_hierarchy.rs b/src/types/sync_folder_hierarchy.rs
index 722e8d2..39ec3b0 100644
--- a/src/types/sync_folder_hierarchy.rs
+++ b/src/types/sync_folder_hierarchy.rs
@@ -6,8 +6,8 @@ use serde::Deserialize;
 use xml_struct::XmlSerialize;
 
 use crate::{
-    BaseFolderId, Folder, FolderId, FolderShape, Operation, OperationResponse, ResponseClass,
-    MESSAGES_NS_URI,
+    types::sealed::NamedStructure, BaseFolderId, Folder, FolderId, FolderShape, Operation,
+    OperationResponse, ResponseClass, MESSAGES_NS_URI,
 };
 
 /// The request for update regarding the folder hierarchy in a mailbox.
@@ -23,7 +23,9 @@ pub struct SyncFolderHierarchy {
 
 impl Operation for SyncFolderHierarchy {
     type Response = SyncFolderHierarchyResponse;
+}
 
+impl NamedStructure for SyncFolderHierarchy {
     fn name() -> &'static str {
         "SyncFolderHierarchy"
     }
@@ -38,7 +40,9 @@ pub struct SyncFolderHierarchyResponse {
     pub response_messages: ResponseMessages,
 }
 
-impl OperationResponse for SyncFolderHierarchyResponse {
+impl OperationResponse for SyncFolderHierarchyResponse {}
+
+impl NamedStructure for SyncFolderHierarchyResponse {
     fn name() -> &'static str {
         "SyncFolderHierarchyResponse"
     }

From 4ef6083071996b455b349468b17b873c8ddc1e0c Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Se=C3=A1n=20de=20B=C3=BArca?= <leftmostcat@gmail.com>
Date: Tue, 16 Apr 2024 11:10:34 -0700
Subject: [PATCH 3/7] Improve organization, naming, and documentation

---
 src/types/get_folder.rs            |  6 +--
 src/types/operations.rs            | 31 ++++++++++--
 src/types/soap.rs                  | 79 +++--------------------------
 src/types/soap/de.rs               | 80 ++++++++++++++++++++++++++++++
 src/types/sync_folder_hierarchy.rs |  6 +--
 5 files changed, 121 insertions(+), 81 deletions(-)
 create mode 100644 src/types/soap/de.rs

diff --git a/src/types/get_folder.rs b/src/types/get_folder.rs
index b739b93..300291c 100644
--- a/src/types/get_folder.rs
+++ b/src/types/get_folder.rs
@@ -6,7 +6,7 @@ use serde::Deserialize;
 use xml_struct::XmlSerialize;
 
 use crate::{
-    types::sealed::NamedStructure, BaseFolderId, Folder, FolderShape, Operation, OperationResponse,
+    types::sealed::EnvelopeBodyContents, BaseFolderId, Folder, FolderShape, Operation, OperationResponse,
     ResponseClass, MESSAGES_NS_URI,
 };
 
@@ -24,7 +24,7 @@ impl Operation for GetFolder {
     type Response = GetFolderResponse;
 }
 
-impl NamedStructure for GetFolder {
+impl EnvelopeBodyContents for GetFolder {
     fn name() -> &'static str {
         "GetFolder"
     }
@@ -41,7 +41,7 @@ pub struct GetFolderResponse {
 
 impl OperationResponse for GetFolderResponse {}
 
-impl NamedStructure for GetFolderResponse {
+impl EnvelopeBodyContents for GetFolderResponse {
     fn name() -> &'static str {
         "GetFolderResponse"
     }
diff --git a/src/types/operations.rs b/src/types/operations.rs
index b967f58..857a647 100644
--- a/src/types/operations.rs
+++ b/src/types/operations.rs
@@ -5,14 +5,39 @@
 use serde::Deserialize;
 use xml_struct::XmlSerialize;
 
-pub trait Operation: XmlSerialize + sealed::NamedStructure {
+/// 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;
 }
 
-pub trait OperationResponse: for<'de> Deserialize<'de> + sealed::NamedStructure {}
+/// 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 {}
 
 pub(super) mod sealed {
-    pub trait NamedStructure {
+    /// 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;
     }
 }
diff --git a/src/types/soap.rs b/src/types/soap.rs
index 362a5f5..e30c73d 100644
--- a/src/types/soap.rs
+++ b/src/types/soap.rs
@@ -2,18 +2,18 @@
  * 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::{de::Visitor, Deserialize, Deserializer};
 
 use crate::{
     Error, MessageXml, Operation, OperationResponse, ResponseCode, SOAP_NS_URI, TYPES_NS_URI,
 };
 
+mod de;
+use self::de::DummyEnvelope;
+
 /// A SOAP envelope wrapping an EWS operation.
 ///
 /// See <https://www.w3.org/TR/2000/NOTE-SOAP-20000508/#_Toc478383494>
@@ -63,71 +63,6 @@ where
 {
     /// Populates an [`Envelope`] from raw XML.
     pub fn from_xml_document(document: &[u8]) -> Result<Self, Error> {
-        struct BodyVisitor<T>(PhantomData<T>);
-
-        impl<'de, T> Visitor<'de> for BodyVisitor<T>
-        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<A>(self, mut map: A) -> Result<Self::Value, A::Error>
-            where
-                A: serde::de::MapAccess<'de>,
-            {
-                let name: Option<String> = 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::<String>()? {
-                        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<T, D::Error>
-        where
-            D: Deserializer<'de>,
-            T: OperationResponse,
-        {
-            body.deserialize_map(BodyVisitor::<T>(PhantomData))
-        }
-
-        #[derive(Deserialize)]
-        #[serde(rename_all = "PascalCase")]
-        struct DummyEnvelope<T>
-        where
-            T: OperationResponse,
-        {
-            #[serde(deserialize_with = "deserialize_body")]
-            body: 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
@@ -453,7 +388,7 @@ pub struct FaultDetail {
 mod tests {
     use serde::Deserialize;
 
-    use crate::{types::sealed::NamedStructure, Error, OperationResponse};
+    use crate::{types::sealed::EnvelopeBodyContents, Error, OperationResponse};
 
     use super::Envelope;
 
@@ -469,7 +404,7 @@ mod tests {
 
         impl OperationResponse for SomeStruct {}
 
-        impl NamedStructure for SomeStruct {
+        impl EnvelopeBodyContents for SomeStruct {
             fn name() -> &'static str {
                 "Foo"
             }
@@ -496,7 +431,7 @@ mod tests {
 
         impl OperationResponse for Foo {}
 
-        impl NamedStructure for Foo {
+        impl EnvelopeBodyContents for Foo {
             fn name() -> &'static str {
                 "Foo"
             }
@@ -549,7 +484,7 @@ mod tests {
 
         impl OperationResponse for Foo {}
 
-        impl NamedStructure for Foo {
+        impl EnvelopeBodyContents for Foo {
             fn name() -> &'static str {
                 "Foo"
             }
diff --git a/src/types/soap/de.rs b/src/types/soap/de.rs
new file mode 100644
index 0000000..00fe5a0
--- /dev/null
+++ b/src/types/soap/de.rs
@@ -0,0 +1,80 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * 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 serde::{de::Visitor, Deserialize, Deserializer};
+
+use crate::OperationResponse;
+
+#[derive(Deserialize)]
+#[serde(rename_all = "PascalCase")]
+pub(super) struct DummyEnvelope<T>
+where
+    T: OperationResponse,
+{
+    #[serde(deserialize_with = "deserialize_body")]
+    pub body: T,
+}
+
+fn deserialize_body<'de, D, T>(body: D) -> Result<T, D::Error>
+where
+    D: Deserializer<'de>,
+    T: OperationResponse,
+{
+    body.deserialize_map(BodyVisitor::<T>(PhantomData))
+}
+
+/// A visitor for custom name-based deserialization of operation responses.
+struct BodyVisitor<T>(PhantomData<T>);
+
+impl<'de, T> Visitor<'de> for BodyVisitor<T>
+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<A>(self, mut map: A) -> Result<Self::Value, A::Error>
+    where
+        A: serde::de::MapAccess<'de>,
+    {
+        match map.next_key::<String>()? {
+            Some(name) => {
+                // We expect the body of the response to contain a single
+                // element with the name of the expected operation response.
+                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.
+                match map.next_key::<String>()? {
+                    Some(name) => {
+                        // The response body contained more than one element,
+                        // which violates our expectations.
+                        Err(serde::de::Error::custom(format_args!(
+                            "unexpected field `{}`",
+                            name
+                        )))
+                    }
+                    None => Ok(value),
+                }
+            }
+            None => Err(serde::de::Error::invalid_type(
+                serde::de::Unexpected::Map,
+                &self,
+            )),
+        }
+    }
+}
diff --git a/src/types/sync_folder_hierarchy.rs b/src/types/sync_folder_hierarchy.rs
index 39ec3b0..d8a9d4d 100644
--- a/src/types/sync_folder_hierarchy.rs
+++ b/src/types/sync_folder_hierarchy.rs
@@ -6,7 +6,7 @@ use serde::Deserialize;
 use xml_struct::XmlSerialize;
 
 use crate::{
-    types::sealed::NamedStructure, BaseFolderId, Folder, FolderId, FolderShape, Operation,
+    types::sealed::EnvelopeBodyContents, BaseFolderId, Folder, FolderId, FolderShape, Operation,
     OperationResponse, ResponseClass, MESSAGES_NS_URI,
 };
 
@@ -25,7 +25,7 @@ impl Operation for SyncFolderHierarchy {
     type Response = SyncFolderHierarchyResponse;
 }
 
-impl NamedStructure for SyncFolderHierarchy {
+impl EnvelopeBodyContents for SyncFolderHierarchy {
     fn name() -> &'static str {
         "SyncFolderHierarchy"
     }
@@ -42,7 +42,7 @@ pub struct SyncFolderHierarchyResponse {
 
 impl OperationResponse for SyncFolderHierarchyResponse {}
 
-impl NamedStructure for SyncFolderHierarchyResponse {
+impl EnvelopeBodyContents for SyncFolderHierarchyResponse {
     fn name() -> &'static str {
         "SyncFolderHierarchyResponse"
     }

From 30be0ca0d257054b491795d0eaf830d2735022a0 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Se=C3=A1n=20de=20B=C3=BArca?= <leftmostcat@gmail.com>
Date: Tue, 16 Apr 2024 11:11:57 -0700
Subject: [PATCH 4/7] cargo fmt

---
 src/types/get_folder.rs | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/src/types/get_folder.rs b/src/types/get_folder.rs
index 300291c..1df1f90 100644
--- a/src/types/get_folder.rs
+++ b/src/types/get_folder.rs
@@ -6,8 +6,8 @@ use serde::Deserialize;
 use xml_struct::XmlSerialize;
 
 use crate::{
-    types::sealed::EnvelopeBodyContents, BaseFolderId, Folder, FolderShape, Operation, OperationResponse,
-    ResponseClass, MESSAGES_NS_URI,
+    types::sealed::EnvelopeBodyContents, BaseFolderId, Folder, FolderShape, Operation,
+    OperationResponse, ResponseClass, MESSAGES_NS_URI,
 };
 
 /// A request to get information on one or more folders.

From 90c7a214e70bd5f12e87688548ba4e35b26fc2f7 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Se=C3=A1n=20de=20B=C3=BArca?= <leftmostcat@gmail.com>
Date: Tue, 16 Apr 2024 14:36:46 -0700
Subject: [PATCH 5/7] More documentation updates

---
 src/types/get_folder.rs            |  7 +++++
 src/types/soap.rs                  |  2 +-
 src/types/sync_folder_hierarchy.rs | 50 ++++++++++++++++++++++++------
 3 files changed, 48 insertions(+), 11 deletions(-)

diff --git a/src/types/get_folder.rs b/src/types/get_folder.rs
index 1df1f90..82d3390 100644
--- a/src/types/get_folder.rs
+++ b/src/types/get_folder.rs
@@ -16,7 +16,11 @@ use crate::{
 #[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>,
 }
 
@@ -62,8 +66,11 @@ pub struct ResponseMessages {
 #[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,
 }
 
diff --git a/src/types/soap.rs b/src/types/soap.rs
index e30c73d..51e49e0 100644
--- a/src/types/soap.rs
+++ b/src/types/soap.rs
@@ -14,7 +14,7 @@ use crate::{
 mod de;
 use self::de::DummyEnvelope;
 
-/// 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)]
diff --git a/src/types/sync_folder_hierarchy.rs b/src/types/sync_folder_hierarchy.rs
index d8a9d4d..e43f80d 100644
--- a/src/types/sync_folder_hierarchy.rs
+++ b/src/types/sync_folder_hierarchy.rs
@@ -10,14 +10,26 @@ use crate::{
     OperationResponse, ResponseClass, MESSAGES_NS_URI,
 };
 
-/// The request for update regarding the folder hierarchy in a mailbox.
+/// A request for a list of folders which have been created, updated, or deleted
+/// server-side.
 ///
 /// See <https://learn.microsoft.com/en-us/exchange/client-developer/web-service-reference/syncfolderhierarchy>
 #[derive(Debug, XmlSerialize)]
 #[xml_struct(default_ns = MESSAGES_NS_URI)]
 pub struct SyncFolderHierarchy {
+    /// A description of the information to be included in the response for each
+    /// changed folder.
     pub folder_shape: FolderShape,
+
+    /// The ID of the folder to sync.
     pub sync_folder_id: Option<BaseFolderId>,
+
+    /// The synchronization state after which to list changes.
+    ///
+    /// If `None`, the response will include `Create` changes for each folder
+    /// which is a descendant of the requested folder.
+    ///
+    /// See <https://learn.microsoft.com/en-us/exchange/client-developer/web-service-reference/syncstate-ex15websvcsotherref>
     pub sync_state: Option<String>,
 }
 
@@ -31,7 +43,7 @@ impl EnvelopeBodyContents for SyncFolderHierarchy {
     }
 }
 
-/// The response to a SyncFolderHierarchy request.
+/// A response to a [`SyncFolderHierarchy`] request.
 ///
 /// See <https://learn.microsoft.com/en-us/exchange/client-developer/web-service-reference/syncfolderhierarchyresponse>
 #[derive(Debug, Deserialize)]
@@ -48,27 +60,40 @@ impl EnvelopeBodyContents for SyncFolderHierarchyResponse {
     }
 }
 
-/// A collection of response messages from a SyncFolderHierarchy response.
+/// 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 sync_folder_hierarchy_response_message: Vec<SyncFolderHierarchyResponseMessage>,
 }
 
-/// A message in a SyncFolderHierarchy response.
+/// A response to a request for an individual folder within a [`SyncFolderHierarchy`] operation.
 ///
 /// See <https://learn.microsoft.com/en-us/exchange/client-developer/web-service-reference/syncfolderhierarchyresponsemessage>
 #[derive(Debug, Deserialize)]
 #[serde(rename_all = "PascalCase")]
 pub struct SyncFolderHierarchyResponseMessage {
+    /// The success value of the corresponding request.
     #[serde(rename = "@ResponseClass")]
     pub response_class: ResponseClass,
+
+    /// An identifier for the synchronization state following application of the
+    /// changes included in this response.
     pub sync_state: String,
+
+    /// Whether all relevant folder changes have been synchronized following
+    /// this response.
     pub includes_last_folder_in_range: bool,
+
+    /// The collection of changes between the prior synchronization state and
+    /// the one represented by this response.
     pub changes: Changes,
 }
 
-/// The changes that happened since the last folder hierachy sync.
+/// A sequentially-ordered collection of folder creations, updates, and
+/// deletions.
 ///
 /// See <https://learn.microsoft.com/en-us/exchange/client-developer/web-service-reference/changes-hierarchy>
 #[derive(Debug, Deserialize)]
@@ -77,29 +102,34 @@ pub struct Changes {
     pub inner: Vec<Change>,
 }
 
-/// A single change described in a SyncFolderHierarchy response message.
+/// A server-side change to a folder.
 ///
 /// See <https://learn.microsoft.com/en-us/exchange/client-developer/web-service-reference/changes-hierarchy>
 #[derive(Debug, Deserialize)]
 pub enum Change {
-    /// A folder to create.
+    /// A creation of a folder.
     ///
     /// See <https://learn.microsoft.com/en-us/exchange/client-developer/web-service-reference/create-foldersync>
     Create {
+        /// The state of the folder upon creation.
         #[serde(rename = "$value")]
         folder: Folder,
     },
 
-    /// A folder to update.
+    /// An update to a folder.
     ///
     /// See <https://learn.microsoft.com/en-us/exchange/client-developer/web-service-reference/update-foldersync>
     Update {
+        /// The updated state of the folder.
         #[serde(rename = "$value")]
         folder: Folder,
     },
 
-    /// A folder to delete.
+    /// A deletion of a folder.
     ///
     /// See <https://learn.microsoft.com/en-us/exchange/client-developer/web-service-reference/delete-foldersync>
-    Delete(FolderId),
+    Delete(
+        /// The EWS ID for the deleted folder.
+        FolderId
+    ),
 }

From 5f65e7d37d7ee996fbe383c680e753db7467c87a Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Se=C3=A1n=20de=20B=C3=BArca?= <leftmostcat@gmail.com>
Date: Tue, 16 Apr 2024 14:43:32 -0700
Subject: [PATCH 6/7] cargo fmt

---
 src/types/sync_folder_hierarchy.rs | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/src/types/sync_folder_hierarchy.rs b/src/types/sync_folder_hierarchy.rs
index e43f80d..f986f24 100644
--- a/src/types/sync_folder_hierarchy.rs
+++ b/src/types/sync_folder_hierarchy.rs
@@ -130,6 +130,6 @@ pub enum Change {
     /// See <https://learn.microsoft.com/en-us/exchange/client-developer/web-service-reference/delete-foldersync>
     Delete(
         /// The EWS ID for the deleted folder.
-        FolderId
+        FolderId,
     ),
 }

From c1dbd49aefe1744941095c33ce0596757060cc61 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Se=C3=A1n=20de=20B=C3=BArca?= <leftmostcat@gmail.com>
Date: Wed, 17 Apr 2024 10:23:41 -0700
Subject: [PATCH 7/7] Improve naming and documentation

---
 src/types/soap.rs    |  4 ++--
 src/types/soap/de.rs | 12 +++++++++---
 2 files changed, 11 insertions(+), 5 deletions(-)

diff --git a/src/types/soap.rs b/src/types/soap.rs
index 51e49e0..ed6e2fe 100644
--- a/src/types/soap.rs
+++ b/src/types/soap.rs
@@ -12,7 +12,7 @@ use crate::{
 };
 
 mod de;
-use self::de::DummyEnvelope;
+use self::de::DeserializeEnvelope;
 
 /// A SOAP envelope containing the body of an EWS operation or response.
 ///
@@ -77,7 +77,7 @@ 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,
diff --git a/src/types/soap/de.rs b/src/types/soap/de.rs
index 00fe5a0..8ef577c 100644
--- a/src/types/soap/de.rs
+++ b/src/types/soap/de.rs
@@ -8,9 +8,15 @@ use serde::{de::Visitor, Deserialize, Deserializer};
 
 use crate::OperationResponse;
 
+/// A helper for deserialization of SOAP envelopes.
+///
+/// This struct is declared separately from the more general [`Envelope`] type
+/// so that the latter can be used with types that are write-only.
+///
+/// [`Envelope`]: super::Envelope
 #[derive(Deserialize)]
 #[serde(rename_all = "PascalCase")]
-pub(super) struct DummyEnvelope<T>
+pub(super) struct DeserializeEnvelope<T>
 where
     T: OperationResponse,
 {
@@ -50,7 +56,7 @@ where
                 let expected = T::name();
                 if name.as_str() != expected {
                     return Err(serde::de::Error::custom(format_args!(
-                        "unknown field `{}`, expected {}",
+                        "unknown element `{}`, expected {}",
                         name, expected
                     )));
                 }
@@ -64,7 +70,7 @@ where
                         // The response body contained more than one element,
                         // which violates our expectations.
                         Err(serde::de::Error::custom(format_args!(
-                            "unexpected field `{}`",
+                            "unexpected element `{}`",
                             name
                         )))
                     }