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}