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}