odoo_api/service/
object.rs

1//! The Odoo "object" service (JSON-RPC)
2//!
3//! This service provides low-level methods to interact with Odoo models (`execute`
4//! and `execute_kw`).
5//!
6//! For higher-level methods (e.g., `read` and `search_read`), see [`crate::service::orm`]
7
8use crate as odoo_api;
9use crate::jsonrpc::{OdooApiMethod, OdooId};
10use odoo_api_macros::odoo_api;
11use serde::ser::SerializeTuple;
12use serde::{Deserialize, Serialize};
13use serde_json::{Map, Value};
14use serde_tuple::Serialize_tuple;
15
16/// Call a business-logic method on an Odoo model (positional args)
17///
18/// This method allows you to call an arbitrary Odoo method (e.g. `read` or
19/// `create` or `my_function`), passing some arbitrary data, and returns the
20/// result of that method call.
21///
22/// Note that the way this method handles keyword argument is unintuitive. If
23/// you need to send `kwargs` to an Odoo method, you should use [`ExecuteKw`]
24/// instead
25///
26/// ## Example
27/// ```no_run
28/// # #[cfg(not(feature = "types-only"))]
29/// # fn test() -> Result<(), Box<dyn std::error::Error>> {
30/// # use odoo_api::OdooClient;
31/// # let client = OdooClient::new_reqwest_blocking("")?;
32/// # let mut client = client.authenticate_manual("", "", 1, "", None);
33/// use odoo_api::jvec;
34///
35/// // read `id` and `login` from users id=1,2,3
36/// client.execute(
37///     "res.users",
38///     "read",
39///     jvec![
40///         [1, 2, 3],
41///         ["id", "login"]
42///     ]
43/// ).send()?;
44/// # Ok(())
45/// # }
46/// ```
47///
48/// <br />
49///
50/// ## Arguments
51///
52/// ### `method`
53///
54/// The `method` field indicates the Python function to be called. This can be
55/// any non-private method. Methods starting with an underscore (e.g. `_onchange_name`)
56/// are considered to be "private".
57///
58/// ### `args`
59///
60/// The arguments are passed to Python as `object.method_name(*args)`, so
61/// kwargs are technically supported here.
62///
63/// For example, consider the Python function
64/// ```python
65/// def search_read(domain, fields=None):
66///     pass
67/// ```
68///
69/// Our `args` field should be structured like:
70/// ```no_run
71/// # use odoo_api::jvec;
72/// let args = jvec![
73///     // element #1 goes to `domain`
74///     [
75///         ["name", "!=", "admin"],
76///     ],
77///
78///     // element #2 goes to `fields`
79///     ["id", "login"]
80/// ];
81/// ```
82///
83/// <br />
84///
85/// Also note that many Odoo methods accept `self` as the first param. In that
86/// case, you should pass a list of IDs as the first element.
87///
88/// See: [odoo/service/model.py](https://github.com/odoo/odoo/blob/b6e195ccb3a6c37b0d980af159e546bdc67b1e42/odoo/service/model.py#L62-L68)
89#[odoo_api(service = "object", method = "execute", auth = true)]
90#[derive(Debug)]
91pub struct Execute {
92    /// The database name (auto-filled by [`OdooClient`](crate::client::OdooClient))
93    pub database: String,
94
95    /// The user id (auto-filled by [`OdooClient`](crate::client::OdooClient))
96    pub uid: OdooId,
97
98    /// The user password (auto-filled by [`OdooClient`](crate::client::OdooClient))
99    pub password: String,
100
101    /// The model name
102    pub model: String,
103
104    /// The method name
105    pub method: String,
106
107    /// The method arguments
108    pub args: Vec<Value>,
109}
110
111// execute is a special case: each element of the `args` field must be serialized
112// as a sibling of the `model`/`method`/etc fields.
113//
114// so the final result looks like this:
115//
116// ```
117// "args": [
118//      database,
119//      uid,
120//      password,
121//      model,
122//      method
123//      args[1],
124//      args[2],
125//      args[3]
126//      ...
127// ]
128// ```
129//
130// also note that Execute needs to be serialized as a tuple, not an object
131impl Serialize for Execute {
132    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
133    where
134        S: serde::Serializer,
135    {
136        let mut state = serializer.serialize_tuple(5 + self.args.len())?;
137        state.serialize_element(&self.database)?;
138        state.serialize_element(&self.uid)?;
139        state.serialize_element(&self.password)?;
140        state.serialize_element(&self.model)?;
141        state.serialize_element(&self.method)?;
142        for arg in &self.args {
143            state.serialize_element(&arg)?;
144        }
145
146        state.end()
147    }
148}
149
150/// Represents the response to an Odoo [`Execute`]
151///
152/// This struct is intentionally very generic, as the `execute` call can return
153/// any arbitrary JSON data.
154#[derive(Debug, Serialize, Deserialize)]
155#[serde(transparent)]
156pub struct ExecuteResponse {
157    pub data: Value,
158}
159
160/// Call a business-logic method on an Odoo model (positional & keyword args)
161///
162/// This method is very similar to `execute`; It allows you to call an arbitrary
163/// Odoo method (e.g. `read` or `create` or `my_function`), passing some arbitrary
164/// data, and returns the result of that method call.
165///
166/// This differs from `execute` in that keyword args (`kwargs`) can be passed.
167///
168/// ## Execute:
169/// ```no_run
170/// # #[cfg(not(feature = "types-only"))]
171/// # fn test() -> Result<(), Box<dyn std::error::Error>> {
172/// # use odoo_api::OdooClient;
173/// # let client = OdooClient::new_reqwest_blocking("")?;
174/// # let mut client = client.authenticate_manual("", "", 1, "", None);
175/// use odoo_api::{jvec, jmap};
176///
177/// // read `id` and `login` from any user whose email matches "%@example.com"
178/// client.execute_kw(
179///     "res.users",
180///     "search_read",
181///     jvec![
182///         [["login", "=ilike", "%@example.com"]]
183///     ],
184///     jmap!{
185///         "fields": ["id", "login"]
186///     }
187/// ).send()?;
188/// # Ok(())
189/// # }
190/// ```
191///
192/// <br />
193///
194/// ## Arguments
195///
196/// ### `method`
197///
198/// The `method` field indicates the Python function to be called. This can be
199/// any non-private method. Methods starting with an underscore (e.g. `_onchange_name`)
200/// are considered to be "private".
201///
202/// ### `args` and `kwargs`
203///
204/// The method args (position and keyword) are passed to Python as `(*args, **kwargs)`.
205///
206/// For example:
207/// ```python
208/// ## this function...
209/// def search_read(self, domain, fields=None):
210///     pass
211///
212/// ## ...would be called like
213/// model.search_read(*args, **kwargs)
214/// ```
215///
216/// This is much simpler than [`Execute`].
217///
218/// Also note that many Odoo methods accept `self` as the first param. In that
219/// case, you should pass a list of IDs as the first element.
220///
221/// <br />
222///
223/// Reference: [odoo/service/model.py](https://github.com/odoo/odoo/blob/b6e195ccb3a6c37b0d980af159e546bdc67b1e42/odoo/service/model.py#L58-L59)
224#[odoo_api(service = "object", method = "execute_kw", auth = true)]
225#[derive(Debug, Serialize_tuple)]
226pub struct ExecuteKw {
227    /// The database name (auto-filled by [`OdooClient`](crate::client::OdooClient))
228    pub database: String,
229
230    /// The user id (auto-filled by [`OdooClient`](crate::client::OdooClient))
231    pub uid: OdooId,
232
233    /// The user password (auto-filled by [`OdooClient`](crate::client::OdooClient))
234    pub password: String,
235
236    /// The model name
237    pub model: String,
238
239    /// The method name
240    pub method: String,
241
242    /// The positional arguments
243    pub args: Vec<Value>,
244
245    /// The keyword argments
246    pub kwargs: Map<String, Value>,
247}
248
249/// Represents the response to an Odoo [`Execute`] call
250///
251/// This struct is intentionally very generic, as the `execute` call can return
252/// any arbitrary JSON data.
253#[derive(Debug, Serialize, Deserialize)]
254#[serde(transparent)]
255pub struct ExecuteKwResponse {
256    pub data: Value,
257}
258
259#[cfg(test)]
260mod test {
261    use super::*;
262    use crate::client::error::Result;
263    use crate::jsonrpc::{JsonRpcParams, JsonRpcResponse};
264    use crate::{jmap, jvec};
265    use serde_json::{from_value, json, to_value};
266
267    /// Test that serializing the [`Execute`] struct produces the expected
268    /// JSON output.
269    ///
270    /// This is important because we're *always* using named-field structs on
271    /// the Rust side (for convenience), but several API methods actually
272    /// expect lists of values.
273    ///
274    /// Additionally, for Execute, the `args` field is serialized as a sibling
275    /// to the other fields (see the `impl Serialize` above for more info),
276    ///
277    ///
278    /// We'll follow this test pattern for all other API methods:
279    ///  - Build a valid JSON payload in Postman, using a real production Odoo 14.0+e instance
280    ///  - That JSON payload becomes the `expected` variable
281    ///  - Build the request struct in the test function (`execute` variable below)
282    ///  - Compare the two with `assert_eq!()`
283    ///
284    /// This should ensure that the crate is producing valid JSON payloads
285    #[test]
286    fn execute() -> Result<()> {
287        let expected = json!({
288            "jsonrpc": "2.0",
289            "method": "call",
290            "id": 1000,
291            "params": {
292                "service": "object",
293                "method": "execute",
294                "args": [
295                    "some-database",
296                    2,
297                    "password",
298                    "res.users",
299                    "read",
300                    [1, 2],
301                    ["id", "login"]
302                ]
303            }
304        });
305        let actual = to_value(
306            Execute {
307                database: "some-database".into(),
308                uid: 2,
309                password: "password".into(),
310
311                model: "res.users".into(),
312                method: "read".into(),
313                args: jvec![[1, 2], ["id", "login"]],
314            }
315            .build(1000),
316        )?;
317
318        assert_eq!(actual, expected);
319
320        Ok(())
321    }
322
323    /// Test that a valid Odoo response payload is serializable into [`ExecuteResponse`]
324    ///
325    /// As with [`execute`] above, this is achieved by firing a JSON-RPC request
326    /// at a live Odoo instance. Here we take the response JSON and try to serialize
327    /// it into the [`ExecuteResponse`] struct via `from_value()`.
328    ///
329    /// If this succeeds, then the response struct is set up properly!
330    #[test]
331    fn execute_response() -> Result<()> {
332        let payload = json!({
333            "jsonrpc": "2.0",
334            "id": 1000,
335            "result": [
336                {
337                    "id": 1,
338                    "login": "__system__"
339                },
340                {
341                    "id": 2,
342                    "login": "admin"
343                }
344            ]
345        });
346
347        let response: JsonRpcResponse<ExecuteResponse> = from_value(payload)?;
348
349        // note that this match isn't strictly necessary right now, because
350        // the Error() variant is only produced when the input JSON contains
351        // an `"error": {}` key (and we aren't testing those cases).
352        match response {
353            JsonRpcResponse::Error(e) => Err(e.error.into()),
354            JsonRpcResponse::Success(_) => Ok(()),
355        }
356    }
357
358    /// See [`crate::service::object::test::execute`] for more info
359    #[test]
360    fn execute_kw() -> Result<()> {
361        let expected = json!({
362            "jsonrpc": "2.0",
363            "method": "call",
364            "id": 1000,
365            "params": {
366                "service": "object",
367                "method": "execute_kw",
368                "args": [
369                    "some-database",
370                    2,
371                    "password",
372                    "res.users",
373                    "read",
374                    [
375                        [1, 2]
376                    ],
377                    {
378                        "fields": ["id", "login"]
379                    }
380                ]
381            }
382        });
383        let actual = to_value(
384            ExecuteKw {
385                database: "some-database".into(),
386                uid: 2,
387                password: "password".into(),
388
389                model: "res.users".into(),
390                method: "read".into(),
391                args: jvec![[1, 2]],
392                kwargs: jmap! {
393                    "fields": ["id", "login"]
394                },
395            }
396            .build(1000),
397        )?;
398
399        assert_eq!(actual, expected);
400
401        Ok(())
402    }
403
404    /// See [`crate::service::object::test::execute_response`] for more info
405    #[test]
406    fn execute_kw_response() -> Result<()> {
407        let payload = json!({
408            "jsonrpc": "2.0",
409            "id": 1000,
410            "result": [
411                {
412                    "id": 1,
413                    "login": "__system__"
414                },
415                {
416                    "id": 2,
417                    "login": "admin"
418                }
419            ]
420        });
421
422        let response: JsonRpcResponse<ExecuteKwResponse> = from_value(payload)?;
423        match response {
424            JsonRpcResponse::Error(e) => Err(e.error.into()),
425            JsonRpcResponse::Success(_) => Ok(()),
426        }
427    }
428}