odoo_api/service/
db.rs

1//! The Odoo "db" service (JSON-RPC)
2//!
3//! This service handles database-management related methods (like create, drop, etc)
4//!
5//! Note that you will see some methods that require a `passwd` argument. This is **not**
6//! the Odoo user password (database-level). Instead, it's the Odoo server-level
7//! "master password", which can be found in the Odoo `.conf` file as the `admin_passwd` key.
8
9use crate as odoo_api;
10use crate::jsonrpc::OdooApiMethod;
11use odoo_api_macros::odoo_api;
12use serde::de::Visitor;
13use serde::ser::SerializeTuple;
14use serde::{Deserialize, Serialize};
15use serde_tuple::Serialize_tuple;
16
17/// Create and initialize a new database
18///
19/// Note that this request may take some time to complete, and it's likely
20/// worth only firing this from an async-type client
21///
22/// ## Example
23/// ```no_run
24/// # #[cfg(not(feature = "types-only"))]
25/// # fn test() -> Result<(), Box<dyn std::error::Error>> {
26/// # use odoo_api::OdooClient;
27/// # let client = OdooClient::new_reqwest_blocking("")?;
28/// # let mut client = client.authenticate_manual("", "", 1, "", None);
29/// let resp = client.db_create_database(
30///     "master-password",
31///     "new-database-name",
32///     false,      // demo
33///     "en_GB",    // lang
34///     "password1",// user password
35///     "admin",    // username
36///     Some("gb".into()), // country
37///     None        // phone
38/// ).send()?;
39/// # Ok(())
40/// # }
41/// ```
42///<br />
43///
44/// Reference: [odoo/service/db.py](https://github.com/odoo/odoo/blob/b6e195ccb3a6c37b0d980af159e546bdc67b1e42/odoo/service/db.py#L136-L142)
45#[odoo_api(
46    service = "db",
47    method = "create_database",
48    name = "db_create_database",
49    auth = false
50)]
51#[derive(Debug, Serialize_tuple)]
52pub struct CreateDatabase {
53    /// The Odoo master password
54    pub passwd: String,
55
56    /// The name for the new database
57    pub db_name: String,
58
59    /// Should demo data be included?
60    pub demo: bool,
61
62    /// What language should be installed?
63    ///
64    /// This should be an "ISO" formatted string, e.g., "en_US" or "en_GB".
65    ///
66    /// See also: [`ListLang`]
67    pub lang: String,
68
69    /// A password for the "admin" user
70    pub user_password: String,
71
72    /// A login/username for the "admin" user
73    pub login: String,
74
75    /// Optionally specify a country
76    ///
77    /// This is used as a default for the default company created when the database
78    /// is initialised.
79    ///
80    /// See also: [`ListCountries`]
81    pub country_code: Option<String>,
82
83    /// Optionally specify a phone number
84    ///
85    /// As with `country_code`, this is used as a default for the newly-created
86    /// company.
87    pub phone: Option<String>,
88}
89
90/// The response to a [`CreateDatabase`] request
91#[derive(Debug, Serialize, Deserialize)]
92#[serde(transparent)]
93pub struct CreateDatabaseResponse {
94    pub ok: bool,
95}
96
97/// Duplicate a database
98///
99/// Note that this request may take some time to complete, and it's likely
100/// worth only firing this from an async-type client
101///
102/// ## Example
103/// ```no_run
104/// # #[cfg(not(feature = "types-only"))]
105/// # fn test() -> Result<(), Box<dyn std::error::Error>> {
106/// # use odoo_api::OdooClient;
107/// # let client = OdooClient::new_reqwest_blocking("")?;
108/// # let mut client = client.authenticate_manual("", "", 1, "", None);
109/// let resp = client.db_duplicate_database(
110///     "master-password",
111///     "old-database",
112///     "new-database"
113/// ).send()?;
114/// # Ok(())
115/// # }
116/// ```
117///<br />
118///
119/// Reference: [odoo/service/db.py](https://github.com/odoo/odoo/blob/b6e195ccb3a6c37b0d980af159e546bdc67b1e42/odoo/service/db.py#L144-L184)
120#[odoo_api(
121    service = "db",
122    method = "duplicate_database",
123    name = "db_duplicate_database",
124    auth = false
125)]
126#[derive(Debug, Serialize_tuple)]
127pub struct DuplicateDatabase {
128    /// The Odoo master password
129    pub passwd: String,
130
131    /// The original DB name (copy source)
132    pub db_original_name: String,
133
134    /// The new DB name (copy dest)
135    pub db_name: String,
136}
137
138/// The response to a [`DuplicateDatabase`] request
139#[derive(Debug, Serialize, Deserialize)]
140#[serde(transparent)]
141pub struct DuplicateDatabaseResponse {
142    pub ok: bool,
143}
144
145/// Drop (delete) a database
146///
147/// Note that this request may take some time to complete, and it's likely
148/// worth only firing this from an async-type client
149///
150/// ## Example
151/// ```no_run
152/// # #[cfg(not(feature = "types-only"))]
153/// # fn test() -> Result<(), Box<dyn std::error::Error>> {
154/// # use odoo_api::OdooClient;
155/// # let client = OdooClient::new_reqwest_blocking("")?;
156/// # let mut client = client.authenticate_manual("", "", 1, "", None);
157/// let resp = client.db_drop(
158///     "master-password",
159///     "database-to-delete",
160/// ).send()?;
161/// # Ok(())
162/// # }
163/// ```
164///<br />
165///
166/// Reference: [odoo/service/db.py](https://github.com/odoo/odoo/blob/b6e195ccb3a6c37b0d980af159e546bdc67b1e42/odoo/service/db.py#L212-L217)
167#[odoo_api(service = "db", method = "drop", name = "db_drop", auth = false)]
168#[derive(Debug, Serialize_tuple)]
169pub struct Drop {
170    /// The Odoo master password
171    pub passwd: String,
172
173    /// The database to be deleted
174    pub db_name: String,
175}
176
177/// The response to a [`Drop`] request
178#[derive(Debug, Serialize, Deserialize)]
179#[serde(transparent)]
180pub struct DropResponse {
181    pub ok: bool,
182}
183
184/// Dump (backup) a database, optionally including the filestore folder
185///
186/// Note that this request may take some time to complete, and it's likely
187/// worth only firing this from an async-type client
188///
189/// Note that the data is returned a base64-encoded buffer.
190///
191/// ## Example
192/// ```no_run
193/// # #[cfg(not(feature = "types-only"))]
194/// # fn test() -> Result<(), Box<dyn std::error::Error>> {
195/// # use odoo_api::OdooClient;
196/// # let client = OdooClient::new_reqwest_blocking("")?;
197/// # let mut client = client.authenticate_manual("", "", 1, "", None);
198/// # #[allow(non_camel_case_types)]
199/// # struct base64 {}
200/// # impl base64 { fn decode(input: &str) -> Result<Vec<u8>, Box<dyn std::error::Error>> { Ok(Vec::new()) }}
201/// use odoo_api::service::db::DumpFormat;
202///
203/// let resp = client.db_dump(
204///     "master-password",
205///     "database-to-dump",
206///     DumpFormat::Zip
207/// ).send()?;
208///
209/// // parse the returned b64 string into a byte array
210/// // e.g., with the `base64` crate: https://docs.rs/base64/latest/base64/
211/// let data: Vec<u8> = base64::decode(&resp.b64_bytes)?;
212///
213/// // write the data to a file ...
214/// # Ok(())
215/// # }
216/// ```
217///<br />
218///
219/// Reference: [odoo/service/db.py](https://github.com/odoo/odoo/blob/b6e195ccb3a6c37b0d980af159e546bdc67b1e42/odoo/service/db.py#L212-L217)  
220/// See also: [odoo/service/db.py](https://github.com/odoo/odoo/blob/b6e195ccb3a6c37b0d980af159e546bdc67b1e42/odoo/service/db.py#L219-L269)
221#[odoo_api(service = "db", method = "dump", name = "db_dump", auth = false)]
222#[derive(Debug, Serialize_tuple)]
223pub struct Dump {
224    /// The Odoo master password
225    pub passwd: String,
226
227    /// The database to be backed-up
228    pub db_name: String,
229
230    /// The dump format. See [`DumpFormat`] for more info
231    pub format: crate::service::db::DumpFormat,
232}
233
234/// The format for a database dump
235#[derive(Debug, Serialize, Deserialize)]
236pub enum DumpFormat {
237    /// Output a zipfile containing the SQL dump in "plain" format, manifest, and filestore
238    ///
239    /// Note that with this mode, the database is dumped to a Python
240    /// NamedTemporaryFile first, then to the out stream - this means that
241    /// the backup takes longer, and probably involves some filesystem writes.
242    ///
243    /// Also note that the SQL format is "plain"; that is, it's a text file
244    /// containing SQL statements. This style of database dump is slightly less
245    /// flexible when importing (e.g., you cannot choose to exclude some
246    /// tables during import).
247    ///
248    /// See the [Postgres `pg_dump` docs](https://www.postgresql.org/docs/current/app-pgdump.html) for more info on "plain" dumps (`-F` option).
249    #[serde(rename = "zip")]
250    Zip,
251
252    /// Output a `.dump` file containing the SQL dump in "custom" format
253    ///
254    /// This style of database dump is more flexible on the import side (e.g.,
255    /// you can choose to exclude some tables from the import), but does not
256    /// include the filestore.
257    ///
258    /// See the [Postgres `pg_dump` docs](https://www.postgresql.org/docs/current/app-pgdump.html) for more info on "custom" dumps (`-F` option).
259    #[serde(rename = "dump")]
260    Dump,
261}
262
263/// The response to a [`Dump`] request
264#[derive(Debug, Serialize, Deserialize)]
265#[serde(transparent)]
266pub struct DumpResponse {
267    /// The database dump, as a base-64 encoded string
268    ///
269    /// Note that the file type will depend on the `format` used in the original request:
270    /// - [`DumpFormat::Zip`]: `backup.zip`
271    /// - [`DumpFormat::Dump`]: `backup.dump` (text file containig SQL CREATE/INSERT/etc statements )
272    pub b64_bytes: String,
273}
274
275/// Upload and restore an Odoo dump to a new database
276///
277/// Note that this request may take some time to complete, and it's likely
278/// worth only firing this from an async-type client
279///
280/// Note also that the uploaded "file" must:
281///  - Be a zip file
282///  - Contain a folder named `filestore`, whose direct descendents are the databases filestore content (e.g. `filestore/a0`, `filestore/a1`, etc)
283///  - Contain a file name `dump.sql`, which is a `pg_dump` "plain" format dump (e.g. a text file of SQL statements)
284///
285/// Typically Odoo backups also include a `manifest.json`, but this file isn't checked
286/// by the Restore endpoint.
287///
288/// ## Example
289/// ```no_run
290/// # #[cfg(not(feature = "types-only"))]
291/// # fn test() -> Result<(), Box<dyn std::error::Error>> {
292/// # use odoo_api::OdooClient;
293/// # let client = OdooClient::new_reqwest_blocking("")?;
294/// # let mut client = client.authenticate_manual("", "", 1, "", None);
295/// # #[allow(non_camel_case_types)]
296/// # struct base64 {}
297/// # impl base64 { fn encode(data: &Vec<u8>) -> Result<Vec<u8>, Box<dyn std::error::Error>> { Ok(data.to_owned()) }}
298/// use odoo_api::service::db::RestoreType;
299/// use std::fs;
300///
301/// // load the file data
302/// let data = fs::read("/my/database/backup.zip")?;
303///
304/// // convert raw bytes to base64
305/// // e.g., with the `base64` crate: https://crates.io/crates/base64
306/// let data_b64 = base64::encode(&data)?;
307///
308/// // convert base64's `Vec<u8>` to a `&str`
309/// let data_b64 = std::str::from_utf8(&data_b64)?;
310///
311/// // read `id` and `login` from users id=1,2,3
312/// client.db_restore(
313///     "master-password",
314///     data_b64,
315///     RestoreType::Copy
316/// ).send()?;
317/// # Ok(())
318/// # }
319/// ```
320///<br />
321///
322/// Reference: [odoo/service/db.py](https://github.com/odoo/odoo/blob/b6e195ccb3a6c37b0d980af159e546bdc67b1e42/odoo/service/db.py#L271-L284)  
323/// See also: [odoo/service/db.py](https://github.com/odoo/odoo/blob/b6e195ccb3a6c37b0d980af159e546bdc67b1e42/odoo/service/db.py#L286-L335)
324#[odoo_api(service = "db", method = "restore", name = "db_restore", auth = false)]
325#[derive(Debug, Serialize_tuple)]
326pub struct Restore {
327    /// The Odoo master password
328    pub passwd: String,
329
330    /// The backup data, as a base64-encoded string
331    pub b64_data: String,
332
333    /// The restore type (see [`RestoreType`])
334    pub restore_type: RestoreType,
335}
336
337/// The type of database restore
338#[derive(Debug)]
339pub enum RestoreType {
340    /// Restore as a "copy"
341    ///
342    /// In this case, the database UUID is automatically updated to prevent
343    /// conflicts.
344    ///
345    /// This is typically used when restoring a database for testing.
346    Copy,
347
348    /// Restore as a "move"
349    ///
350    /// In this case, the database UUID is **not** updated, and the database
351    /// is restored as-is.
352    ///
353    /// This is typically used when restoring a database to a new hosting environment.
354    Move,
355}
356
357// As far as I can tell, there isn't an easy way to serialize/deserialize
358// a two-variant enum to/from a boolean, so we need to implement those manually.
359// note that Deserialize isn't strictly necessary, but I'll include it for
360// completeness.
361impl Serialize for RestoreType {
362    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
363    where
364        S: serde::Serializer,
365    {
366        serializer.serialize_bool(match self {
367            Self::Copy => true,
368            Self::Move => false,
369        })
370    }
371}
372struct RestoreTypeVisitor;
373impl<'de> Visitor<'de> for RestoreTypeVisitor {
374    type Value = bool;
375
376    fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
377        formatter.write_str("a boolean (`true` or `false`)")
378    }
379
380    fn visit_bool<E>(self, v: bool) -> Result<Self::Value, E>
381    where
382        E: serde::de::Error,
383    {
384        Ok(v)
385    }
386}
387impl<'de> Deserialize<'de> for RestoreType {
388    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
389    where
390        D: serde::Deserializer<'de>,
391    {
392        let b = deserializer.deserialize_bool(RestoreTypeVisitor)?;
393
394        Ok(match b {
395            true => Self::Copy,
396            false => Self::Move,
397        })
398    }
399}
400
401/// The response to a [`Restore`] request
402#[derive(Debug, Serialize, Deserialize)]
403#[serde(transparent)]
404pub struct RestoreResponse {
405    pub ok: bool,
406}
407
408/// Rename a database
409///
410/// On the Odoo side, this is handled by issuing an SQL query like:
411/// ```sql
412/// ALTER DATABSE {old_name} RENAME TO {new_name};
413/// ```
414///
415/// It should be a fairly quick request, but note that the above `ALTER DATABASE` statement
416/// may fail for various reasons. See the Postgres documentation for info.
417///
418/// ## Example
419/// ```no_run
420/// # #[cfg(not(feature = "types-only"))]
421/// # fn test() -> Result<(), Box<dyn std::error::Error>> {
422/// # use odoo_api::OdooClient;
423/// # let client = OdooClient::new_reqwest_blocking("")?;
424/// # let mut client = client.authenticate_manual("", "", 1, "", None);
425/// let resp = client.db_rename(
426///     "master-password",
427///     "old-database-name",
428///     "new-database-name",
429/// ).send()?;
430/// # Ok(())
431/// # }
432/// ```
433///<br />
434///
435/// Reference: [odoo/service/db.py](https://github.com/odoo/odoo/blob/b6e195ccb3a6c37b0d980af159e546bdc67b1e42/odoo/service/db.py#L337-L358)
436#[odoo_api(service = "db", method = "rename", name = "db_rename", auth = false)]
437#[derive(Debug, Serialize_tuple)]
438pub struct Rename {
439    /// The Odoo master password
440    pub passwd: String,
441
442    /// The database name
443    pub old_name: String,
444
445    /// The new database name
446    pub new_name: String,
447}
448
449/// The response to a [`Rename`] request
450#[derive(Debug, Serialize, Deserialize)]
451#[serde(transparent)]
452pub struct RenameResponse {
453    pub ok: bool,
454}
455
456/// Change the Odoo "master password"
457///
458/// This method updates the Odoo config file, writing a new value to the `admin_passwd`
459/// key. If the config file is not writeable by Odoo, this will fail.
460///
461/// ## Example
462/// ```no_run
463/// # #[cfg(not(feature = "types-only"))]
464/// # fn test() -> Result<(), Box<dyn std::error::Error>> {
465/// # use odoo_api::OdooClient;
466/// # let client = OdooClient::new_reqwest_blocking("")?;
467/// # let mut client = client.authenticate_manual("", "", 1, "", None);
468/// let resp = client.db_change_admin_password(
469///     "master-password",
470///     "new-master-password",
471/// ).send()?;
472/// # Ok(())
473/// # }
474/// ```
475///<br />
476///
477/// Reference: [odoo/service/db.py](https://github.com/odoo/odoo/blob/b6e195ccb3a6c37b0d980af159e546bdc67b1e42/odoo/service/db.py#L360-L364)
478#[odoo_api(
479    service = "db",
480    method = "change_admin_password",
481    name = "db_change_admin_password",
482    auth = false
483)]
484#[derive(Debug, Serialize_tuple)]
485pub struct ChangeAdminPassword {
486    /// The Odoo master password
487    pub passwd: String,
488
489    /// The  new Odoo master password
490    pub new_passwd: String,
491}
492
493/// The response to a [`ChangeAdminPassword`] request
494#[derive(Debug, Serialize, Deserialize)]
495#[serde(transparent)]
496pub struct ChangeAdminPasswordResponse {
497    pub ok: bool,
498}
499
500/// Perform a "database migration" (upgrade the `base` module)
501///
502/// Note that this method doesn't actually perform any upgrades - instead, it
503/// force-update the `base` module, which has the effect of triggering an update
504/// on all Odoo modules that depend on `base` (which is all of them).
505///
506/// This method is probably used internally by Odoo's upgrade service, and likely
507/// isn't useful on its own. If you need to upgrade a module, the [`Execute`][crate::service::object::Execute]
508/// is probably more suitable.
509///
510/// ## Example
511/// ```no_run
512/// # #[cfg(not(feature = "types-only"))]
513/// # fn test() -> Result<(), Box<dyn std::error::Error>> {
514/// # use odoo_api::OdooClient;
515/// # let client = OdooClient::new_reqwest_blocking("")?;
516/// # let mut client = client.authenticate_manual("", "", 1, "", None);
517/// let resp = client.db_migrate_databases(
518///     "master-password",
519///     vec![
520///         "database1".into(),
521///         "database2".into()
522///     ]
523/// ).send()?;
524/// # Ok(())
525/// # }
526/// ```
527///<br />
528///
529/// Reference: [odoo/service/db.py](https://github.com/odoo/odoo/blob/b6e195ccb3a6c37b0d980af159e546bdc67b1e42/odoo/service/db.py#L366-L372)
530#[odoo_api(
531    service = "db",
532    method = "migrate_databases",
533    name = "db_migrate_databases",
534    auth = false
535)]
536#[derive(Debug, Serialize_tuple)]
537pub struct MigrateDatabases {
538    /// The Odoo master password
539    pub passwd: String,
540
541    /// A list of databases to be migrated
542    pub databases: Vec<String>,
543}
544
545/// The response to a [`MigrateDatabases`] request
546#[derive(Debug, Serialize, Deserialize)]
547#[serde(transparent)]
548pub struct MigrateDatabasesResponse {
549    pub ok: bool,
550}
551
552/// Check if a database exists
553///
554/// ## Example
555/// ```no_run
556/// # #[cfg(not(feature = "types-only"))]
557/// # fn test() -> Result<(), Box<dyn std::error::Error>> {
558/// # use odoo_api::OdooClient;
559/// # let client = OdooClient::new_reqwest_blocking("")?;
560/// # let mut client = client.authenticate_manual("", "", 1, "", None);
561/// let resp = client.db_exist(
562///     "does-this-database-exist?",
563/// ).send()?;
564/// # Ok(())
565/// # }
566/// ```
567///<br />
568///
569/// Reference: [odoo/service/db.py](https://github.com/odoo/odoo/blob/b6e195ccb3a6c37b0d980af159e546bdc67b1e42/odoo/service/db.py#L378-L386)
570#[odoo_api(service = "db", method = "db_exist", auth = false)]
571#[derive(Debug, Serialize_tuple)]
572pub struct DbExist {
573    /// The database name to check
574    pub db_name: String,
575}
576
577/// The response to a [`DbExist`] request
578#[derive(Debug, Serialize, Deserialize)]
579#[serde(transparent)]
580pub struct DbExistResponse {
581    pub exists: bool,
582}
583
584/// List the databases currently available to Odoo
585///
586/// ## Example
587/// ```no_run
588/// # #[cfg(not(feature = "types-only"))]
589/// # fn test() -> Result<(), Box<dyn std::error::Error>> {
590/// # use odoo_api::OdooClient;
591/// # let client = OdooClient::new_reqwest_blocking("")?;
592/// # let mut client = client.authenticate_manual("", "", 1, "", None);
593/// let resp = client.db_list(false).send()?;
594///
595/// println!("Databases: {:#?}", resp.databases);
596/// # Ok(())
597/// # }
598/// ```
599///<br />
600///
601/// Reference: [odoo/service/db.py](https://github.com/odoo/odoo/blob/b6e195ccb3a6c37b0d980af159e546bdc67b1e42/odoo/service/db.py#L439-L442)  
602/// See also: [odoo/service/db.py](https://github.com/odoo/odoo/blob/b6e195ccb3a6c37b0d980af159e546bdc67b1e42/odoo/service/db.py#L388-L409)
603#[odoo_api(service = "db", method = "list", name = "db_list", auth = false)]
604#[derive(Debug, Serialize_tuple)]
605pub struct List {
606    /// This argument isn't currently used and has no effect on the output
607    pub document: bool,
608}
609
610/// The response to a [`List`] request
611#[derive(Debug, Serialize, Deserialize)]
612#[serde(transparent)]
613pub struct ListResponse {
614    pub databases: Vec<String>,
615}
616
617/// List the languages available to Odoo (ISO name + code)
618///
619/// Note that this function is used by the database manager, in order to let the
620/// user select which language should be used when creating a new database.
621///
622/// ## Example
623/// ```no_run
624/// # #[cfg(not(feature = "types-only"))]
625/// # fn test() -> Result<(), Box<dyn std::error::Error>> {
626/// # use odoo_api::OdooClient;
627/// # let client = OdooClient::new_reqwest_blocking("")?;
628/// # let mut client = client.authenticate_manual("", "", 1, "", None);
629/// let resp = client.db_list_lang().send()?;
630///
631/// println!("Languages: {:#?}", resp.languages);
632/// # Ok(())
633/// # }
634/// ```
635///<br />
636///
637/// Reference: [odoo/service/db.py](https://github.com/odoo/odoo/blob/b6e195ccb3a6c37b0d980af159e546bdc67b1e42/odoo/service/db.py#L444-L445)
638#[odoo_api(
639    service = "db",
640    method = "list_lang",
641    name = "db_list_lang",
642    auth = false
643)]
644#[derive(Debug)]
645pub struct ListLang {}
646
647// ListLang has no fields, but needs to output in JSON: `[]`
648impl Serialize for ListLang {
649    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
650    where
651        S: serde::Serializer,
652    {
653        let state = serializer.serialize_tuple(0)?;
654        state.end()
655    }
656}
657
658/// The response to a [`ListLang`] request
659#[derive(Debug, Serialize, Deserialize)]
660#[serde(transparent)]
661pub struct ListLangResponse {
662    pub languages: Vec<ListLangResponseItem>,
663}
664
665/// A single language item from the [`ListLang`] request
666#[derive(Debug, Serialize_tuple, Deserialize)]
667pub struct ListLangResponseItem {
668    /// The ISO language code (e.g., `en_GB`)
669    pub code: String,
670
671    /// The "pretty" language name
672    ///
673    /// This is formatted as: `english_pretty_name / local_name`
674    ///
675    /// Examples:
676    ///     - `Danish / Dansk`
677    ///     - `English (UK)`
678    ///     - `Chinese (Simplified) / 简体中文`
679    pub name: String,
680}
681
682/// List the countries available to Odoo (ISO name + code)
683///
684/// Note that this function is used by the database manager, in order to let the
685/// user select which country should be used when creating a new database.
686///
687/// ## Example
688/// ```no_run
689/// # #[cfg(not(feature = "types-only"))]
690/// # fn test() -> Result<(), Box<dyn std::error::Error>> {
691/// # use odoo_api::OdooClient;
692/// # let client = OdooClient::new_reqwest_blocking("")?;
693/// # let mut client = client.authenticate_manual("", "", 1, "", None);
694/// let resp = client.db_list_countries(
695///     "master-password",
696/// ).send()?;
697///
698/// println!("Countries: {:#?}", resp.countries);
699/// # Ok(())
700/// # }
701/// ```
702///<br />
703///
704/// Reference: [odoo/service/db.py](https://github.com/odoo/odoo/blob/b6e195ccb3a6c37b0d980af159e546bdc67b1e42/odoo/service/db.py#L447-L454)
705#[odoo_api(
706    service = "db",
707    method = "list_countries",
708    name = "db_list_countries",
709    auth = false
710)]
711#[derive(Debug, Serialize_tuple)]
712pub struct ListCountries {
713    /// The Odoo master password
714    pub passwd: String,
715}
716
717/// The response to a [`ListCountries`] request
718#[derive(Debug, Serialize, Deserialize)]
719#[serde(transparent)]
720pub struct ListCountriesResponse {
721    pub countries: Vec<ListLangResponseItem>,
722}
723
724/// A single country item from the [`ListCountries`] request
725#[derive(Debug, Serialize_tuple, Deserialize)]
726pub struct ListCountriesResponseItem {
727    /// The ISO country code
728    pub code: String,
729
730    /// An English "pretty" representation of the country name, e.g.:
731    ///     - `Afghanistan`
732    ///     - `China`
733    ///     - `New Zealand`
734    pub name: String,
735}
736
737/// Return the server version
738///
739/// This returns the "base" server version, e.g., `14.0` or `15.0`. It does not
740/// include any indication of whether the database is Community or Enterprise
741///
742/// ## Example
743/// ```no_run
744/// # #[cfg(not(feature = "types-only"))]
745/// # fn test() -> Result<(), Box<dyn std::error::Error>> {
746/// # use odoo_api::OdooClient;
747/// # let client = OdooClient::new_reqwest_blocking("")?;
748/// # let mut client = client.authenticate_manual("", "", 1, "", None);
749/// let resp = client.db_server_version().send()?;
750///
751/// println!("Version: {}", resp.version);
752/// # Ok(())
753/// # }
754/// ```
755///<br />
756///
757/// Reference: [odoo/service/db.py](https://github.com/odoo/odoo/blob/b6e195ccb3a6c37b0d980af159e546bdc67b1e42/odoo/service/db.py#L456-L460)
758#[odoo_api(
759    service = "db",
760    method = "server_version",
761    name = "db_server_version",
762    auth = false
763)]
764#[derive(Debug)]
765pub struct ServerVersion {}
766
767// ServerVersion has no fields, but needs to output in JSON: `[]`
768impl Serialize for ServerVersion {
769    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
770    where
771        S: serde::Serializer,
772    {
773        let state = serializer.serialize_tuple(0)?;
774        state.end()
775    }
776}
777
778/// The response to a [`ServerVersion`] request
779#[derive(Debug, Serialize, Deserialize)]
780#[serde(transparent)]
781pub struct ServerVersionResponse {
782    /// The database version, e.g., `14.0` or `15.0`
783    pub version: String,
784}
785
786#[cfg(test)]
787mod test {
788    use super::*;
789    use crate::client::error::Result;
790    use crate::jsonrpc::{JsonRpcParams, JsonRpcResponse};
791    use serde_json::{from_value, json, to_value};
792
793    /// See [`crate::service::object::test::execute`] for more info
794    #[test]
795    fn create_database() -> Result<()> {
796        let expected = json!({
797            "jsonrpc": "2.0",
798            "method": "call",
799            "id": 1000,
800            "params": {
801                "service": "db",
802                "method": "create_database",
803                "args": [
804                    "master-password",
805                    "new-database",
806                    false,
807                    "en_US",
808                    "password",
809                    "admin",
810                    null,
811                    "123 123 123"
812                ]
813            }
814        });
815        let actual = to_value(
816            CreateDatabase {
817                passwd: "master-password".into(),
818                db_name: "new-database".into(),
819                demo: false,
820                lang: "en_US".into(),
821                user_password: "password".into(),
822                login: "admin".into(),
823                country_code: None,
824                phone: Some("123 123 123".into()),
825            }
826            .build(1000),
827        )?;
828
829        assert_eq!(actual, expected);
830
831        Ok(())
832    }
833
834    /// See [`crate::service::object::test::execute_response`] for more info
835    #[test]
836    fn create_database_response() -> Result<()> {
837        let payload = json!({
838            "jsonrpc": "2.0",
839            "id": 1000,
840            "result": true
841        });
842
843        let response: JsonRpcResponse<CreateDatabaseResponse> = from_value(payload)?;
844        match response {
845            JsonRpcResponse::Error(e) => Err(e.error.into()),
846            JsonRpcResponse::Success(_) => Ok(()),
847        }
848    }
849
850    /// See [`crate::service::object::test::execute`] for more info
851    #[test]
852    fn duplicate_database() -> Result<()> {
853        let expected = json!({
854            "jsonrpc": "2.0",
855            "method": "call",
856            "id": 1000,
857            "params": {
858                "service": "db",
859                "method": "duplicate_database",
860                "args": [
861                    "master-password",
862                    "old-database",
863                    "new-database",
864                ]
865            }
866        });
867        let actual = to_value(
868            DuplicateDatabase {
869                passwd: "master-password".into(),
870                db_original_name: "old-database".into(),
871                db_name: "new-database".into(),
872            }
873            .build(1000),
874        )?;
875
876        assert_eq!(actual, expected);
877
878        Ok(())
879    }
880
881    /// See [`crate::service::object::test::execute_response`] for more info
882    #[test]
883    fn duplicate_database_response() -> Result<()> {
884        let payload = json!({
885            "jsonrpc": "2.0",
886            "id": 1000,
887            "result": true
888        });
889
890        let response: JsonRpcResponse<DuplicateDatabaseResponse> = from_value(payload)?;
891        match response {
892            JsonRpcResponse::Error(e) => Err(e.error.into()),
893            JsonRpcResponse::Success(_) => Ok(()),
894        }
895    }
896
897    /// See [`crate::service::object::test::execute`] for more info
898    #[test]
899    fn drop() -> Result<()> {
900        let expected = json!({
901            "jsonrpc": "2.0",
902            "method": "call",
903            "id": 1000,
904            "params": {
905                "service": "db",
906                "method": "drop",
907                "args": [
908                    "master-password",
909                    "old-database",
910                ]
911            }
912        });
913        let actual = to_value(
914            Drop {
915                passwd: "master-password".into(),
916                db_name: "old-database".into(),
917            }
918            .build(1000),
919        )?;
920
921        assert_eq!(actual, expected);
922
923        Ok(())
924    }
925
926    /// See [`crate::service::object::test::execute_response`] for more info
927    #[test]
928    fn drop_response() -> Result<()> {
929        let payload = json!({
930            "jsonrpc": "2.0",
931            "id": 1000,
932            "result": true
933        });
934
935        let response: JsonRpcResponse<DropResponse> = from_value(payload)?;
936        match response {
937            JsonRpcResponse::Error(e) => Err(e.error.into()),
938            JsonRpcResponse::Success(_) => Ok(()),
939        }
940    }
941
942    /// See [`crate::service::object::test::execute`] for more info
943    #[test]
944    fn dump_zip() -> Result<()> {
945        let expected = json!({
946            "jsonrpc": "2.0",
947            "method": "call",
948            "id": 1000,
949            "params": {
950                "service": "db",
951                "method": "dump",
952                "args": [
953                    "master-password",
954                    "old-database",
955                    "zip",
956                ]
957            }
958        });
959        let actual = to_value(
960            Dump {
961                passwd: "master-password".into(),
962                db_name: "old-database".into(),
963                format: DumpFormat::Zip,
964            }
965            .build(1000),
966        )?;
967
968        assert_eq!(actual, expected);
969
970        Ok(())
971    }
972
973    /// See [`crate::service::object::test::execute`] for more info
974    #[test]
975    fn dump_dump() -> Result<()> {
976        let expected = json!({
977            "jsonrpc": "2.0",
978            "method": "call",
979            "id": 1000,
980            "params": {
981                "service": "db",
982                "method": "dump",
983                "args": [
984                    "master-password",
985                    "old-database",
986                    "dump",
987                ]
988            }
989        });
990        let actual = to_value(
991            Dump {
992                passwd: "master-password".into(),
993                db_name: "old-database".into(),
994                format: DumpFormat::Dump,
995            }
996            .build(1000),
997        )?;
998
999        assert_eq!(actual, expected);
1000
1001        Ok(())
1002    }
1003
1004    /// See [`crate::service::object::test::execute_response`] for more info
1005    #[test]
1006    fn dump_response() -> Result<()> {
1007        let payload = json!({
1008            "jsonrpc": "2.0",
1009            "id": 1000,
1010            "result": "base64-data-will-be-here"
1011        });
1012
1013        let response: JsonRpcResponse<DumpResponse> = from_value(payload)?;
1014        match response {
1015            JsonRpcResponse::Error(e) => Err(e.error.into()),
1016            JsonRpcResponse::Success(_) => Ok(()),
1017        }
1018    }
1019
1020    /// See [`crate::service::object::test::execute`] for more info
1021    #[test]
1022    fn restore_move() -> Result<()> {
1023        let expected = json!({
1024            "jsonrpc": "2.0",
1025            "method": "call",
1026            "id": 1000,
1027            "params": {
1028                "service": "db",
1029                "method": "restore",
1030                "args": [
1031                    "master-password",
1032                    "base64-data-would-be-here",
1033                    false,
1034                ]
1035            }
1036        });
1037        let actual = to_value(
1038            Restore {
1039                passwd: "master-password".into(),
1040                b64_data: "base64-data-would-be-here".into(),
1041                restore_type: RestoreType::Move,
1042            }
1043            .build(1000),
1044        )?;
1045
1046        assert_eq!(actual, expected);
1047
1048        Ok(())
1049    }
1050
1051    /// See [`crate::service::object::test::execute`] for more info
1052    #[test]
1053    fn restore_copy() -> Result<()> {
1054        let expected = json!({
1055            "jsonrpc": "2.0",
1056            "method": "call",
1057            "id": 1000,
1058            "params": {
1059                "service": "db",
1060                "method": "restore",
1061                "args": [
1062                    "master-password",
1063                    "base64-data-would-be-here",
1064                    true,
1065                ]
1066            }
1067        });
1068        let actual = to_value(
1069            Restore {
1070                passwd: "master-password".into(),
1071                b64_data: "base64-data-would-be-here".into(),
1072                restore_type: RestoreType::Copy,
1073            }
1074            .build(1000),
1075        )?;
1076
1077        assert_eq!(actual, expected);
1078
1079        Ok(())
1080    }
1081
1082    /// See [`crate::service::object::test::execute_response`] for more info
1083    #[test]
1084    fn restore_response() -> Result<()> {
1085        let payload = json!({
1086            "jsonrpc": "2.0",
1087            "id": 1000,
1088            "result": true
1089        });
1090
1091        let response: JsonRpcResponse<RestoreResponse> = from_value(payload)?;
1092        match response {
1093            JsonRpcResponse::Error(e) => Err(e.error.into()),
1094            JsonRpcResponse::Success(_) => Ok(()),
1095        }
1096    }
1097
1098    /// See [`crate::service::object::test::execute`] for more info
1099    #[test]
1100    fn rename() -> Result<()> {
1101        let expected = json!({
1102            "jsonrpc": "2.0",
1103            "method": "call",
1104            "id": 1000,
1105            "params": {
1106                "service": "db",
1107                "method": "rename",
1108                "args": [
1109                    "master-password",
1110                    "old-database",
1111                    "new-database"
1112                ]
1113            }
1114        });
1115        let actual = to_value(
1116            Rename {
1117                passwd: "master-password".into(),
1118                old_name: "old-database".into(),
1119                new_name: "new-database".into(),
1120            }
1121            .build(1000),
1122        )?;
1123
1124        assert_eq!(actual, expected);
1125
1126        Ok(())
1127    }
1128
1129    /// See [`crate::service::object::test::execute_response`] for more info
1130    #[test]
1131    fn rename_response() -> Result<()> {
1132        let payload = json!({
1133            "jsonrpc": "2.0",
1134            "id": 1000,
1135            "result": true
1136        });
1137
1138        let response: JsonRpcResponse<RenameResponse> = from_value(payload)?;
1139        match response {
1140            JsonRpcResponse::Error(e) => Err(e.error.into()),
1141            JsonRpcResponse::Success(_) => Ok(()),
1142        }
1143    }
1144
1145    /// See [`crate::service::object::test::execute`] for more info
1146    #[test]
1147    fn change_admin_password() -> Result<()> {
1148        let expected = json!({
1149            "jsonrpc": "2.0",
1150            "method": "call",
1151            "id": 1000,
1152            "params": {
1153                "service": "db",
1154                "method": "change_admin_password",
1155                "args": [
1156                    "master-password",
1157                    "new-master-password",
1158                ]
1159            }
1160        });
1161        let actual = to_value(
1162            ChangeAdminPassword {
1163                passwd: "master-password".into(),
1164                new_passwd: "new-master-password".into(),
1165            }
1166            .build(1000),
1167        )?;
1168
1169        assert_eq!(actual, expected);
1170
1171        Ok(())
1172    }
1173
1174    /// See [`crate::service::object::test::execute_response`] for more info
1175    #[test]
1176    fn change_admin_password_response() -> Result<()> {
1177        let payload = json!({
1178            "jsonrpc": "2.0",
1179            "id": 1000,
1180            "result": true
1181        });
1182
1183        let response: JsonRpcResponse<ChangeAdminPasswordResponse> = from_value(payload)?;
1184        match response {
1185            JsonRpcResponse::Error(e) => Err(e.error.into()),
1186            JsonRpcResponse::Success(_) => Ok(()),
1187        }
1188    }
1189
1190    /// See [`crate::service::object::test::execute`] for more info
1191    #[test]
1192    fn migrate_databases() -> Result<()> {
1193        let expected = json!({
1194            "jsonrpc": "2.0",
1195            "method": "call",
1196            "id": 1000,
1197            "params": {
1198                "service": "db",
1199                "method": "migrate_databases",
1200                "args": [
1201                    "master-password",
1202                    [
1203                        "new-database",
1204                        "new-database2",
1205                    ]
1206                ]
1207            }
1208        });
1209        let actual = to_value(
1210            MigrateDatabases {
1211                passwd: "master-password".into(),
1212                databases: vec!["new-database".into(), "new-database2".into()],
1213            }
1214            .build(1000),
1215        )?;
1216
1217        assert_eq!(actual, expected);
1218
1219        Ok(())
1220    }
1221
1222    /// See [`crate::service::object::test::execute_response`] for more info
1223    #[test]
1224    fn migrate_databases_response() -> Result<()> {
1225        let payload = json!({
1226            "jsonrpc": "2.0",
1227            "id": 1000,
1228            "result": true
1229        });
1230
1231        let response: JsonRpcResponse<MigrateDatabasesResponse> = from_value(payload)?;
1232        match response {
1233            JsonRpcResponse::Error(e) => Err(e.error.into()),
1234            JsonRpcResponse::Success(_) => Ok(()),
1235        }
1236    }
1237
1238    /// See [`crate::service::object::test::execute`] for more info
1239    #[test]
1240    fn db_exist() -> Result<()> {
1241        let expected = json!({
1242            "jsonrpc": "2.0",
1243            "method": "call",
1244            "id": 1000,
1245            "params": {
1246                "service": "db",
1247                "method": "db_exist",
1248                "args": [
1249                    "new-database"
1250                ]
1251            }
1252        });
1253        let actual = to_value(
1254            DbExist {
1255                db_name: "new-database".into(),
1256            }
1257            .build(1000),
1258        )?;
1259
1260        assert_eq!(actual, expected);
1261
1262        Ok(())
1263    }
1264
1265    /// See [`crate::service::object::test::execute_response`] for more info
1266    #[test]
1267    fn db_exist_response() -> Result<()> {
1268        let payload = json!({
1269            "jsonrpc": "2.0",
1270            "id": 1000,
1271            "result": true
1272        });
1273
1274        let response: JsonRpcResponse<DbExistResponse> = from_value(payload)?;
1275        match response {
1276            JsonRpcResponse::Error(e) => Err(e.error.into()),
1277            JsonRpcResponse::Success(_) => Ok(()),
1278        }
1279    }
1280
1281    /// See [`crate::service::object::test::execute`] for more info
1282    #[test]
1283    fn list() -> Result<()> {
1284        let expected = json!({
1285            "jsonrpc": "2.0",
1286            "method": "call",
1287            "id": 1000,
1288            "params": {
1289                "service": "db",
1290                "method": "list",
1291                "args": [
1292                    false
1293                ]
1294            }
1295        });
1296        let actual = to_value(List { document: false }.build(1000))?;
1297
1298        assert_eq!(actual, expected);
1299
1300        Ok(())
1301    }
1302
1303    /// See [`crate::service::object::test::execute_response`] for more info
1304    #[test]
1305    fn list_response() -> Result<()> {
1306        let payload = json!({
1307            "jsonrpc": "2.0",
1308            "id": 1000,
1309            "result": [
1310                "old-database",
1311                "new-database",
1312                "new-database2"
1313            ]
1314        });
1315
1316        let response: JsonRpcResponse<ListResponse> = from_value(payload)?;
1317        match response {
1318            JsonRpcResponse::Error(e) => Err(e.error.into()),
1319            JsonRpcResponse::Success(_) => Ok(()),
1320        }
1321    }
1322
1323    /// See [`crate::service::object::test::execute`] for more info
1324    #[test]
1325    fn list_lang() -> Result<()> {
1326        let expected = json!({
1327            "jsonrpc": "2.0",
1328            "method": "call",
1329            "id": 1000,
1330            "params": {
1331                "service": "db",
1332                "method": "list_lang",
1333                "args": []
1334            }
1335        });
1336        let actual = to_value(ListLang {}.build(1000))?;
1337
1338        assert_eq!(actual, expected);
1339
1340        Ok(())
1341    }
1342
1343    /// See [`crate::service::object::test::execute_response`] for more info
1344    #[test]
1345    fn list_lang_response() -> Result<()> {
1346        let payload = json!({
1347            "jsonrpc": "2.0",
1348            "id": 1000,
1349            "result": [
1350                [
1351                    "sq_AL",
1352                    "Albanian / Shqip"
1353                ],
1354                [
1355                    "am_ET",
1356                    "Amharic / አምሃርኛ"
1357                ],
1358                [
1359                    "ar_SY",
1360                    "Arabic (Syria) / الْعَرَبيّة"
1361                ],
1362                // snipped for brevity
1363            ]
1364        });
1365
1366        let response: JsonRpcResponse<ListLangResponse> = from_value(payload)?;
1367        match response {
1368            JsonRpcResponse::Error(e) => Err(e.error.into()),
1369            JsonRpcResponse::Success(_) => Ok(()),
1370        }
1371    }
1372
1373    /// See [`crate::service::object::test::execute`] for more info
1374    #[test]
1375    fn list_countries() -> Result<()> {
1376        let expected = json!({
1377            "jsonrpc": "2.0",
1378            "method": "call",
1379            "id": 1000,
1380            "params": {
1381                "service": "db",
1382                "method": "list_countries",
1383                "args": [
1384                    "master-password"
1385                ]
1386            }
1387        });
1388        let actual = to_value(
1389            ListCountries {
1390                passwd: "master-password".into(),
1391            }
1392            .build(1000),
1393        )?;
1394
1395        assert_eq!(actual, expected);
1396
1397        Ok(())
1398    }
1399
1400    /// See [`crate::service::object::test::execute_response`] for more info
1401    #[test]
1402    fn list_countries_response() -> Result<()> {
1403        let payload = json!({
1404            "jsonrpc": "2.0",
1405            "id": 1000,
1406            "result": [
1407                [
1408                    "af",
1409                    "Afghanistan"
1410                ],
1411                [
1412                    "al",
1413                    "Albania"
1414                ],
1415                [
1416                    "dz",
1417                    "Algeria"
1418                ],
1419                // snipped for brevity
1420            ]
1421        });
1422
1423        let response: JsonRpcResponse<ListCountriesResponse> = from_value(payload)?;
1424        match response {
1425            JsonRpcResponse::Error(e) => Err(e.error.into()),
1426            JsonRpcResponse::Success(_) => Ok(()),
1427        }
1428    }
1429
1430    /// See [`crate::service::object::test::execute`] for more info
1431    #[test]
1432    fn server_version() -> Result<()> {
1433        let expected = json!({
1434            "jsonrpc": "2.0",
1435            "method": "call",
1436            "id": 1000,
1437            "params": {
1438                "service": "db",
1439                "method": "server_version",
1440                "args": []
1441            }
1442        });
1443        let actual = to_value(ServerVersion {}.build(1000))?;
1444
1445        assert_eq!(actual, expected);
1446
1447        Ok(())
1448    }
1449
1450    /// See [`crate::service::object::test::execute_response`] for more info
1451    #[test]
1452    fn server_version_response() -> Result<()> {
1453        let payload = json!({
1454            "jsonrpc": "2.0",
1455            "id": 1000,
1456            "result": "14.0+e"
1457        });
1458
1459        let response: JsonRpcResponse<ServerVersionResponse> = from_value(payload)?;
1460        match response {
1461            JsonRpcResponse::Error(e) => Err(e.error.into()),
1462            JsonRpcResponse::Success(_) => Ok(()),
1463        }
1464    }
1465}