diff --git a/src/client.rs b/src/client.rs index ac52fc3..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, }) } @@ -120,7 +123,7 @@ impl EppClient { &mut self, data: impl Into>, id: &str, - ) -> Result, Error> + ) -> Result, Error> where Cmd: Transaction + Command + 'c, CmdExt: Extension + 'e, @@ -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); diff --git a/src/extensions/change_poll.rs b/src/extensions/change_poll.rs index a266a2e..efa0077 100644 --- a/src/extensions/change_poll.rs +++ b/src/extensions/change_poll.rs @@ -7,18 +7,11 @@ 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 /// @@ -204,13 +197,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 +217,18 @@ mod tests { "Command completed successfully; ack to dequeue" ); - assert_eq!(object.extension().unwrap().state.unwrap(), State::Before); + 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().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"); - 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 +236,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 +248,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 +259,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 +272,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 +284,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 +303,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 +322,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..d7b19be 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,116 @@ 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 {} + +/// 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"); +}