aboutsummaryrefslogtreecommitdiff
path: root/src/client.rs
blob: 8aeb0996b80445e7db4ad809c53276c685ab532f (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
use crate::{call, response};
use crate::{Error, Result};

use std::collections::BTreeMap;
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 From<Endpoint> 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<Endpoint> 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<ClientRef>,
}

impl Client {
    /// Initialises a session and returns a `Client` if successful.
    pub fn login(ep: Endpoint, user: String, pass: String) -> Result<Client> {
        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)
    }

    /// 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: impl call::Call) -> Result<response::Response> {
        let expected = call.expected();

        let transport = self.inner.http.post::<Url>(self.inner.endpoint.into());

        let binding = call.method_name();
        let request = xmlrpc::Request::new(&binding).arg(call);

        let raw = request.call(transport)?;
        match raw {
            xmlrpc::Value::Struct(map) => {
                let code = map
                    .get("code")
                    .ok_or_else(|| Error::Inexistent("code".into()))?;

                match code {
                    xmlrpc::Value::Int(code) => {
                        if expected.contains(code) {
                            let default = &xmlrpc::Value::Struct(BTreeMap::new());
                            let data = map.get("resData").unwrap_or(default);

                            match data {
                                xmlrpc::Value::Struct(response) => Ok(response::Response {
                                    status: *code,
                                    data: response.clone(),
                                }),
                                _ => Err(Error::Type(
                                    "resData".into(),
                                    "Struct".into(),
                                    data.clone(),
                                )),
                            }
                        } else {
                            Err(Error::BadStatus(expected, *code))
                        }
                    }
                    _ => Err(Error::Type("code".into(), "Int".into(), code.clone())),
                }
            }
            _ => Err(Error::BadResponse(raw.clone())),
        }
    }
}

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,
}