diff --git a/src/client.rs b/src/client.rs index 5b9480a..dfc8967 100644 --- a/src/client.rs +++ b/src/client.rs @@ -1,3 +1,4 @@ +use std::marker::PhantomData; use std::time::Duration; #[cfg(feature = "__rustls")] @@ -10,7 +11,7 @@ use crate::connection::EppConnection; use crate::error::Error; use crate::hello::{Greeting, Hello}; use crate::request::{Command, CommandWrapper, Extension, Transaction}; -use crate::response::{Response, ResponseStatus}; +use crate::response::{ConnectionExtensionResponse, Response, ResponseStatus}; use crate::xml; /// An `EppClient` provides an interface to sending EPP requests to a registry @@ -64,12 +65,13 @@ use crate::xml; /// Domain: eppdev.com, Available: 1 /// Domain: eppdev.net, Available: 1 /// ``` -pub struct EppClient { +pub struct EppClient { connection: EppConnection, + phantom_type: PhantomData, } #[cfg(feature = "__rustls")] -impl EppClient { +impl EppClient { /// Connect to the specified `addr` and `hostname` over TLS /// /// The `registry` is used as a name in internal logging; `host` provides the host name @@ -97,11 +99,12 @@ impl EppClient { } } -impl EppClient { +impl EppClient { /// Create an `EppClient` from an already established connection pub async fn new(connector: C, registry: String, timeout: Duration) -> Result { Ok(Self { connection: EppConnection::new(connector, registry, timeout).await?, + phantom_type: PhantomData, }) } @@ -116,14 +119,14 @@ impl EppClient { xml::deserialize::(&response) } - pub async fn transact<'c, 'e, Cmd, Ext>( + pub async fn transact<'c, 'e, Cmd, CmdExt>( &mut self, - data: impl Into>, + data: impl Into>, id: &str, - ) -> Result, Error> + ) -> Result, Error> where - Cmd: Transaction + Command + 'c, - Ext: Extension + 'e, + Cmd: Transaction + Command + 'c, + CmdExt: Extension + 'e, { let data = data.into(); let document = CommandWrapper::new(data.command, data.extension, id); @@ -133,13 +136,15 @@ impl EppClient { let response = self.connection.transact(&xml)?.await?; debug!("{}: response: {}", self.connection.registry, &response); - let rsp = match xml::deserialize::>(&response) { - Ok(rsp) => rsp, - Err(e) => { - error!(%response, "failed to deserialize response for transaction: {e}"); - return Err(e); - } - }; + let rsp = + match xml::deserialize::>(&response) + { + Ok(rsp) => rsp, + Err(e) => { + error!(%response, "failed to deserialize response for transaction: {e}"); + return Err(e); + } + }; if rsp.result.code.is_success() { return Ok(rsp); @@ -179,9 +184,9 @@ impl EppClient { } #[derive(Debug)] -pub struct RequestData<'c, 'e, C, E> { - pub(crate) command: &'c C, - pub(crate) extension: Option<&'e E>, +pub struct RequestData<'c, 'e, Command, CommandExt> { + pub(crate) command: &'c Command, + pub(crate) extension: Option<&'e CommandExt>, } impl<'c, C: Command> From<&'c C> for RequestData<'c, 'static, C, NoExtension> { @@ -193,8 +198,10 @@ impl<'c, C: Command> From<&'c C> for RequestData<'c, 'static, C, NoExtension> { } } -impl<'c, 'e, C: Command, E: Extension> From<(&'c C, &'e E)> for RequestData<'c, 'e, C, E> { - fn from((command, extension): (&'c C, &'e E)) -> Self { +impl<'c, 'e, C: Command, CommandExt: Extension> From<(&'c C, &'e CommandExt)> + for RequestData<'c, 'e, C, CommandExt> +{ + fn from((command, extension): (&'c C, &'e CommandExt)) -> Self { Self { command, extension: Some(extension), @@ -203,14 +210,14 @@ impl<'c, 'e, C: Command, E: Extension> From<(&'c C, &'e E)> for RequestData<'c, } // Manual impl because this does not depend on whether `C` and `E` are `Clone` -impl Clone for RequestData<'_, '_, C, E> { +impl Clone for RequestData<'_, '_, Command, CommandExt> { fn clone(&self) -> Self { *self } } // Manual impl because this does not depend on whether `C` and `E` are `Copy` -impl Copy for RequestData<'_, '_, C, E> {} +impl Copy for RequestData<'_, '_, Command, CommandExt> {} #[cfg(feature = "__rustls")] pub use rustls_connector::RustlsConnector; diff --git a/src/extensions/change_poll.rs b/src/extensions/change_poll.rs index a266a2e..649c452 100644 --- a/src/extensions/change_poll.rs +++ b/src/extensions/change_poll.rs @@ -3,40 +3,31 @@ //! As described in RFC8590: [Change Poll Extension for the Extensible Provisioning Protocol (EPP)](https://www.rfc-editor.org/rfc/rfc8590.html). //! Tests cases in `tests/resources/response/extensions/changepoll`` are taken from the RFC. -use std::borrow::Cow; - use instant_xml::{Error, FromXml, ToXml}; -use crate::{ - poll::Poll, - request::{Extension, Transaction}, -}; +use crate::response::ConnectionExtensionResponse; pub const XMLNS: &str = "urn:ietf:params:xml:ns:changePoll-1.0"; -impl Transaction> for Poll {} - -impl Extension for ChangePoll<'_> { - type Response = ChangePoll<'static>; -} +impl ConnectionExtensionResponse for ChangePoll {} /// Type for EPP XML `` extension /// /// Attributes associated with the change #[derive(Debug, FromXml, ToXml)] #[xml(rename = "changeData", ns(XMLNS))] -pub struct ChangePoll<'a> { +pub struct ChangePoll { /// Transform operation executed on the object - pub operation: Operation<'a>, + pub operation: Operation, /// Date and time when the operation was executed - pub date: Cow<'a, str>, + pub date: String, /// Server transaction identifier of the operation #[xml(rename = "svTRID")] - pub server_tr_id: Cow<'a, str>, + pub server_tr_id: String, /// Who executed the operation - pub who: Cow<'a, str>, + pub who: String, /// Case identifier associated with the operation - pub case_id: Option>, + pub case_id: Option, /// Reason for executing the operation pub reason: Option, /// Enumerated state of the object in the poll message @@ -46,7 +37,7 @@ pub struct ChangePoll<'a> { state: Option, } -impl ChangePoll<'_> { +impl ChangePoll { /// State reflects if the `infData` describes the object before or after the operation pub fn state(&self) -> State { self.state.unwrap_or_default() @@ -58,16 +49,16 @@ impl ChangePoll<'_> { // to make this struct more ergonomic. #[derive(Debug, FromXml, ToXml)] #[xml(rename = "operation", ns(XMLNS))] -pub struct Operation<'a> { +pub struct Operation { /// Custom value for`OperationKind::Custom` #[xml(attribute, rename = "op")] - op: Option>, + op: Option, /// The operation #[xml(direct)] kind: OperationType, } -impl Operation<'_> { +impl Operation { pub fn kind(&self) -> Result { Ok(match self.kind { OperationType::Create => OperationKind::Create, @@ -128,16 +119,16 @@ enum OperationType { // to make this struct more ergonomic. #[derive(Debug, FromXml, ToXml)] #[xml(rename = "caseId", ns(XMLNS))] -pub struct CaseIdentifier<'a> { +pub struct CaseIdentifier { #[xml(attribute, rename = "type")] id_type: CaseIdentifierType, #[xml(attribute)] - name: Option>, + name: Option, #[xml(direct)] - pub id: Cow<'a, str>, + pub id: String, } -impl CaseIdentifier<'_> { +impl CaseIdentifier { pub fn kind(&self) -> Result { Ok(match self.id_type { CaseIdentifierType::Udrp => CaseIdentifierKind::Udrp, @@ -204,13 +195,14 @@ pub enum State { #[cfg(test)] mod tests { use super::*; + use crate::common::NoExtension; use crate::poll::Poll; use crate::response::ResultCode; use crate::tests::{response_from_file_with_ext, CLTRID, SVTRID}; #[test] fn urs_lock_before() { - let object = response_from_file_with_ext::( + let object = response_from_file_with_ext::( "response/extensions/change_poll/urs_lock_before.xml", ); @@ -223,29 +215,18 @@ mod tests { "Command completed successfully; ack to dequeue" ); - assert_eq!(object.extension().unwrap().state.unwrap(), State::Before); - assert_eq!( - object.extension().unwrap().operation.kind().unwrap(), - OperationKind::Update - ); - assert_eq!(object.extension().unwrap().date, "2013-10-22T14:25:57.0Z"); - assert_eq!(object.extension().unwrap().server_tr_id, "12345-XYZ"); - assert_eq!(object.extension().unwrap().who, "URS Admin"); + let ext = &object.connection_extension().unwrap(); + + assert_eq!(ext.state.unwrap(), State::Before); + assert_eq!(ext.operation.kind().unwrap(), OperationKind::Update); + assert_eq!(ext.date, "2013-10-22T14:25:57.0Z"); + assert_eq!(ext.server_tr_id, "12345-XYZ"); + assert_eq!(ext.who, "URS Admin"); assert_eq!( - object - .extension() - .unwrap() - .case_id - .as_ref() - .unwrap() - .kind() - .unwrap(), + ext.case_id.as_ref().unwrap().kind().unwrap(), CaseIdentifierKind::Urs ); - assert_eq!( - object.extension().unwrap().reason.as_ref().unwrap().inner, - "URS Lock" - ); + assert_eq!(ext.reason.as_ref().unwrap().inner, "URS Lock"); assert_eq!(object.tr_ids.client_tr_id.unwrap(), CLTRID); assert_eq!(object.tr_ids.server_tr_id, SVTRID); @@ -253,7 +234,7 @@ mod tests { #[test] fn urs_lock_after() { - let object = response_from_file_with_ext::( + let object = response_from_file_with_ext::( "response/extensions/change_poll/urs_lock_after.xml", ); @@ -265,7 +246,10 @@ mod tests { object.result.message, "Command completed successfully; ack to dequeue" ); - assert_eq!(object.extension().unwrap().state.unwrap(), State::After); + + let ext = &object.connection_extension().unwrap(); + + assert_eq!(ext.state.unwrap(), State::After); assert_eq!(object.tr_ids.client_tr_id.unwrap(), CLTRID); assert_eq!(object.tr_ids.server_tr_id, SVTRID); @@ -273,7 +257,7 @@ mod tests { #[test] fn custom_sync_after() { - let object = response_from_file_with_ext::( + let object = response_from_file_with_ext::( "response/extensions/change_poll/custom_sync_after.xml", ); @@ -286,15 +270,11 @@ mod tests { "Command completed successfully; ack to dequeue" ); - assert_eq!( - object.extension().unwrap().operation.kind().unwrap(), - OperationKind::Custom("sync") - ); - assert_eq!(object.extension().unwrap().who, "CSR"); - assert_eq!( - object.extension().unwrap().reason.as_ref().unwrap().inner, - "Customer sync request" - ); + let ext = &object.connection_extension().unwrap(); + + assert_eq!(ext.operation.kind().unwrap(), OperationKind::Custom("sync")); + assert_eq!(ext.who, "CSR"); + assert_eq!(ext.reason.as_ref().unwrap().inner, "Customer sync request"); assert_eq!(object.tr_ids.client_tr_id.unwrap(), CLTRID); assert_eq!(object.tr_ids.server_tr_id, SVTRID); @@ -302,7 +282,7 @@ mod tests { #[test] fn delete_before() { - let object = response_from_file_with_ext::( + let object = response_from_file_with_ext::( "response/extensions/change_poll/delete_before.xml", ); @@ -321,7 +301,7 @@ mod tests { #[test] fn autopurge_before() { - let object = response_from_file_with_ext::( + let object = response_from_file_with_ext::( "response/extensions/change_poll/autopurge_before.xml", ); @@ -340,7 +320,7 @@ mod tests { #[test] fn update_after() { - let object = response_from_file_with_ext::( + let object = response_from_file_with_ext::( "response/extensions/change_poll/update_after.xml", ); diff --git a/src/extensions/namestore.rs b/src/extensions/namestore.rs index ac7825d..021c0bb 100644 --- a/src/extensions/namestore.rs +++ b/src/extensions/namestore.rs @@ -75,6 +75,7 @@ pub struct NameStore<'a> { #[cfg(test)] mod tests { use super::NameStore; + use crate::common::NoExtension; use crate::domain::check::DomainCheck; use crate::tests::{assert_serialized, response_from_file_with_ext}; @@ -94,10 +95,10 @@ mod tests { #[test] fn response() { - let object = response_from_file_with_ext::( + let object = response_from_file_with_ext::( "response/extensions/namestore.xml", ); - let ext = object.extension().unwrap(); + let ext = &object.command_extension().unwrap(); assert_eq!(ext.subproduct, "com"); } } diff --git a/src/extensions/rgp/request.rs b/src/extensions/rgp/request.rs index c87bb09..d93900d 100644 --- a/src/extensions/rgp/request.rs +++ b/src/extensions/rgp/request.rs @@ -69,6 +69,7 @@ pub enum RgpRequestResponse { #[cfg(test)] mod tests { use super::{RgpRestoreRequest, Update}; + use crate::common::NoExtension; use crate::domain::info::DomainInfo; use crate::domain::update::{DomainChangeInfo, DomainUpdate}; use crate::extensions::rgp::request::RgpRequestResponse; @@ -99,15 +100,16 @@ mod tests { #[test] fn request_response() { - let object = response_from_file_with_ext::>( - "response/extensions/rgp_restore.xml", - ); - let ext = object.extension.unwrap(); + let object = + response_from_file_with_ext::, NoExtension>( + "response/extensions/rgp_restore.xml", + ); + let ext = object.command_extension().unwrap(); assert_eq!(object.result.code, ResultCode::CommandCompletedSuccessfully); assert_eq!(object.result.message, SUCCESS_MSG); - let data = match ext.data { + let data = match ext { RgpRequestResponse::Update(data) => data, _ => panic!("Unexpected response type"), }; @@ -118,12 +120,13 @@ mod tests { #[test] fn domain_info_request_response() { - let object = response_from_file_with_ext::>( - "response/extensions/domain_info_rgp.xml", - ); - let ext = object.extension.unwrap(); + let object = + response_from_file_with_ext::, NoExtension>( + "response/extensions/domain_info_rgp.xml", + ); + let ext = object.command_extension().unwrap(); - let data = match ext.data { + let data = match ext { RgpRequestResponse::Info(data) => data, _ => panic!("Unexpected response type"), }; diff --git a/src/response.rs b/src/response.rs index e1e5d8b..7424295 100644 --- a/src/response.rs +++ b/src/response.rs @@ -3,9 +3,9 @@ use std::fmt::Debug; use chrono::{DateTime, Utc}; -use instant_xml::{FromXml, Kind}; +use instant_xml::{Deserializer, FromXml, FromXmlOwned, Id, Kind}; -use crate::common::EPP_XMLNS; +use crate::common::{NoExtension, EPP_XMLNS}; /// Type corresponding to the `` tag an EPP response XML #[derive(Debug, Eq, FromXml, PartialEq)] @@ -238,7 +238,7 @@ pub struct Message { /// Type corresponding to the `` tag in an EPP response XML /// containing an `` tag #[xml(rename = "response", ns(EPP_XMLNS))] -pub struct Response { +pub struct Response { /// Data under the `` tag pub result: EppResult, /// Data under the `` tag @@ -247,7 +247,7 @@ pub struct Response { /// Data under the `` tag pub res_data: Option>, /// Data under the `` tag - pub extension: Option>, + pub extension: Option>, /// Data under the `` tag pub tr_ids: ResponseTRID, } @@ -276,7 +276,7 @@ pub struct ResponseStatus { pub tr_ids: ResponseTRID, } -impl Response { +impl Response { /// Returns the data under the corresponding `` from the EPP XML pub fn res_data(&self) -> Option<&T> { match &self.res_data { @@ -285,9 +285,16 @@ impl Response { } } - pub fn extension(&self) -> Option<&E> { + pub fn command_extension(&self) -> Option<&CmdExt> { match &self.extension { - Some(extension) => Some(&extension.data), + Some(extension) => extension.data.command.as_ref(), + None => None, + } + } + + pub fn connection_extension(&self) -> Option<&ConnExt> { + match &self.extension { + Some(extension) => extension.data.connection.as_ref(), None => None, } } @@ -303,8 +310,118 @@ impl Response { #[derive(Debug, Eq, FromXml, PartialEq)] #[xml(rename = "extension", ns(EPP_XMLNS))] -pub struct Extension { - pub data: E, +pub struct Extension { + pub data: CombinedExtensions, +} + +/// Types implementing this can be used as ConnectionExtensions. +/// +/// Their type will be assumed to be in the response's extension element. +pub trait ConnectionExtensionResponse: FromXmlOwned + Debug {} + +impl ConnectionExtensionResponse for NoExtension {} + +impl ConnectionExtensionResponse for Option {} + +/// Combines connection extensions and command extensions +/// +/// Some extensions are sent by the server no matter if an extension was defined for the command. +#[derive(Debug, Eq, PartialEq)] +pub struct CombinedExtensions { + pub command: Option, + pub connection: Option, +} + +// // This manual impl is needed because instant-xml is not able to create the correct derived impl. +// // for this transparent type. It fails to set the correct bounds for struct __CombinedExtensionsAccumulator. +impl<'xml, CmdExt: FromXml<'xml>, ConnExt: FromXml<'xml>> FromXml<'xml> + for CombinedExtensions +{ + #[inline] + fn matches(id: Id<'_>, _: Option>) -> bool { + >::matches(id, None) + || >::matches(id, None) + } + fn deserialize<'cx>( + into: &mut Self::Accumulator, + _: &'static str, + deserializer: &mut Deserializer<'cx, 'xml>, + ) -> Result<(), instant_xml::Error> { + use instant_xml::Kind; + let current = deserializer.parent(); + if >::matches(current, None) { + match ::KIND { + Kind::Element => { + >::deserialize( + &mut into.command, + "CombinedExtensions::command", + deserializer, + )?; + } + Kind::Scalar => { + >::deserialize( + &mut into.command, + "CombinedExtensions::command", + deserializer, + )?; + deserializer.ignore()?; + } + } + } else if >::matches(current, None) { + match ::KIND { + Kind::Element => { + >::deserialize( + &mut into.connection, + "CombinedExtensions::connection", + deserializer, + )?; + } + Kind::Scalar => { + >::deserialize( + &mut into.connection, + "CombinedExtensions::connection", + deserializer, + )?; + deserializer.ignore()?; + } + } + } + Ok(()) + } + type Accumulator = __CombinedExtensionsAccumulator<'xml, CmdExt, ConnExt>; + const KIND: instant_xml::Kind = instant_xml::Kind::Element; +} + +pub struct __CombinedExtensionsAccumulator<'xml, CommandExt: FromXml<'xml>, ConnExt: FromXml<'xml>> +{ + command: as FromXml<'xml>>::Accumulator, + connection: as FromXml<'xml>>::Accumulator, +} + +impl<'xml, CommandExt: FromXml<'xml>, ConnExt: FromXml<'xml>> + instant_xml::Accumulate> + for __CombinedExtensionsAccumulator<'xml, CommandExt, ConnExt> +{ + fn try_done( + self, + _: &'static str, + ) -> Result, instant_xml::Error> { + Ok(CombinedExtensions { + command: self.command.try_done("CombinedExtensions::command")?, + connection: self.connection.try_done("CombinedExtensions::connection")?, + }) + } +} + +impl<'xml, CommandExt: FromXml<'xml>, ConnExt: instant_xml::FromXml<'xml>> Default + for __CombinedExtensionsAccumulator<'xml, CommandExt, ConnExt> +{ + fn default() -> Self { + Self { + command: Default::default(), + connection: Default::default(), + } + } } #[cfg(test)] diff --git a/src/tests/mod.rs b/src/tests/mod.rs index 79e1806..b7b9caa 100644 --- a/src/tests/mod.rs +++ b/src/tests/mod.rs @@ -9,7 +9,7 @@ use crate::{ client::RequestData, common::NoExtension, request::{Command, CommandWrapper, Extension, Transaction}, - response::Response, + response::{ConnectionExtensionResponse, Response}, xml, }; @@ -53,23 +53,24 @@ pub(crate) fn assert_serialized<'c, 'e, Cmd, Ext>( pub(crate) fn response_from_file<'c, Cmd>( path: &str, -) -> Response::Response> +) -> Response::Response, NoExtension> where Cmd: Transaction + Command + 'c, { - response_from_file_with_ext::(path) + response_from_file_with_ext::(path) } -pub(crate) fn response_from_file_with_ext( +pub(crate) fn response_from_file_with_ext( path: &str, -) -> Response +) -> Response where Cmd: Transaction + Command, - Ext: Extension, + CmdExt: Extension, + ConnExt: ConnectionExtensionResponse, { let xml = get_xml(path).unwrap(); dbg!(&xml); - let rsp = xml::deserialize::>(&xml).unwrap(); + let rsp = xml::deserialize::>(&xml).unwrap(); assert!(rsp.result.code.is_success()); rsp } diff --git a/tests/basic.rs b/tests/basic.rs index 1dd64cd..689d39d 100644 --- a/tests/basic.rs +++ b/tests/basic.rs @@ -4,11 +4,14 @@ use std::str; use std::time::Duration; use async_trait::async_trait; +use instant_epp::extensions::change_poll::ChangePoll; +use instant_epp::poll::{Poll, PollData}; use regex::Regex; use tokio::time::timeout; use tokio_test::io::Builder; use instant_epp::client::{Connector, EppClient}; +use instant_epp::common::NoExtension; use instant_epp::domain::{DomainCheck, DomainContact, DomainCreate, Period, PeriodLength}; use instant_epp::login::Login; use instant_epp::response::ResultCode; @@ -100,9 +103,10 @@ async fn client() { } } - let mut client = EppClient::new(FakeConnector, "test".into(), Duration::from_secs(5)) - .await - .unwrap(); + let mut client = + EppClient::<_, NoExtension>::new(FakeConnector, "test".into(), Duration::from_secs(5)) + .await + .unwrap(); assert_eq!(client.xml_greeting(), xml("response/greeting.xml")); let rsp = client @@ -176,9 +180,10 @@ async fn dropped() { } } - let mut client = EppClient::new(FakeConnector, "test".into(), Duration::from_secs(5)) - .await - .unwrap(); + let mut client = + EppClient::<_, NoExtension>::new(FakeConnector, "test".into(), Duration::from_secs(5)) + .await + .unwrap(); assert_eq!(client.xml_greeting(), xml("response/greeting.xml")); let rsp = client @@ -242,3 +247,62 @@ async fn dropped() { let rsp = client.transact(&create, CLTRID).await.unwrap(); assert_eq!(rsp.result.code, ResultCode::CommandCompletedSuccessfully); } + +#[tokio::test] +async fn poll_with_extensions() { + let _guard = log_to_stdout(); + + struct FakeConnector; + + #[async_trait] + impl Connector for FakeConnector { + type Connection = tokio_test::io::Mock; + + async fn connect(&self, _: Duration) -> Result { + Ok(build_stream(&[ + "response/greeting.xml", + "request/login.xml", + "response/login.xml", + "request/poll/poll.xml", + "response/extensions/change_poll/urs_lock_after.xml", + ]) + .build()) + } + } + + let mut client = + EppClient::<_, ChangePoll>::new(FakeConnector, "test".into(), Duration::from_secs(5)) + .await + .unwrap(); + + assert_eq!(client.xml_greeting(), xml("response/greeting.xml")); + let rsp = client + .transact( + &Login::new( + "username", + "password", + Some("new-password"), + Some(&["http://schema.ispapi.net/epp/xml/keyvalue-1.0"]), + ), + CLTRID, + ) + .await + .unwrap(); + + assert_eq!(rsp.result.code, ResultCode::CommandCompletedSuccessfully); + + let rsp = client.transact(&Poll, CLTRID).await.unwrap(); + assert_eq!( + rsp.result.code, + ResultCode::CommandCompletedSuccessfullyAckToDequeue + ); + + let data = rsp.res_data().unwrap(); + let data = match data { + PollData::DomainInfo(info_data) => info_data, + _ => panic!("expected domain:infData"), + }; + assert_eq!(data.name, "domain.example"); + let ext = rsp.connection_extension().unwrap(); + assert_eq!(ext.case_id.as_ref().unwrap().id, "urs123"); +}