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
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
use serde::{de::DeserializeOwned, ser::SerializeStruct, Deserialize, Serialize, Serializer};
use serde_json::{to_string, to_string_pretty, to_value, Map, Value};
use std::fmt::Debug;

mod error;
pub use error::*;

pub mod types;

#[cfg(all(feature = "async"))]
pub mod asynch;

#[cfg(feature = "blocking")]
pub mod blocking;

/// A JSON-RPC call id
pub type JsonRpcId = u32;

/// An Odoo id
pub type OdooID = u32;

/// A string representing the JSON-RPC version
///
/// At the time of writing, this is always set to "2.0"
#[derive(Debug, Serialize, Deserialize, PartialEq)]
pub enum JsonRpcVersion {
    /// The JSON-RPC call version (this is always "2.0")

    /// Odoo JSON-RCP API version 2.0
    #[serde(rename = "2.0")]
    V2,
}

/// A string representing the JSON-RPC "method"
///
/// At the time of writing, this is always set to "call"
#[derive(Debug, Serialize, Deserialize, PartialEq)]
pub enum JsonRpcMethod {
    #[serde(rename = "call")]
    Call,
}

/// An Odoo JSON-RPC API request
///
/// This struct represents the base JSON data, and is paramterized over the
/// [`OdooApiMethod`] (e.g., the `param` field will be an `OdooApiMethod`)
///
/// See: [base/controllers/rpc.py](https://github.com/odoo/odoo/blob/b6e195ccb3a6c37b0d980af159e546bdc67b1e42/odoo/addons/base/controllers/rpc.py#L154-L157)
/// See also: [odoo/http.py](https://github.com/odoo/odoo/blob/b6e195ccb3a6c37b0d980af159e546bdc67b1e42/odoo/http.py#L347-L368)
#[derive(Debug, Serialize, Deserialize, PartialEq)]
pub struct OdooApiRequest<T>
where
    T: OdooApiMethod + Serialize + Debug + PartialEq,
{
    /// The JSON-RPC version (`2.0`)
    pub version: JsonRpcVersion,

    /// The JSON-RPC method (`call`)
    pub method: JsonRpcMethod,

    /// The request id
    ///
    /// This is not used for any stateful behaviour on the Odoo/Python side
    pub id: JsonRpcId,

    /// The request params (service, method, and arguments)
    pub params: JsonRpcRequestParams<T>,
}

/// A container struct for the API request data
///
/// This struct is used to implement a custom [`Serialize`](serde::Serialize).
/// The struct is actually serialized into JSON as:
/// ```jsonc
/// {
///     "service": "xxx"
///     "method": "xxx",
///     "args": args
/// }
/// ```
#[derive(Debug, Deserialize, PartialEq)]
pub struct JsonRpcRequestParams<T>
where
    T: OdooApiMethod + Serialize + Debug + PartialEq,
{
    pub args: T,
}

impl<T> Serialize for JsonRpcRequestParams<T>
where
    T: OdooApiMethod + Serialize,
{
    fn serialize<S>(&self, serializer: S) -> ::std::result::Result<S::Ok, S::Error>
    where
        S: Serializer,
    {
        let mut state = serializer.serialize_struct("args", 3)?;
        let (service, method) = self.args.describe_odoo_api_method();
        state.serialize_field("service", service)?;
        state.serialize_field("method", method)?;
        state.serialize_field("args", &self.args)?;
        state.end()
    }
}

/// An Odoo JSON-RPC API response
///
/// This struct represents the base JSON data, and is paramterized over the
/// *request* [`OdooApiMethod`]. The deserialization struct is chosen by
/// looking at the associated type [`OdooApiMethod::Response`].
///
/// See: [odoo/http.py](https://github.com/odoo/odoo/blob/b6e195ccb3a6c37b0d980af159e546bdc67b1e42/odoo/http.py#L1805-L1841)
#[derive(Debug, Serialize, Deserialize, PartialEq)]
#[serde(untagged)]
pub enum OdooApiResponse<T>
where
    T: OdooApiMethod + Serialize + Debug + PartialEq,
{
    /// A successful Odoo API response
    Success(JsonRpcResponseSuccess<T>),

    /// A failed Odoo API response
    Error(JsonRpcResponseError),
}

impl<T: OdooApiMethod + Serialize + Debug + PartialEq> OdooApiResponse<T> {
    /// Convert the response struct into a [`serde_json::Value`]
    pub fn to_json_value(&self) -> serde_json::Result<Value> {
        to_value(self)
    }

    /// Convert the response struct into a "minified" string
    pub fn to_json_string(&self) -> serde_json::Result<String> {
        to_string(self)
    }

    /// Convert the response struct into a "prettified" string
    pub fn to_json_string_pretty(&self) -> serde_json::Result<String> {
        to_string_pretty(self)
    }
}

/// A successful Odoo API response
#[derive(Debug, Serialize, Deserialize, PartialEq)]
pub struct JsonRpcResponseSuccess<T>
where
    T: OdooApiMethod + Serialize + Debug + PartialEq,
{
    /// The JSON-RPC version (`2.0`)
    pub jsonrpc: JsonRpcVersion,

    /// The request id
    ///
    /// This is not used for any stateful behaviour on the Odoo/Python side
    pub id: JsonRpcId,

    /// The response data, parameterized on the *request* [`OdooApiMethod::Response`]
    /// associated type.
    pub result: T::Response,
}

/// A failed Odoo API response
#[derive(Debug, Serialize, Deserialize, PartialEq)]
pub struct JsonRpcResponseError {
    /// The JSON-RPC version (`2.0`)
    pub jsonrpc: JsonRpcVersion,

    /// The request id
    ///
    /// This is not used for any stateful behaviour on the Odoo/Python side
    pub id: JsonRpcId,

    /// A struct containing the error information
    pub error: JsonRpcError,
}

/// A struct representing the high-level error information
///
/// See: [odoo/http.py](https://github.com/odoo/odoo/blob/b6e195ccb3a6c37b0d980af159e546bdc67b1e42/odoo/http.py#L1805-L1841)
#[derive(Debug, Serialize, Deserialize, PartialEq)]
pub struct JsonRpcError {
    /// The error code. Currently hardcoded to `200`
    pub code: u32,

    /// The error "message". This is a short string indicating the type of
    /// error. Some examples are:
    ///  * `Odoo Server Error`
    ///  * `404: Not Found`
    ///  * `Odoo Session Expired`
    pub message: String,

    /// The actual error data
    pub data: JsonRpcErrorData,
}

impl std::fmt::Display for JsonRpcError {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        write!(f, "{:?}", self)
    }
}

/// A struct representing the low-level error information
///
/// See: [odoo/http.py](https://github.com/odoo/odoo/blob/b6e195ccb3a6c37b0d980af159e546bdc67b1e42/odoo/http.py#L375-L385)
#[derive(Debug, Serialize, Deserialize, PartialEq)]
pub struct JsonRpcErrorData {
    /// The module? and type of the object where the exception was raised
    ///
    /// For example:
    ///  * `builtins.TypeError`
    ///  * `odoo.addons.account.models.account_move.AccountMove`
    pub name: String,

    /// The Python exception stack trace
    pub debug: String,

    /// The Python exception message (e.g. `str(exception)`)
    pub message: String,

    /// The Python exception arguments (e.g. `excetion.args`)
    pub arguments: Vec<Value>,

    /// The Python exception context (e.g. `excetion.context`)
    pub context: Map<String, Value>,
}

impl<T: OdooApiMethod + Serialize + Debug + PartialEq> OdooApiRequest<T> {
    /// Convert the request struct into a [`serde_json::Value`]
    pub fn to_json_value(&self) -> serde_json::Result<Value> {
        to_value(self)
    }

    /// Convert the request struct into a "minified" string
    pub fn to_json_string(&self) -> serde_json::Result<String> {
        to_string(self)
    }

    /// Convert the request struct into a "prettified" string
    pub fn to_json_string_pretty(&self) -> serde_json::Result<String> {
        to_string_pretty(self)
    }

    /// Parse a JSON string into the [`OdooApiMethod::Response`] associated type
    pub fn parse_json_response(&self, json_data: &str) -> serde_json::Result<OdooApiResponse<T>> {
        self.params.args.parse_json_response(json_data)
    }
}

/// A trait implemented by the "request" structs
///
/// This trait serves a few purposes:
///  1. Create a link between the request and response structs (e.g., [`Execute`](crate::types::object::Execute) and [`ExecuteResponse`](crate::types::object::ExecuteResponse))
///  2. Describe the request (e.g. service: `object`, method: `execute`)
///  3. Provide a response-parsing function
pub trait OdooApiMethod
where
    Self: Sized + Serialize + Debug + PartialEq,
{
    /// The response type (e.g., the [`ExecuteResponse`](crate::types::object::ExecuteResponse) for [`Execute`](crate::types::object::Execute))
    type Response: Sized
        + Serialize
        + DeserializeOwned
        + Debug
        + PartialEq
        + TryFrom<String>
        + TryFrom<Value>;

    /// Describes the Odoo API method (including the service)
    ///
    /// The Odoo API is split into "services" and "methods".
    ///
    /// For example, his function is responsible for returning the `"common"`
    /// and `"version"` below:
    /// ```jsonc
    /// {
    ///     "jsonrpc": "2.0",
    ///     "method": "call",
    ///     "params": {
    ///         // the "service"
    ///         "service": "common",
    ///
    ///         // the "method"
    ///         "method": "version",
    ///
    ///         "args": []
    ///     }
    /// }
    /// ```
    ///
    fn describe_odoo_api_method(&self) -> (&'static str, &'static str);

    /// Parse some JSON string data into an [`OdooApiResponse`](crate::jsonrpc::OdooApiRequest) object
    ///
    /// Internally, `OdooApiResponse` uses the [`Response`](crate::jsonrpc::OdooApiMethod::Response) associated type to
    /// decide how to deserialize the JSON data.
    fn parse_json_response(&self, json_data: &str) -> serde_json::Result<OdooApiResponse<Self>>;
}

// use std::convert::{TryFrom, TryInto};

// impl<T> TryFrom<&str> for T
// where
//     T: OdooApiMethod
// {
//     type Error = Error;

//     fn try_from(value: &str) -> ::std::result::Result<T::Response, Self::Error> {

//         Ok(())
//     }
// }
// impl<T: OdooApiMethod> TryInto<T> for &str
// {
//     type Error = Error;

//     fn try_into(value: &str) -> ::std::result::Result<T, Self::Error> {

//         Ok(())
//     }
// }