use crate::call; use crate::response::{self, ResponseData}; use crate::{Error, Result}; use std::net::SocketAddr; use std::sync::Arc; use reqwest::{blocking, Url}; /// The INWX environment to use. The Sandbox is good for testing /// or debugging purposes. #[derive(Clone, Copy, Debug)] pub enum Endpoint { Production, Sandbox, } impl Endpoint { pub fn domain(&self) -> &'static str { match self { Self::Production => "api.domrobot.com", Self::Sandbox => "api.ote.domrobot.com", } } } impl From for &str { fn from(endpoint: Endpoint) -> &'static str { match endpoint { Endpoint::Production => "https://api.domrobot.com/xmlrpc/", Endpoint::Sandbox => "https://api.ote.domrobot.com/xmlrpc/", } } } impl From for String { fn from(endpoint: Endpoint) -> String { match endpoint { Endpoint::Production => String::from("https://api.domrobot.com/xmlrpc/"), Endpoint::Sandbox => String::from("https://api.ote.domrobot.com/xmlrpc/"), } } } impl From for Url { fn from(endpoint: Endpoint) -> Self { String::from(endpoint).parse().unwrap() } } /// A synchronous client to make API calls with. /// You do **not** need to wrap it in an `Arc` or `Rc` /// because it already uses an `Arc` internally. /// [`Rc`]: std::rc::Rc pub struct Client { inner: Arc, } impl Client { /// Initialises a session and returns a `Client` if successful. pub fn login(ep: Endpoint, user: String, pass: String) -> Result { let client = Client { inner: Arc::new(ClientRef { http: blocking::Client::builder().cookie_store(true).build()?, endpoint: ep, }), }; client.call(call::account::Login { user, pass, case_insensitive: false, })?; Ok(client) } /// Initialises a session using the provided `SocketAddr` /// and returns a `Client` if successful. /// This is useful if your system can't resolve DNS hostnames. pub fn login_addr( ep: Endpoint, addr: SocketAddr, user: String, pass: String, ) -> Result { let client = Client { inner: Arc::new(ClientRef { http: blocking::Client::builder() .cookie_store(true) .resolve(ep.domain(), addr) .build()?, endpoint: ep, }), }; client.call(call::account::Login { user, pass, case_insensitive: false, })?; Ok(client) } /// Issues a `Call` and returns a `Response` /// if successful and if the status code /// matches one of the expected status codes. pub fn call(&self, call: T) -> Result where T: call::Call + call::Response, U: response::Response + Clone + serde::de::DeserializeOwned, { let expected = call.expected(); let xml = serde_xmlrpc::request_to_str(&call.method_name(), vec![call])?; let raw_response = self .inner .http .post::(self.inner.endpoint.into()) .body(xml) .send()? .text()?; let resp: ResponseData = serde_xmlrpc::response_from_str(&raw_response)?; if !expected.contains(&resp.status) { return Err(Error::BadStatus(expected, resp.status)); } Ok(U::unwrap(resp.params)) } } impl Drop for Client { fn drop(&mut self) { // Ignore the result. Failed logout doesn't really matter. self.call(call::account::Logout).ok(); } } // The underlying data of a `Client`. struct ClientRef { http: blocking::Client, endpoint: Endpoint, }