odoo_api/service/
common.rs

1//! The Odoo "common" service (JSON-RPC)
2//!
3//! This service provides misc methods like `version` and `authenticate`.
4//!
5//! Note that the authentication methods (`login` and `authenticate`) are both "dumb";
6//! that is, they do not work with Odoo's sessioning mechanism. The result is that
7//! these methods will not work for non-JSON-RPC methods (e.g. "Web" methods), and
8//! they will not handle multi-database Odoo deployments.
9
10use crate as odoo_api;
11use crate::jsonrpc::{OdooApiMethod, OdooId};
12use odoo_api_macros::odoo_api;
13use serde::ser::SerializeTuple;
14use serde::{Deserialize, Serialize};
15use serde_json::{Map, Value};
16use serde_tuple::Serialize_tuple;
17
18/// Check the user credentials and return the user ID
19///
20/// This method performs a "login" to the Odoo server, and returns the corresponding
21/// user ID (`uid`).
22///
23/// Note that the Odoo JSON-RPC API is stateless; there are no sessions or tokens,
24/// each requests passes the password (or API key). Therefore, calling this method
25/// "login" is a misnomer - it doesn't actually "login", just checks the credentials
26/// and returns the ID.
27///
28/// ## Example
29/// ```no_run
30/// # #[cfg(not(feature = "types-only"))]
31/// # fn test() -> Result<(), Box<dyn std::error::Error>> {
32/// # use odoo_api::OdooClient;
33/// # let client = OdooClient::new_reqwest_blocking("")?;
34/// # let mut client = client.authenticate_manual("", "", 1, "", None);
35/// // note that auth fields (db, login, password) are auto-filled
36/// // for you by the client
37/// let resp = client.common_login().send()?;
38///
39/// println!("UID: {}", resp.uid);
40/// # Ok(())
41/// # }
42/// ```
43///<br />
44///
45/// See: [odoo/service/common.py](https://github.com/odoo/odoo/blob/b6e195ccb3a6c37b0d980af159e546bdc67b1e42/odoo/service/common.py#L19-L20)  
46/// See also: [base/models/res_users.py](https://github.com/odoo/odoo/blob/b6e195ccb3a6c37b0d980af159e546bdc67b1e42/odoo/addons/base/models/res_users.py#L762-L787)
47#[odoo_api(
48    service = "common",
49    method = "login",
50    name = "common_login",
51    auth = true
52)]
53#[derive(Debug, Serialize_tuple)]
54pub struct Login {
55    /// The database name
56    pub db: String,
57
58    /// The username (e.g., email)
59    pub login: String,
60
61    /// The user password
62    pub password: String,
63}
64
65/// Represents the response to an Odoo [`Login`] call
66#[derive(Debug, Serialize, Deserialize)]
67#[serde(transparent)]
68pub struct LoginResponse {
69    pub uid: OdooId,
70}
71
72/// Check the user credentials and return the user ID (web)
73///
74/// This method performs a "login" to the Odoo server, and returns the corresponding
75/// user ID (`uid`). It is identical to [`Login`], except that it accepts an extra
76/// param `user_agent_env`, which is normally sent by the browser.
77///
78/// This method is inteded for browser-based API implementations. You should use [`Login`] instead.
79///
80/// ## Example
81/// ```no_run
82/// # #[cfg(not(feature = "types-only"))]
83/// # fn test() -> Result<(), Box<dyn std::error::Error>> {
84/// # use odoo_api::OdooClient;
85/// # let client = OdooClient::new_reqwest_blocking("")?;
86/// # let mut client = client.authenticate_manual("", "", 1, "", None);
87/// use odoo_api::jmap;
88///
89/// // note that auth fields (db, login, password) are auto-filled
90/// // for you by the client
91/// let resp = client.common_authenticate(
92///     jmap!{
93///         "base_location": "https://demo.odoo.com"
94///     }
95/// ).send()?;
96///
97/// println!("UID: {}", resp.uid);
98/// # Ok(())
99/// # }
100/// ```
101///<br />
102///
103/// See: [odoo/service/common.py](https://github.com/odoo/odoo/blob/b6e195ccb3a6c37b0d980af159e546bdc67b1e42/odoo/service/common.py#L22-L29)  
104/// See also: [base/models/res_users.py](https://github.com/odoo/odoo/blob/b6e195ccb3a6c37b0d980af159e546bdc67b1e42/odoo/addons/base/models/res_users.py#L762-L787)
105#[odoo_api(
106    service = "common",
107    method = "authenticate",
108    name = "common_authenticate",
109    auth = true
110)]
111#[derive(Debug, Serialize_tuple)]
112pub struct Authenticate {
113    /// The database name
114    pub db: String,
115
116    /// The username (e.g., email)
117    pub login: String,
118
119    /// The user password
120    pub password: String,
121
122    /// A mapping of user agent env entries
123    pub user_agent_env: Map<String, Value>,
124}
125
126/// Represents the response to an Odoo [`Authenticate`] call
127#[derive(Debug, Serialize, Deserialize)]
128#[serde(transparent)]
129pub struct AuthenticateResponse {
130    pub uid: OdooId,
131}
132
133/// Fetch detailed information about the Odoo version
134///
135/// This method returns some information about the Odoo version (represented in
136/// the [`ServerVersionInfo`] struct), along with some other metadata.
137///
138/// Odoo's versioning was inspired by Python's [`sys.version_info`](https://github.com/odoo/odoo/blob/b6e195ccb3a6c37b0d980af159e546bdc67b1e42/odoo/release.py#L11),
139/// with an added field to indicate whether the server is running Enterprise or
140/// Community edition. In practice, `minor` and `micro` are typically both `0`,
141/// so an Odoo version looks something like: `14.0.0.final.0.e`
142///
143/// ## Example
144/// ```no_run
145/// # #[cfg(not(feature = "types-only"))]
146/// # fn test() -> Result<(), Box<dyn std::error::Error>> {
147/// # use odoo_api::OdooClient;
148/// # let client = OdooClient::new_reqwest_blocking("")?;
149/// # let mut client = client.authenticate_manual("", "", 1, "", None);
150/// let resp = client.common_version().send()?;
151///
152/// println!("Version Info: {:#?}", resp);
153/// # Ok(())
154/// # }
155/// ```
156///<br />
157///
158/// See: [odoo/service/common.py](https://github.com/odoo/odoo/blob/b6e195ccb3a6c37b0d980af159e546bdc67b1e42/odoo/service/common.py#L31-L32)  
159/// See also: [odoo/service/common.py](https://github.com/odoo/odoo/blob/b6e195ccb3a6c37b0d980af159e546bdc67b1e42/odoo/service/common.py#L12-L17)  
160/// See also: [odoo/release.py](https://github.com/odoo/odoo/blob/b6e195ccb3a6c37b0d980af159e546bdc67b1e42/odoo/release.py)
161#[odoo_api(
162    service = "common",
163    method = "version",
164    name = "common_version",
165    auth = false
166)]
167#[derive(Debug)]
168pub struct Version {}
169
170// Version has no fields, but needs to output in JSON: `[]`
171impl Serialize for Version {
172    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
173    where
174        S: serde::Serializer,
175    {
176        let state = serializer.serialize_tuple(0)?;
177        state.end()
178    }
179}
180
181/// Represents the response to an Odoo [`Version`] call
182#[derive(Debug, Serialize, Deserialize)]
183pub struct VersionResponse {
184    /// The "pretty" version, normally something like `16.0+e` or `15.0`
185    pub server_version: String,
186
187    /// The "full" version. See [`ServerVersionInfo`] for details
188    pub server_version_info: ServerVersionInfo,
189
190    /// The server "series"; like `server_version`, but without any indication of Enterprise vs Community (e.g., `16.0` or `15.0`)
191    pub server_serie: String,
192
193    /// The Odoo "protocol version". At the time of writing, it isn't clear where this is actually used, and `1` is always returned
194    pub protocol_version: u32,
195}
196
197/// A struct representing the Odoo server version info
198///
199/// See: [odoo/services/common.py](https://github.com/odoo/odoo/blob/b6e195ccb3a6c37b0d980af159e546bdc67b1e42/odoo/service/common.py#L12-L17)  
200/// See also: [odoo/release.py](https://github.com/odoo/odoo/blob/b6e195ccb3a6c37b0d980af159e546bdc67b1e42/odoo/release.py)
201#[derive(Debug, Serialize_tuple, Deserialize)]
202pub struct ServerVersionInfo {
203    /// The "major" version (e.g., `16`)
204    pub major: u32,
205
206    /// The "minor" version (e.g., `0`)
207    pub minor: u32,
208
209    /// The "micro" version (e.g., `0`)
210    pub micro: u32,
211
212    /// The "release level"; one of `alpha`, `beta`, `candidate`, or `final`. For live servers, this is almost always `final`
213    pub release_level: String,
214
215    /// The release serial
216    pub serial: u32,
217
218    /// A string indicating whether Odoo is running in Enterprise or Community mode; `None` = Community, Some("e") = Enterprise
219    pub enterprise: Option<String>,
220}
221
222/// Fetch basic information about the Odoo version
223///
224/// Returns a link to the old OpenERP website, and optionally the "basic" Odoo
225/// version string (e.g. `16.0+e`).
226///
227/// This call isn't particularly useful on its own - you probably want to use [`Version`]
228/// instead.
229///
230/// ## Example
231/// ```no_run
232/// # #[cfg(not(feature = "types-only"))]
233/// # fn test() -> Result<(), Box<dyn std::error::Error>> {
234/// # use odoo_api::OdooClient;
235/// # let client = OdooClient::new_reqwest_blocking("")?;
236/// # let mut client = client.authenticate_manual("", "", 1, "", None);
237/// let resp = client.common_about(true).send()?;
238///
239/// println!("About Info: {:?}", resp);
240/// # Ok(())
241/// # }
242/// ```
243///<br />
244///
245/// See: [odoo/service/common.py](https://github.com/odoo/odoo/blob/b6e195ccb3a6c37b0d980af159e546bdc67b1e42/odoo/service/common.py#L34-L45)  
246/// See also: [odoo/release.py](https://github.com/odoo/odoo/blob/b6e195ccb3a6c37b0d980af159e546bdc67b1e42/odoo/release.py)
247#[odoo_api(
248    service = "common",
249    method = "about",
250    name = "common_about",
251    auth = false
252)]
253#[derive(Debug, Serialize_tuple)]
254pub struct About {
255    pub extended: bool,
256}
257
258//TODO: flat deserializ so we can have either `result: "http://..."` or `result: ["http://..", "14.0+e"]`
259/// Represents the response to an Odoo [`About`] call
260#[derive(Debug, Serialize, Deserialize)]
261#[serde(untagged)]
262pub enum AboutResponse {
263    /// Basic response; includes only the `info` string
264    Basic(AboutResponseBasic),
265
266    /// Extended response; includes `info` string and version info
267    Extended(AboutResponseExtended),
268}
269
270/// Represents the response to an Odoo [`About`] call
271#[derive(Debug, Serialize, Deserialize)]
272#[serde(transparent)]
273pub struct AboutResponseBasic {
274    /// The "info" string
275    ///
276    /// At the time of writing, this is hard-coded to `See http://openerp.com`
277    pub info: String,
278}
279
280/// Represents the response to an Odoo [`About`] call
281#[derive(Debug, Serialize_tuple, Deserialize)]
282pub struct AboutResponseExtended {
283    /// The "info" string
284    ///
285    /// At the time of writing, this is hard-coded to `See http://openerp.com`
286    pub info: String,
287
288    /// The "pretty" version, normally something like `16.0+e` or `15.0`
289    ///
290    /// Note that this is only returned when the original reques was made with
291    /// `extended: true` (see [`AboutResponse`])
292    pub server_version: String,
293}
294
295#[cfg(test)]
296mod test {
297    use super::*;
298    use crate::client::error::Result;
299    use crate::jmap;
300    use crate::jsonrpc::{JsonRpcParams, JsonRpcResponse};
301    use serde_json::{from_value, json, to_value};
302
303    /// See [`crate::service::object::test::execute`] for more info
304    #[test]
305    fn login() -> Result<()> {
306        let expected = json!({
307            "jsonrpc": "2.0",
308            "method": "call",
309            "id": 1000,
310            "params": {
311                "service": "common",
312                "method": "login",
313                "args": [
314                    "some-database",
315                    "admin",
316                    "password",
317                ]
318            }
319        });
320        let actual = to_value(
321            Login {
322                db: "some-database".into(),
323                login: "admin".into(),
324                password: "password".into(),
325            }
326            .build(1000),
327        )?;
328
329        assert_eq!(actual, expected);
330
331        Ok(())
332    }
333
334    /// See [`crate::service::object::test::execute_response`] for more info
335    #[test]
336    fn login_response() -> Result<()> {
337        let payload = json!({
338            "jsonrpc": "2.0",
339            "id": 1000,
340            "result": 2
341        });
342
343        let response: JsonRpcResponse<LoginResponse> = from_value(payload)?;
344        match response {
345            JsonRpcResponse::Error(e) => Err(e.error.into()),
346            JsonRpcResponse::Success(_) => Ok(()),
347        }
348    }
349
350    /// See [`crate::service::object::test::execute`] for more info
351    #[test]
352    fn authenticate() -> Result<()> {
353        let expected = json!({
354            "jsonrpc": "2.0",
355            "method": "call",
356            "id": 1000,
357            "params": {
358                "service": "common",
359                "method": "authenticate",
360                "args": [
361                    "some-database",
362                    "admin",
363                    "password",
364                    {
365                        "base_location": "https://demo.odoo.com"
366                    }
367                ]
368            }
369        });
370        let actual = to_value(
371            Authenticate {
372                db: "some-database".into(),
373                login: "admin".into(),
374                password: "password".into(),
375                user_agent_env: jmap! {
376                    "base_location": "https://demo.odoo.com"
377                },
378            }
379            .build(1000),
380        )?;
381
382        assert_eq!(actual, expected);
383
384        Ok(())
385    }
386
387    /// See [`crate::service::object::test::execute_response`] for more info
388    #[test]
389    fn authenticate_response() -> Result<()> {
390        let payload = json!({
391            "jsonrpc": "2.0",
392            "id": 1000,
393            "result": 2
394        });
395
396        let response: JsonRpcResponse<AuthenticateResponse> = from_value(payload)?;
397        match response {
398            JsonRpcResponse::Error(e) => Err(e.error.into()),
399            JsonRpcResponse::Success(_) => Ok(()),
400        }
401    }
402
403    /// See [`crate::service::object::test::execute`] for more info
404    #[test]
405    fn version() -> Result<()> {
406        let expected = json!({
407            "jsonrpc": "2.0",
408            "method": "call",
409            "id": 1000,
410            "params": {
411                "service": "common",
412                "method": "version",
413                "args": []
414            }
415        });
416        let actual = to_value(Version {}.build(1000))?;
417
418        assert_eq!(actual, expected);
419
420        Ok(())
421    }
422
423    /// See [`crate::service::object::test::execute_response`] for more info
424    #[test]
425    fn version_response() -> Result<()> {
426        let payload = json!({
427            "jsonrpc": "2.0",
428            "id": 1000,
429            "result": {
430                "server_version": "14.0+e",
431                "server_version_info": [
432                    14,
433                    0,
434                    0,
435                    "final",
436                    0,
437                    "e"
438                ],
439                "server_serie": "14.0",
440                "protocol_version": 1
441            }
442        });
443
444        let response: JsonRpcResponse<VersionResponse> = from_value(payload)?;
445        match response {
446            JsonRpcResponse::Error(e) => Err(e.error.into()),
447            JsonRpcResponse::Success(_) => Ok(()),
448        }
449    }
450
451    /// See [`crate::service::object::test::execute`] for more info
452    #[test]
453    fn about_basic() -> Result<()> {
454        let expected = json!({
455            "jsonrpc": "2.0",
456            "method": "call",
457            "id": 1000,
458            "params": {
459                "service": "common",
460                "method": "about",
461                "args": [
462                    false
463                ]
464            }
465        });
466        let actual = to_value(About { extended: false }.build(1000))?;
467
468        assert_eq!(actual, expected);
469
470        Ok(())
471    }
472
473    /// See [`crate::service::object::test::execute_response`] for more info
474    #[test]
475    fn about_basic_response() -> Result<()> {
476        let payload = json!({
477            "jsonrpc": "2.0",
478            "id": 1000,
479            "result": "See http://openerp.com"
480        });
481
482        let response: JsonRpcResponse<AboutResponse> = from_value(payload)?;
483        match response {
484            JsonRpcResponse::Error(e) => Err(e.error.into()),
485            JsonRpcResponse::Success(data) => match data.result {
486                AboutResponse::Basic(_) => Ok(()),
487                AboutResponse::Extended(_) => {
488                    panic!("Expected the `Basic` response, but got `Extended`")
489                }
490            },
491        }
492    }
493
494    /// See [`crate::service::object::test::execute`] for more info
495    #[test]
496    fn about_extended() -> Result<()> {
497        let expected = json!({
498            "jsonrpc": "2.0",
499            "method": "call",
500            "id": 1000,
501            "params": {
502                "service": "common",
503                "method": "about",
504                "args": [
505                    true
506                ]
507            }
508        });
509        let actual = to_value(About { extended: true }.build(1000))?;
510
511        assert_eq!(actual, expected);
512
513        Ok(())
514    }
515
516    /// See [`crate::service::object::test::execute_response`] for more info
517    #[test]
518    fn about_extended_response() -> Result<()> {
519        let payload = json!({
520            "jsonrpc": "2.0",
521            "id": 1000,
522            "result": [
523                "See http://openerp.com",
524                "14.0+e"
525            ]
526        });
527
528        let response: JsonRpcResponse<AboutResponse> = from_value(payload)?;
529        match response {
530            JsonRpcResponse::Error(e) => Err(e.error.into()),
531            JsonRpcResponse::Success(data) => match data.result {
532                AboutResponse::Extended(_) => Ok(()),
533                AboutResponse::Basic(_) => {
534                    panic!("Expected the `Extended` response, but got `Basic`")
535                }
536            },
537        }
538    }
539}