├── .clippy.toml
├── .github
├── FUNDING.yml
└── workflows
│ └── ci.yaml
├── .gitignore
├── Cargo.toml
├── LICENSE-APACHE
├── LICENSE-MIT
├── README.md
├── odoo-api-macros
├── .gitignore
├── Cargo.toml
├── LICENSE-APACHE
├── LICENSE-MIT
├── README.md
└── src
│ ├── common.rs
│ ├── error.rs
│ ├── lib.rs
│ ├── odoo_api.rs
│ ├── odoo_orm.rs
│ ├── odoo_web.rs
│ └── serialize_tuple.rs
└── odoo-api
├── .gitignore
├── Cargo.toml
├── LICENSE-APACHE
├── LICENSE-MIT
├── README.md
├── README.tpl
└── src
├── client
├── error.rs
├── http_impl
│ ├── closure_async.rs
│ ├── closure_blocking.rs
│ ├── mod.rs
│ ├── reqwest_async.rs
│ └── reqwest_blocking.rs
├── mod.rs
├── odoo_client.rs
└── odoo_request.rs
├── jsonrpc
├── mod.rs
├── request.rs
├── request
│ ├── api.rs
│ ├── orm.rs
│ └── web.rs
└── response.rs
├── lib.rs
├── macros.rs
└── service
├── common.rs
├── db.rs
├── mod.rs
├── object.rs
├── orm.rs
└── web.rs
/.clippy.toml:
--------------------------------------------------------------------------------
1 | too-many-arguments-threshold = 9
2 |
--------------------------------------------------------------------------------
/.github/FUNDING.yml:
--------------------------------------------------------------------------------
1 | github: ryanc-me
2 |
--------------------------------------------------------------------------------
/.github/workflows/ci.yaml:
--------------------------------------------------------------------------------
1 | on:
2 | push:
3 | branches: ['master']
4 | pull_request:
5 | branches: ['master']
6 |
7 | name: Continuous integration
8 |
9 | jobs:
10 | check:
11 | name: Check
12 | runs-on: ubuntu-latest
13 | steps:
14 | - uses: actions/checkout@v2
15 | - uses: actions-rs/toolchain@v1
16 | with:
17 | profile: minimal
18 | toolchain: stable
19 | override: true
20 | - uses: actions-rs/cargo@v1
21 | with:
22 | command: check
23 |
24 | test:
25 | name: Test Suite
26 | runs-on: ubuntu-latest
27 | steps:
28 | - uses: actions/checkout@v2
29 | - uses: actions-rs/toolchain@v1
30 | with:
31 | profile: minimal
32 | toolchain: stable
33 | override: true
34 | - uses: actions-rs/cargo@v1
35 | with:
36 | command: test
37 | args: --features=async,blocking
38 |
39 | fmt:
40 | name: Rustfmt
41 | runs-on: ubuntu-latest
42 | steps:
43 | - uses: actions/checkout@v2
44 | - uses: actions-rs/toolchain@v1
45 | with:
46 | profile: minimal
47 | toolchain: stable
48 | override: true
49 | - run: rustup component add rustfmt
50 | - uses: actions-rs/cargo@v1
51 | with:
52 | command: fmt
53 | args: --all -- --check
54 |
55 | clippy:
56 | name: Clippy
57 | runs-on: ubuntu-latest
58 | steps:
59 | - uses: actions/checkout@v2
60 | - uses: actions-rs/toolchain@v1
61 | with:
62 | profile: minimal
63 | toolchain: stable
64 | override: true
65 | - run: rustup component add clippy
66 | - uses: actions-rs/cargo@v1
67 | with:
68 | command: clippy
69 | args: -- -D warnings
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | /target
2 | /Cargo.lock
3 |
--------------------------------------------------------------------------------
/Cargo.toml:
--------------------------------------------------------------------------------
1 | [workspace]
2 |
3 | members = [
4 | 'odoo-api-macros',
5 | 'odoo-api',
6 | ]
7 |
8 | [patch.crates-io]
9 | odoo-api-macros = { version = "0.2", path = "odoo-api-macros" }
10 |
--------------------------------------------------------------------------------
/LICENSE-APACHE:
--------------------------------------------------------------------------------
1 | Apache License
2 | Version 2.0, January 2004
3 | http://www.apache.org/licenses/
4 |
5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
6 |
7 | 1. Definitions.
8 |
9 | "License" shall mean the terms and conditions for use, reproduction,
10 | and distribution as defined by Sections 1 through 9 of this document.
11 |
12 | "Licensor" shall mean the copyright owner or entity authorized by
13 | the copyright owner that is granting the License.
14 |
15 | "Legal Entity" shall mean the union of the acting entity and all
16 | other entities that control, are controlled by, or are under common
17 | control with that entity. For the purposes of this definition,
18 | "control" means (i) the power, direct or indirect, to cause the
19 | direction or management of such entity, whether by contract or
20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the
21 | outstanding shares, or (iii) beneficial ownership of such entity.
22 |
23 | "You" (or "Your") shall mean an individual or Legal Entity
24 | exercising permissions granted by this License.
25 |
26 | "Source" form shall mean the preferred form for making modifications,
27 | including but not limited to software source code, documentation
28 | source, and configuration files.
29 |
30 | "Object" form shall mean any form resulting from mechanical
31 | transformation or translation of a Source form, including but
32 | not limited to compiled object code, generated documentation,
33 | and conversions to other media types.
34 |
35 | "Work" shall mean the work of authorship, whether in Source or
36 | Object form, made available under the License, as indicated by a
37 | copyright notice that is included in or attached to the work
38 | (an example is provided in the Appendix below).
39 |
40 | "Derivative Works" shall mean any work, whether in Source or Object
41 | form, that is based on (or derived from) the Work and for which the
42 | editorial revisions, annotations, elaborations, or other modifications
43 | represent, as a whole, an original work of authorship. For the purposes
44 | of this License, Derivative Works shall not include works that remain
45 | separable from, or merely link (or bind by name) to the interfaces of,
46 | the Work and Derivative Works thereof.
47 |
48 | "Contribution" shall mean any work of authorship, including
49 | the original version of the Work and any modifications or additions
50 | to that Work or Derivative Works thereof, that is intentionally
51 | submitted to Licensor for inclusion in the Work by the copyright owner
52 | or by an individual or Legal Entity authorized to submit on behalf of
53 | the copyright owner. For the purposes of this definition, "submitted"
54 | means any form of electronic, verbal, or written communication sent
55 | to the Licensor or its representatives, including but not limited to
56 | communication on electronic mailing lists, source code control systems,
57 | and issue tracking systems that are managed by, or on behalf of, the
58 | Licensor for the purpose of discussing and improving the Work, but
59 | excluding communication that is conspicuously marked or otherwise
60 | designated in writing by the copyright owner as "Not a Contribution."
61 |
62 | "Contributor" shall mean Licensor and any individual or Legal Entity
63 | on behalf of whom a Contribution has been received by Licensor and
64 | subsequently incorporated within the Work.
65 |
66 | 2. Grant of Copyright License. Subject to the terms and conditions of
67 | this License, each Contributor hereby grants to You a perpetual,
68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
69 | copyright license to reproduce, prepare Derivative Works of,
70 | publicly display, publicly perform, sublicense, and distribute the
71 | Work and such Derivative Works in Source or Object form.
72 |
73 | 3. Grant of Patent License. Subject to the terms and conditions of
74 | this License, each Contributor hereby grants to You a perpetual,
75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
76 | (except as stated in this section) patent license to make, have made,
77 | use, offer to sell, sell, import, and otherwise transfer the Work,
78 | where such license applies only to those patent claims licensable
79 | by such Contributor that are necessarily infringed by their
80 | Contribution(s) alone or by combination of their Contribution(s)
81 | with the Work to which such Contribution(s) was submitted. If You
82 | institute patent litigation against any entity (including a
83 | cross-claim or counterclaim in a lawsuit) alleging that the Work
84 | or a Contribution incorporated within the Work constitutes direct
85 | or contributory patent infringement, then any patent licenses
86 | granted to You under this License for that Work shall terminate
87 | as of the date such litigation is filed.
88 |
89 | 4. Redistribution. You may reproduce and distribute copies of the
90 | Work or Derivative Works thereof in any medium, with or without
91 | modifications, and in Source or Object form, provided that You
92 | meet the following conditions:
93 |
94 | (a) You must give any other recipients of the Work or
95 | Derivative Works a copy of this License; and
96 |
97 | (b) You must cause any modified files to carry prominent notices
98 | stating that You changed the files; and
99 |
100 | (c) You must retain, in the Source form of any Derivative Works
101 | that You distribute, all copyright, patent, trademark, and
102 | attribution notices from the Source form of the Work,
103 | excluding those notices that do not pertain to any part of
104 | the Derivative Works; and
105 |
106 | (d) If the Work includes a "NOTICE" text file as part of its
107 | distribution, then any Derivative Works that You distribute must
108 | include a readable copy of the attribution notices contained
109 | within such NOTICE file, excluding those notices that do not
110 | pertain to any part of the Derivative Works, in at least one
111 | of the following places: within a NOTICE text file distributed
112 | as part of the Derivative Works; within the Source form or
113 | documentation, if provided along with the Derivative Works; or,
114 | within a display generated by the Derivative Works, if and
115 | wherever such third-party notices normally appear. The contents
116 | of the NOTICE file are for informational purposes only and
117 | do not modify the License. You may add Your own attribution
118 | notices within Derivative Works that You distribute, alongside
119 | or as an addendum to the NOTICE text from the Work, provided
120 | that such additional attribution notices cannot be construed
121 | as modifying the License.
122 |
123 | You may add Your own copyright statement to Your modifications and
124 | may provide additional or different license terms and conditions
125 | for use, reproduction, or distribution of Your modifications, or
126 | for any such Derivative Works as a whole, provided Your use,
127 | reproduction, and distribution of the Work otherwise complies with
128 | the conditions stated in this License.
129 |
130 | 5. Submission of Contributions. Unless You explicitly state otherwise,
131 | any Contribution intentionally submitted for inclusion in the Work
132 | by You to the Licensor shall be under the terms and conditions of
133 | this License, without any additional terms or conditions.
134 | Notwithstanding the above, nothing herein shall supersede or modify
135 | the terms of any separate license agreement you may have executed
136 | with Licensor regarding such Contributions.
137 |
138 | 6. Trademarks. This License does not grant permission to use the trade
139 | names, trademarks, service marks, or product names of the Licensor,
140 | except as required for reasonable and customary use in describing the
141 | origin of the Work and reproducing the content of the NOTICE file.
142 |
143 | 7. Disclaimer of Warranty. Unless required by applicable law or
144 | agreed to in writing, Licensor provides the Work (and each
145 | Contributor provides its Contributions) on an "AS IS" BASIS,
146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
147 | implied, including, without limitation, any warranties or conditions
148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
149 | PARTICULAR PURPOSE. You are solely responsible for determining the
150 | appropriateness of using or redistributing the Work and assume any
151 | risks associated with Your exercise of permissions under this License.
152 |
153 | 8. Limitation of Liability. In no event and under no legal theory,
154 | whether in tort (including negligence), contract, or otherwise,
155 | unless required by applicable law (such as deliberate and grossly
156 | negligent acts) or agreed to in writing, shall any Contributor be
157 | liable to You for damages, including any direct, indirect, special,
158 | incidental, or consequential damages of any character arising as a
159 | result of this License or out of the use or inability to use the
160 | Work (including but not limited to damages for loss of goodwill,
161 | work stoppage, computer failure or malfunction, or any and all
162 | other commercial damages or losses), even if such Contributor
163 | has been advised of the possibility of such damages.
164 |
165 | 9. Accepting Warranty or Additional Liability. While redistributing
166 | the Work or Derivative Works thereof, You may choose to offer,
167 | and charge a fee for, acceptance of support, warranty, indemnity,
168 | or other liability obligations and/or rights consistent with this
169 | License. However, in accepting such obligations, You may act only
170 | on Your own behalf and on Your sole responsibility, not on behalf
171 | of any other Contributor, and only if You agree to indemnify,
172 | defend, and hold each Contributor harmless for any liability
173 | incurred by, or claims asserted against, such Contributor by reason
174 | of your accepting any such warranty or additional liability.
175 |
176 | END OF TERMS AND CONDITIONS
177 |
178 | APPENDIX: How to apply the Apache License to your work.
179 |
180 | To apply the Apache License to your work, attach the following
181 | boilerplate notice, with the fields enclosed by brackets "[]"
182 | replaced with your own identifying information. (Don't include
183 | the brackets!) The text should be enclosed in the appropriate
184 | comment syntax for the file format. We also recommend that a
185 | file or class name and description of purpose be included on the
186 | same "printed page" as the copyright notice for easier
187 | identification within third-party archives.
188 |
189 | Copyright [yyyy] [name of copyright owner]
190 |
191 | Licensed under the Apache License, Version 2.0 (the "License");
192 | you may not use this file except in compliance with the License.
193 | You may obtain a copy of the License at
194 |
195 | http://www.apache.org/licenses/LICENSE-2.0
196 |
197 | Unless required by applicable law or agreed to in writing, software
198 | distributed under the License is distributed on an "AS IS" BASIS,
199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
200 | See the License for the specific language governing permissions and
201 | limitations under the License.
202 |
--------------------------------------------------------------------------------
/LICENSE-MIT:
--------------------------------------------------------------------------------
1 | Permission is hereby granted, free of charge, to any
2 | person obtaining a copy of this software and associated
3 | documentation files (the "Software"), to deal in the
4 | Software without restriction, including without
5 | limitation the rights to use, copy, modify, merge,
6 | publish, distribute, sublicense, and/or sell copies of
7 | the Software, and to permit persons to whom the Software
8 | is furnished to do so, subject to the following
9 | conditions:
10 |
11 | The above copyright notice and this permission notice
12 | shall be included in all copies or substantial portions
13 | of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF
16 | ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED
17 | TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
18 | PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT
19 | SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
20 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR
22 | IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
23 | DEALINGS IN THE SOFTWARE.
24 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # odoo-api
2 |
3 | [
](https://github.com/ryanc-me/odoo-api-rs)
4 | [
](https://crates.io/crates/odoo-api)
5 | [
](https://docs.rs/odoo-api/)
6 | [
](https://github.com/ryanc-me/odoo-api-rs/actions?query=branch%3Amaster)
7 |
8 | The `odoo-api` odoo-api is a Rust library crate that provides a user-friendly interface
9 | to interact with the Odoo JSONRPC and ORM APIs, while preserving strong typing. It
10 | includes both async and blocking support out of the box, and allows users to provide
11 | their own implementations if needed.
12 |
13 | See the [Example](#example) section below for a brief example, or the [`client`](https://docs.rs/odoo-api/latest/odoo_api/client/index.html) module for more in-depth examples.
14 |
15 | ### Features
16 |
17 | - **Strong typing**: `odoo-api` prioritizes the use of concrete types wherever
18 | possible, rather than relying on generic `json!{}` calls.
19 | - **Async and blocking support**: the library provides both async and blocking
20 | HTTP impls via [`reqwest`](https://docs.rs/reqwest/latest/reqwest/), and allows users to easily provide their own HTTP
21 | impl via a shim closure.
22 | - **JSONRPC API support**: including database management (create, duplicate, etc),
23 | translations, and generic `execute` and `execute_kw`
24 | - **ORM API support**: including user-friendly APIs for the CRUD, `search_read`,
25 | security rule checking, and more
26 | - **Types-only**: allowing you to include this library for its types only. See
27 | [Types Only](#types-only) below for more info
28 |
29 | #### Supported API Methods
30 |
31 | See the [`service`](https://docs.rs/odoo-api/latest/odoo_api/service/index.html) module for a full list of supported API methods.
32 |
33 | #### Bring Your Own Requests
34 |
35 | Do you already have an HTTP library in your dependencies (e.g., `reqwest`)?
36 |
37 | The `odoo-api` crate allows you to use your existing HTTP library by writing a
38 | simple shim closure. See [`client::ClosureAsync`](https://docs.rs/odoo-api/latest/odoo_api/client/struct.ClosureAsync.html) or [`client::ClosureBlocking`](https://docs.rs/odoo-api/latest/odoo_api/client/struct.ClosureBlocking.html)
39 | for more info.
40 |
41 | #### Types Only
42 |
43 | The crate offers a `types-only` feature. When enabled, the library only exposes
44 | the API request & response types, along with `Serialize` and `Deserialize` impls.
45 | The async/blocking impls (and the `reqwest` dependency) are dropped when this
46 | feature is active.
47 |
48 | See the [`jsonrpc`](https://docs.rs/odoo-api/latest/odoo_api/jsonrpc/index.html) module for information on `types-only`.
49 |
50 | ### Example
51 |
52 |
53 | Add the following to your `Cargo.toml`:
54 |
55 | ```toml
56 | [dependencies]
57 | odoo_api = "0.2"
58 | ```
59 |
60 | Then make your requests:
61 | ```rust
62 | use odoo_api::{OdooClient, jvec, jmap};
63 |
64 | // build the client
65 | let url = "https://odoo.example.com";
66 | let mut client = OdooClient::new_reqwest_async(url)?;
67 |
68 | // authenticate with `some-database`
69 | let mut client = client.authenticate(
70 | "some-database",
71 | "admin",
72 | "password",
73 | ).await?;
74 |
75 | // fetch a list of users with the `execute` method
76 | let users = client.execute(
77 | "res.users",
78 | "search",
79 | jvec![
80 | [["active", "=", true], ["login", "!=", "__system__"]]
81 | ]
82 | ).send().await?;
83 |
84 | // fetch the login and partner_id fields from user id=1
85 | let info = client.execute_kw(
86 | "res.users",
87 | "read",
88 | jvec![[1]],
89 | jmap!{
90 | "fields": ["login", "partner_id"]
91 | }
92 | ).send().await?;
93 |
94 | // create 2 new partners with the `create` ORM method
95 | let partners = client.create(
96 | "res.partner",
97 | jvec![{
98 | "name": "Alice",
99 | "email": "alice@example.com",
100 | "phone": "555-555-5555",
101 | }, {
102 | "name": "Bob",
103 | "email": "bob@example.com",
104 | "phone": "555-555-5555",
105 | }]
106 | ).send().await?;
107 |
108 | // fetch a list of databases
109 | let databases = client.db_list(false).send().await?;
110 |
111 | // fetch server version info
112 | let version_info = client.common_version().send().await?;
113 | ```
114 |
--------------------------------------------------------------------------------
/odoo-api-macros/.gitignore:
--------------------------------------------------------------------------------
1 | /target
2 | /Cargo.lock
3 |
--------------------------------------------------------------------------------
/odoo-api-macros/Cargo.toml:
--------------------------------------------------------------------------------
1 | [package]
2 | name = "odoo-api-macros"
3 | version = "0.2.1"
4 | authors = ["Ryan Cole "]
5 | description = "Macros for the `odoo-api` crate"
6 | repository = "https://github.com/ryanc-me/odoo-api-rs"
7 | homepage = "https://github.com/ryanc-me/odoo-api-rs"
8 | documentation = "https://docs.rs/odoo-api-macros"
9 | include = ["src/**/*.rs", "README.md", "LICENSE-APACHE", "LICENSE-MIT"]
10 | categories = []
11 | keywords = []
12 | license = "MIT OR Apache-2.0"
13 | edition = "2021"
14 |
15 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
16 |
17 | [lib]
18 | proc-macro = true
19 | path = "src/lib.rs"
20 |
21 | [dependencies]
22 | serde_json = "1.0"
23 | serde = { version = "1.0", features = ["derive"] }
24 | syn = { version = "1.0", features = ["full", "parsing"] }
25 | quote = "1.0"
26 | proc-macro2 = "1.0"
27 |
--------------------------------------------------------------------------------
/odoo-api-macros/LICENSE-APACHE:
--------------------------------------------------------------------------------
1 | ../LICENSE-APACHE
--------------------------------------------------------------------------------
/odoo-api-macros/LICENSE-MIT:
--------------------------------------------------------------------------------
1 | ../LICENSE-MIT
--------------------------------------------------------------------------------
/odoo-api-macros/README.md:
--------------------------------------------------------------------------------
1 | # odoo-api-macros
2 |
3 | Helper macros for the `odoo_api` crate
4 |
5 | See the [`odoo_api`](https://crates.io/crates/odoo-api) crate.
6 |
--------------------------------------------------------------------------------
/odoo-api-macros/src/common.rs:
--------------------------------------------------------------------------------
1 | use crate::{Error, Result};
2 | use proc_macro::TokenStream;
3 | use proc_macro2::{Ident, Span, TokenStream as TokenStream2};
4 | use quote::{quote, quote_spanned};
5 | use syn::ext::IdentExt;
6 | use syn::{Fields, FieldsNamed, ItemStruct, Lit, LitStr, Meta, MetaNameValue, Token};
7 |
8 | /// Wrapper type that implements a custom [`syn::parse::Parse`]
9 | pub(crate) struct ItemStructNamed {
10 | pub item: ItemStruct,
11 | pub fields: FieldsNamed,
12 | pub doc_head: String,
13 | }
14 |
15 | impl syn::parse::Parse for ItemStructNamed {
16 | fn parse(input: syn::parse::ParseStream) -> syn::Result {
17 | let item = ItemStruct::parse(input);
18 | match &item {
19 | Ok(ItemStruct {
20 | fields: Fields::Named(fields),
21 | ..
22 | }) => {
23 | let item = item.clone().unwrap();
24 | let mut doc_head = String::new();
25 | for attr in &item.attrs {
26 | if let Meta::NameValue(MetaNameValue {
27 | path,
28 | lit: Lit::Str(lit),
29 | ..
30 | }) = attr.parse_meta()?
31 | {
32 | if let Some(segment) = path.segments.first() {
33 | if segment.ident == "doc" {
34 | doc_head = lit.value();
35 | break;
36 | }
37 | }
38 | }
39 | }
40 |
41 | Ok(Self {
42 | item,
43 | fields: fields.clone(),
44 | doc_head,
45 | })
46 | }
47 | _ => Err(syn::Error::new(
48 | Span::mixed_site(),
49 | "This macro must be applied to a struct with named fields",
50 | )),
51 | }
52 | }
53 | }
54 |
55 | impl quote::ToTokens for ItemStructNamed {
56 | fn to_tokens(&self, tokens: &mut TokenStream2) {
57 | self.item.to_tokens(tokens)
58 | }
59 | }
60 |
61 | /// Helper to parse [`crate::Result`] into [`TokenStream`]
62 | pub(crate) fn parse_result(result: Result) -> TokenStream {
63 | match result {
64 | Ok(ts) => ts,
65 | Err(err) => match err {
66 | Error::TokenStream(ts) => ts,
67 | Error::MacroError(s) => {
68 | let message = s.0;
69 | let span = s.1;
70 | match span {
71 | Some(span) => quote_spanned! {span=> compile_error!(#message);},
72 | None => quote! {compile_error!(#message);},
73 | }
74 | }
75 | },
76 | }
77 | .into()
78 | }
79 |
80 | /// Custom arguments type
81 | ///
82 | /// This type implements [`syn::parse::Parse`], and will convert the macro
83 | /// arguments into a format useable by this crate. Note that we can't use
84 | /// [`syn::AttributeArgs`] because that types' parse impl doesn't support
85 | /// arrays (e.g. `test = ["list", "of", "literals"]). We also don't need
86 | /// to support paths as arg keys (for now), so the returned struct can be
87 | /// simpler and easier to work with on the macro impl side
88 | pub(crate) struct MacroArguments {
89 | inner: Vec,
90 | }
91 |
92 | pub(crate) struct Arg {
93 | pub(crate) key: String,
94 | pub(crate) span: Span,
95 | pub(crate) value: ArgValue,
96 | }
97 |
98 | pub(crate) enum ArgValue {
99 | Lit(Lit),
100 | Array(Vec),
101 | }
102 |
103 | impl IntoIterator for MacroArguments {
104 | type IntoIter = as IntoIterator>::IntoIter;
105 | type Item = Arg;
106 |
107 | fn into_iter(self) -> Self::IntoIter {
108 | self.inner.into_iter()
109 | }
110 | }
111 |
112 | impl TryFrom for String {
113 | type Error = Error;
114 | fn try_from(value: ArgValue) -> std::result::Result {
115 | match value {
116 | ArgValue::Lit(Lit::Str(lit)) => Ok(lit.value()),
117 | _ => Err("expected LitStr, got something else".into()),
118 | }
119 | }
120 | }
121 | impl TryFrom for bool {
122 | type Error = Error;
123 | fn try_from(value: ArgValue) -> std::result::Result {
124 | match value {
125 | ArgValue::Lit(Lit::Bool(lit)) => Ok(lit.value()),
126 | _ => Err("expected LitBool, got something else".into()),
127 | }
128 | }
129 | }
130 | impl TryFrom for Vec {
131 | type Error = Error;
132 | fn try_from(value: ArgValue) -> std::result::Result, Self::Error> {
133 | match value {
134 | ArgValue::Array(val) => Ok(val),
135 | _ => Err("expected LitBool, got something else".into()),
136 | }
137 | }
138 | }
139 |
140 | impl syn::parse::Parse for MacroArguments {
141 | fn parse(input: syn::parse::ParseStream) -> syn::Result {
142 | let mut inner = Vec::new();
143 |
144 | while input.peek(Ident::peek_any) {
145 | inner.push(input.parse()?);
146 | if input.peek(Token![,]) {
147 | input.parse::()?;
148 | }
149 | }
150 |
151 | Ok(Self { inner })
152 | }
153 | }
154 |
155 | impl syn::parse::Parse for Arg {
156 | fn parse(input: syn::parse::ParseStream) -> syn::Result {
157 | let key = input.parse::()?;
158 | let span = key.span();
159 | let key = key.to_string();
160 | input.parse::()?;
161 | let value = input.parse()?;
162 | Ok(Self { key, span, value })
163 | }
164 | }
165 |
166 | impl syn::parse::Parse for ArgValue {
167 | fn parse(input: syn::parse::ParseStream) -> syn::Result {
168 | if input.peek(syn::token::Bracket) {
169 | // the grouped `path = ["group", "of", "literals"]` format
170 | let content;
171 | let mut values = Vec::new();
172 | syn::bracketed!(content in input);
173 | while content.peek(Lit) {
174 | values.push(content.parse::()?.value());
175 | if content.peek(Token![,]) {
176 | content.parse::()?;
177 | }
178 | }
179 |
180 | Ok(ArgValue::Array(values))
181 | } else if input.peek(Lit) {
182 | // standard `path = "literal"` format
183 | input.parse().map(ArgValue::Lit)
184 | } else {
185 | Err(input.error("expected identifier or literal"))
186 | }
187 | }
188 | }
189 |
--------------------------------------------------------------------------------
/odoo-api-macros/src/error.rs:
--------------------------------------------------------------------------------
1 | use proc_macro2::{Span, TokenStream};
2 |
3 | #[derive(Debug)]
4 | pub(crate) enum Error {
5 | /// An error generated internally by the odoo attribute/derive macro functions
6 | MacroError((String, Option)),
7 |
8 | /// A generic TokenStream "error"
9 | TokenStream(TokenStream),
10 | }
11 |
12 | impl From for Error {
13 | fn from(ts: TokenStream) -> Self {
14 | Self::TokenStream(ts)
15 | }
16 | }
17 |
18 | impl From for Error {
19 | fn from(s: String) -> Self {
20 | Self::MacroError((s, None))
21 | }
22 | }
23 |
24 | impl From<(String, Option)> for Error {
25 | fn from(value: (String, Option)) -> Self {
26 | Self::MacroError(value)
27 | }
28 | }
29 |
30 | impl From<&str> for Error {
31 | fn from(s: &str) -> Self {
32 | s.to_string().into()
33 | }
34 | }
35 |
36 | impl From<(&str, Option)> for Error {
37 | fn from(value: (&str, Option)) -> Self {
38 | (value.0.to_string(), value.1).into()
39 | }
40 | }
41 |
42 | pub(crate) type Result = std::result::Result;
43 |
--------------------------------------------------------------------------------
/odoo-api-macros/src/lib.rs:
--------------------------------------------------------------------------------
1 | //! Helper macros for the `odoo_api` crate
2 | //!
3 | //! See the [`odoo_api`](https://crates.io/crates/odoo-api) crate.
4 |
5 | use proc_macro::TokenStream;
6 |
7 | mod common;
8 | mod error;
9 | mod odoo_api;
10 | mod odoo_orm;
11 | mod odoo_web;
12 | mod serialize_tuple;
13 |
14 | use common::parse_result;
15 | use error::{Error, Result};
16 | use syn::parse_macro_input;
17 |
18 | /// Implement traits for an "API" method struct
19 | ///
20 | /// The input should be a struct with named field. Zero-sized structs and generics
21 | /// are supported.
22 | ///
23 | /// Arguments:
24 | /// - Service: The Odoo "service" for this method
25 | /// - Method: The method name
26 | /// - Auth: Whether authentication is required, optional, or ignored
27 | ///
28 | /// For example, consider the following:
29 | /// ```ignore
30 | /// // service: "object"
31 | /// // method: "execute"
32 | /// // auth: "yes"
33 | /// #[derive(Debug, Serialize)]
34 | /// #[odoo_api("object", "execute", "yes")]
35 | /// struct Execute {
36 | /// database: String,
37 | /// uid: OdooId,
38 | /// password: String,
39 | ///
40 | /// model: String,
41 | /// method: String,
42 | /// args: Vec
43 | /// }
44 | /// ```
45 | ///
46 | /// Then the following impls will be generated:
47 | /// ```ignore
48 | /// // The `Execute` is able to be used as JSON-RPC `params`
49 | /// impl JsonRpcParams for Execute {
50 | /// // Set the container; in this case, the container will use `describe` below
51 | /// // to implement a custom Serialize
52 | /// type Container = OdooApiContainer;
53 | /// type Response = ExecuteResponse;
54 | ///
55 | /// fn build(self) -> JsonRpcRequest { self._build() }
56 | /// }
57 | ///
58 | /// // Which JSON-RPC service and method does `Execute` belong to?
59 | /// impl OdooApiMethod for Execute {
60 | /// fn describe(&self) -> (&'static str, &'static str) {
61 | /// ("object", "execute")
62 | /// }
63 | /// }
64 | ///
65 | /// // Implement a method for this struct, so users can write `client.execute(...)`
66 | /// // Note that we specified "yes" for auth, so this impl is bound to `Authed`
67 | /// // clients only
68 | /// impl OdooClient {
69 | /// pub fn execute(&self, model: &str, method: &str, args: Vec) -> OdooRequest {
70 | /// let execute = Execute {
71 | /// // Auth info is pulled from the Client
72 | /// database: self.auth.database.clone(),
73 | /// uid: self.auth.uid,
74 | /// password: self.auth.password.clone(),
75 | ///
76 | /// // Strings are passed as &str then converted to Strings
77 | /// model: model.into(),
78 | /// method: method.into(),
79 | /// args
80 | /// };
81 | ///
82 | /// // Finally, build the request
83 | /// self.build_request(
84 | /// execute,
85 | /// &self.url_jsonrpc
86 | /// )
87 | /// }
88 | /// }
89 | /// ```
90 | #[proc_macro_attribute]
91 | pub fn odoo_api(args: TokenStream, input: TokenStream) -> TokenStream {
92 | let args = parse_macro_input!(args);
93 | let input = parse_macro_input!(input);
94 |
95 | parse_result(odoo_api::odoo_api(args, input))
96 | }
97 |
98 | #[proc_macro_attribute]
99 | pub fn odoo_web(args: TokenStream, input: TokenStream) -> TokenStream {
100 | let args = parse_macro_input!(args);
101 | let input = parse_macro_input!(input);
102 |
103 | parse_result(odoo_web::odoo_web(args, input))
104 | }
105 |
106 | #[proc_macro_attribute]
107 | pub fn odoo_orm(args: TokenStream, input: TokenStream) -> TokenStream {
108 | let args = parse_macro_input!(args);
109 | let input = parse_macro_input!(input);
110 |
111 | parse_result(odoo_orm::odoo_orm(args, input))
112 | }
113 |
114 | #[proc_macro_derive(SerializeTuple)]
115 | pub fn serialize_tuple(input: TokenStream) -> TokenStream {
116 | parse_result(serialize_tuple::serialize_tuple(input))
117 | }
118 |
--------------------------------------------------------------------------------
/odoo-api-macros/src/odoo_api.rs:
--------------------------------------------------------------------------------
1 | use proc_macro2::{Span, TokenStream as TokenStream2};
2 | use quote::{quote, ToTokens};
3 | use syn::{FieldsNamed, Ident, Type};
4 |
5 | use crate::common::{ItemStructNamed, MacroArguments};
6 | use crate::{Error, Result};
7 |
8 | struct OdooApiArgs {
9 | /// The JSON-RPC "service"
10 | service: String,
11 |
12 | /// The JSON-RPC "method"
13 | method: String,
14 |
15 | /// Is authentication required, optional, should we skip generating the
16 | /// OdooClient impl?
17 | auth: Option,
18 |
19 | /// Optionally specify a name for the OdooClient impl
20 | name: Option,
21 | }
22 |
23 | impl TryFrom for OdooApiArgs {
24 | type Error = Error;
25 |
26 | fn try_from(value: MacroArguments) -> Result {
27 | let mut service = None;
28 | let mut method = None;
29 | let mut auth = None;
30 | let mut name = None;
31 |
32 | for arg in value.into_iter() {
33 | match (arg.key.as_str(), arg.value, arg.span) {
34 | ("service", val, span) => {
35 | service = Some(val.try_into().map_err(|_| {
36 | (
37 | "invalid value, expected String (e.g., `service = \"object\"`)",
38 | Some(span),
39 | )
40 | })?);
41 | }
42 | ("method", val, span) => {
43 | method = Some(val.try_into().map_err(|_| {
44 | (
45 | "invalid value, expected String (e.g., `method = \"execute_kw\"`)",
46 | Some(span),
47 | )
48 | })?);
49 | }
50 | ("auth", val, span) => {
51 | auth = Some(val.try_into().map_err(|_| {
52 | (
53 | "invalid value, expected String (e.g., `auth = false`)",
54 | Some(span),
55 | )
56 | })?);
57 | }
58 | ("name", val, span) => {
59 | name = Some(val.try_into().map_err(|_| {
60 | (
61 | "invalid value, expected String (e.g., `name = \"my_execute_kw\"`)",
62 | Some(span),
63 | )
64 | })?);
65 | }
66 |
67 | (key, _val, span) => Err((
68 | format!(
69 | "Invalid argument `{}`. Valid arguments are: service, method, auth, name",
70 | key
71 | ),
72 | Some(span),
73 | ))?,
74 | }
75 | }
76 |
77 | Ok(Self {
78 | service: service
79 | .ok_or("The \"service\" key is required (e.g., `service = \"object\"`)")?,
80 | method: method
81 | .ok_or("The \"method\" key is required (e.g., `method = \"execute\"`)")?,
82 | auth,
83 | name,
84 | })
85 | }
86 | }
87 |
88 | pub(crate) fn odoo_api(args: MacroArguments, input: ItemStructNamed) -> Result {
89 | let args: OdooApiArgs = args.try_into()?;
90 |
91 | // fetch the struct name (and some variations)
92 | let name_struct = input.item.ident.to_string();
93 | let name_response = format!("{}Response", &name_struct);
94 | let name_call = if let Some(name) = &args.name {
95 | name.clone()
96 | } else {
97 | args.method.clone()
98 | };
99 | let ident_struct = input.item.ident.clone();
100 | let ident_response = Ident::new(&name_response, Span::call_site());
101 | let ident_call = Ident::new(&name_call, Span::call_site());
102 |
103 | // build a quick doc-comment directing users from the function impl,
104 | // back to the struct (where we have examples/etc)
105 | let doc_call = format!(
106 | "{}\n\nSee [`{}`](crate::service::{}::{}) for more info.",
107 | &input.doc_head, &name_struct, &args.service, &name_struct
108 | );
109 |
110 | // build the TokenStreams
111 | let out_params = impl_params(&ident_struct, &ident_response)?;
112 | let out_method = impl_method(&ident_struct, &args)?;
113 | let out_client = impl_client(&ident_struct, &ident_call, &args, &input.fields, &doc_call)?;
114 |
115 | // output the result!
116 | Ok(quote!(
117 | #input
118 | #out_params
119 | #out_method
120 | #out_client
121 | ))
122 | }
123 |
124 | /// Output the [`JsonRpcParams`](odoo_api::jsonrpc::JsonRpcParams) impl
125 | fn impl_params(ident_struct: &Ident, ident_response: &Ident) -> Result {
126 | Ok(quote! {
127 | impl odoo_api::jsonrpc::JsonRpcParams for #ident_struct {
128 | type Container = odoo_api::jsonrpc::OdooApiContainer ;
129 | type Response = #ident_response;
130 |
131 | fn build(self, id: odoo_api::jsonrpc::JsonRpcId) -> odoo_api::jsonrpc::JsonRpcRequest { self._build(id) }
132 | }
133 | })
134 | }
135 |
136 | /// Output the OdooApiMethod impl
137 | fn impl_method(ident_struct: &Ident, args: &OdooApiArgs) -> Result {
138 | let service = &args.service;
139 | let method = &args.method;
140 | Ok(quote! {
141 | impl odoo_api::jsonrpc::OdooApiMethod for #ident_struct {
142 | fn describe(&self) -> (&'static str, &'static str) {
143 | (#service, #method)
144 | }
145 | fn endpoint(&self) -> &'static str {
146 | "/jsonrpc"
147 | }
148 | }
149 | })
150 | }
151 |
152 | /// Output the OdooClient impl
153 | fn impl_client(
154 | ident_struct: &Ident,
155 | ident_call: &Ident,
156 | args: &OdooApiArgs,
157 | fields: &FieldsNamed,
158 | doc: &str,
159 | ) -> Result {
160 | if args.auth.is_none() {
161 | // The `auth` key wasn't passed, so we'll just skip the OdooClient impl
162 | return Ok(quote!());
163 | }
164 |
165 | let auth = args.auth.unwrap();
166 |
167 | // parse the `auth` argument options
168 | let (auth_generic, auth_type) = if auth {
169 | // no generic, we're implementing for the concrete `Authed` type
170 | (quote!(), quote!(odoo_api::client::Authed))
171 | } else {
172 | // auth not required, so we'll implement for any `impl AuthState`
173 | (quote!(S: odoo_api::client::AuthState), quote!(S))
174 | };
175 |
176 | // parse fields
177 | let mut field_assigns = Vec::new();
178 | let mut field_arguments = Vec::new();
179 | for field in fields.named.clone() {
180 | let ident = field.ident.unwrap();
181 | let ty = if let Type::Path(path) = field.ty {
182 | path
183 | } else {
184 | continue;
185 | };
186 | let name = ident.to_string();
187 | let path = ty.clone().into_token_stream().to_string();
188 | match (name.as_str(), path.as_str(), auth) {
189 | // special cases (data fetched from the `client.auth` struct)
190 | ("database", "String", true) => {
191 | field_assigns.push(quote!(database: self.auth.database.clone()));
192 | }
193 | ("db", "String", true) => {
194 | field_assigns.push(quote!(db: self.auth.database.clone()));
195 | }
196 | ("uid", "OdooId", true) => {
197 | field_assigns.push(quote!(uid: self.auth.uid));
198 | }
199 | ("login", "String", true) => {
200 | field_assigns.push(quote!(login: self.auth.login.clone()));
201 | }
202 | ("password", "String", true) => {
203 | field_assigns.push(quote!(password: self.auth.password.clone()));
204 | }
205 |
206 | // strings are passed by ref
207 | //TODO: Into more suitable?
208 | (_, "String", _) => {
209 | field_assigns.push(quote!(#ident: #ident.into()));
210 | field_arguments.push(quote!(#ident: &str));
211 | }
212 |
213 | // all other fields are passed as-is
214 | (_, _, _) => {
215 | field_assigns.push(quote!(#ident: #ident));
216 | field_arguments.push(quote!(#ident: #ty));
217 | }
218 | }
219 | }
220 |
221 | Ok(quote! {
222 | #[cfg(not(feature = "types-only"))]
223 | impl odoo_api::client::OdooClient<#auth_type, I> {
224 | #[doc=#doc]
225 | pub fn #ident_call(&mut self, #(#field_arguments),*) -> odoo_api::client::OdooRequest< #ident_struct , I> {
226 | let #ident_call = #ident_struct {
227 | #(#field_assigns),*
228 | };
229 |
230 | let endpoint = self.build_endpoint(#ident_call.endpoint());
231 | self.build_request(
232 | #ident_call,
233 | &endpoint
234 | )
235 | }
236 | }
237 | })
238 | }
239 |
--------------------------------------------------------------------------------
/odoo-api-macros/src/odoo_orm.rs:
--------------------------------------------------------------------------------
1 | use proc_macro2::{Span, TokenStream as TokenStream2};
2 | use quote::{quote, ToTokens};
3 | use syn::{FieldsNamed, Ident, Type};
4 |
5 | use crate::common::{ItemStructNamed, MacroArguments};
6 | use crate::{Error, Result};
7 |
8 | #[derive(Debug)]
9 | struct OdooOrmArgs {
10 | /// The ORM "method" (e.g. 'write', 'read_group', etc)
11 | method: String,
12 |
13 | /// Optionally specify a name for the OdooClient impl
14 | name: Option,
15 |
16 | /// A list of the positional arguments
17 | args: Vec,
18 |
19 | /// A list of the keyword arguments
20 | kwargs: Vec,
21 | }
22 |
23 | impl TryFrom for OdooOrmArgs {
24 | type Error = Error;
25 |
26 | fn try_from(value: MacroArguments) -> Result {
27 | // let from_args = parse_args(value)?;
28 |
29 | let mut method = None;
30 | let mut name = None;
31 | let mut args = None;
32 | let mut kwargs = None;
33 |
34 | for arg in value.into_iter() {
35 | match (arg.key.as_str(), arg.value, arg.span) {
36 | ("method", val, span) => {
37 | method = Some(val.try_into().map_err(|_| {
38 | (
39 | "invalid value, expected String (e.g., `method = \"read\"`)",
40 | Some(span),
41 | )
42 | })?);
43 | }
44 | ("name", val, span) => {
45 | name = Some(val.try_into().map_err(|_| {
46 | (
47 | "invalid value, expected String (e.g., `name = \"my_custom_read\"`)",
48 | Some(span),
49 | )
50 | })?);
51 | }
52 | ("args", val, span) => {
53 | args = Some(val.try_into().map_err(|_| (
54 | "invalid value, expected String (e.g., `args = [\"list\", \"of\", \"literals\"]`)",
55 | Some(span)
56 | ))?);
57 | }
58 | ("kwargs", val, span) => {
59 | kwargs = Some(val.try_into().map_err(|_| (
60 | "invalid value, expected String (e.g., `kwargs = [\"list\", \"of\", \"literals\"]`)",
61 | Some(span)
62 | ))?);
63 | }
64 |
65 | (key, _val, span) => Err((
66 | format!(
67 | "Invalid argument `{}`. Valid arguments are: method, name, args, kwargs",
68 | key
69 | ),
70 | Some(span),
71 | ))?,
72 | }
73 | }
74 |
75 | Ok(Self {
76 | method: method.ok_or(
77 | "The \"method\" key is required (e.g., `method = \"read_group\"`)",
78 | )?,
79 | name,
80 | args: args.ok_or(
81 | "The \"args\" key is required, even if you only pass an empty array (e.g., `args = []`)"
82 | )?,
83 | kwargs: kwargs.ok_or(
84 | "The \"kwargs\" key is required, even if you only pass an empty array (e.g., `kwargs = []`)"
85 | )?,
86 | })
87 | }
88 | }
89 |
90 | pub(crate) fn odoo_orm(args: MacroArguments, input: ItemStructNamed) -> Result {
91 | let args: OdooOrmArgs = args.try_into()?;
92 |
93 | // fetch the struct name (and some variations)
94 | let name_struct = input.item.ident.to_string();
95 | let name_response = format!("{}Response", &name_struct);
96 | let name_call = if let Some(name) = &args.name {
97 | name.clone()
98 | } else {
99 | args.method.clone()
100 | };
101 | let ident_struct = input.item.ident.clone();
102 | let ident_response = Ident::new(&name_response, Span::call_site());
103 | let ident_call = Ident::new(&name_call, Span::call_site());
104 |
105 | // build a quick doc-comment directing users from the function impl,
106 | // back to the struct (where we have examples/etc)
107 | let doc_call = format!(
108 | "{}\n\nSee [`{}`](crate::service::orm::{}) for more info.",
109 | &input.doc_head, &name_struct, &name_struct
110 | );
111 |
112 | // build the TokenStreams
113 | let out_params = impl_params(&ident_struct, &ident_response)?;
114 | let out_method = impl_method(&ident_struct, &args)?;
115 | let out_client = impl_client(&ident_struct, &ident_call, &input.fields, &doc_call)?;
116 | let out_serialize = impl_serialize(&ident_struct, &args)?;
117 |
118 | // output the result!
119 | Ok(quote!(
120 | #input
121 | #out_params
122 | #out_method
123 | #out_client
124 | #out_serialize
125 | ))
126 | }
127 |
128 | /// Output the [`JsonRpcParams`](odoo_api::jsonrpc::JsonRpcParams) impl
129 | pub(crate) fn impl_params(ident_struct: &Ident, ident_response: &Ident) -> Result {
130 | Ok(quote! {
131 | impl odoo_api::jsonrpc::JsonRpcParams for #ident_struct {
132 | type Container = odoo_api::jsonrpc::OdooOrmContainer ;
133 | type Response = #ident_response;
134 |
135 | fn build(self, id: odoo_api::jsonrpc::JsonRpcId) -> odoo_api::jsonrpc::JsonRpcRequest { self._build(id) }
136 | }
137 | })
138 | }
139 |
140 | /// Output the OdooApiMethod impl
141 | fn impl_method(ident_struct: &Ident, args: &OdooOrmArgs) -> Result {
142 | let method = &args.method;
143 | Ok(quote! {
144 | impl odoo_api::jsonrpc::OdooOrmMethod for #ident_struct {
145 | fn endpoint(&self) -> &'static str {
146 | "/jsonrpc"
147 | }
148 |
149 | fn method(&self) -> &'static str {
150 | #method
151 | }
152 | }
153 | })
154 | }
155 |
156 | /// Output the OdooClient impl
157 | fn impl_client(
158 | ident_struct: &Ident,
159 | ident_call: &Ident,
160 | fields: &FieldsNamed,
161 | doc: &str,
162 | ) -> Result {
163 | // parse the `auth` argument options
164 | let auth_generic = quote!();
165 | let auth_type = quote!(odoo_api::client::Authed);
166 |
167 | // parse fields
168 | let mut field_assigns = Vec::new();
169 | let mut field_arguments = Vec::new();
170 | let mut field_generics = Vec::new();
171 | for field in fields.named.clone() {
172 | let ident = field.ident.unwrap();
173 | let ty = if let Type::Path(path) = field.ty {
174 | path
175 | } else {
176 | continue;
177 | };
178 | let name = ident.to_string();
179 | let path = ty.clone().into_token_stream().to_string();
180 | match (name.as_str(), path.as_str()) {
181 | // special cases (data fetched from the `client.auth` struct)
182 | ("database", "String") => {
183 | field_assigns.push(quote!(database: self.auth.database.clone()));
184 | }
185 | ("db", "String") => {
186 | field_assigns.push(quote!(db: self.auth.database.clone()));
187 | }
188 | ("uid", "OdooId") => {
189 | field_assigns.push(quote!(uid: self.auth.uid));
190 | }
191 | ("login", "String") => {
192 | field_assigns.push(quote!(login: self.auth.login.clone()));
193 | }
194 | ("password", "String") => {
195 | field_assigns.push(quote!(password: self.auth.password.clone()));
196 | }
197 |
198 | // strings are passed by ref
199 | //TODO: Into would be more performant in some cases
200 | (_, "String") => {
201 | field_assigns.push(quote!(#ident: #ident.into()));
202 | field_arguments.push(quote!(#ident: &str));
203 | }
204 |
205 | (_, "OdooIds") => {
206 | field_generics.push(quote!(ID: Into));
207 | field_assigns.push(quote!(#ident: #ident.into()));
208 | field_arguments.push(quote!(#ident: ID));
209 | }
210 |
211 | (_, "CreateVals") => {
212 | field_generics.push(quote!(V: Into));
213 | field_assigns.push(quote!(#ident: #ident.into()));
214 | field_arguments.push(quote!(#ident: V));
215 | }
216 |
217 | // all other fields are passed as-is
218 | (_, _) => {
219 | field_assigns.push(quote!(#ident: #ident));
220 | field_arguments.push(quote!(#ident: #ty));
221 | }
222 | }
223 | }
224 |
225 | Ok(quote! {
226 | #[cfg(not(feature = "types-only"))]
227 | #[doc=#doc]
228 | impl odoo_api::client::OdooClient<#auth_type, I> {
229 | pub fn #ident_call<#(#field_generics),*>(&mut self, #(#field_arguments),*) -> odoo_api::client::OdooRequest< #ident_struct , I> {
230 | let #ident_call = #ident_struct {
231 | #(#field_assigns),*
232 | };
233 |
234 | let endpoint = self.build_endpoint(#ident_call.endpoint());
235 | self.build_request(
236 | #ident_call,
237 | &endpoint
238 | )
239 | }
240 | }
241 | })
242 | }
243 |
244 | fn impl_serialize(ident_struct: &Ident, args: &OdooOrmArgs) -> Result {
245 | let ident_args: Vec = args
246 | .args
247 | .iter()
248 | .map(|x| Ident::new(x, Span::call_site()))
249 | .collect();
250 | let lit_kwargs = args.kwargs.clone();
251 | let ident_kwargs: Vec = args
252 | .kwargs
253 | .iter()
254 | .map(|x| Ident::new(x, Span::call_site()))
255 | .collect();
256 | Ok(quote!(
257 | impl serde::Serialize for #ident_struct {
258 | fn serialize(&self, serialize: S) -> ::std::result::Result
259 | where
260 | S: serde::Serializer
261 | {
262 | let mut state = serialize.serialize_tuple(5)?;
263 | state.serialize_element(&self.database)?;
264 | state.serialize_element(&self.uid)?;
265 | state.serialize_element(&self.password)?;
266 | state.serialize_element(&self.model)?;
267 | state.serialize_element(self.method())?;
268 |
269 | //TODO: serialize these directly (serialize.clone() ?)
270 | state.serialize_element(&(
271 | ::serde_json::json!([
272 | #(&self.#ident_args),*
273 | ])
274 | ))?;
275 |
276 | //TODO: serialize these directly (serialize.clone() ?)
277 | state.serialize_element(&(
278 | ::serde_json::json!({
279 | #(#lit_kwargs : &self.#ident_kwargs),*
280 | })
281 | ))?;
282 |
283 | state.end()
284 | }
285 | }
286 | ))
287 | }
288 |
--------------------------------------------------------------------------------
/odoo-api-macros/src/odoo_web.rs:
--------------------------------------------------------------------------------
1 | use proc_macro2::{Span, TokenStream as TokenStream2};
2 | use quote::{quote, ToTokens};
3 | use syn::{FieldsNamed, Ident, Type};
4 |
5 | use crate::common::{ItemStructNamed, MacroArguments};
6 | use crate::{Error, Result};
7 |
8 | struct OdooWebArgs {
9 | /// The API endpoint (e.g. '/web/session/authenticate')
10 | path: String,
11 |
12 | /// A (required) name for the service (e.g. 'web_session_authenticate');
13 | /// this maps to the function implemented on OdooClient, (e.g. `client.web_session_authenticate()`)
14 | name: String,
15 |
16 | /// Is authentication required, optional, should we skip generating the
17 | /// OdooClient impl?
18 | auth: Option,
19 | }
20 |
21 | impl TryFrom for OdooWebArgs {
22 | type Error = Error;
23 |
24 | fn try_from(value: MacroArguments) -> Result {
25 | let mut path = None;
26 | let mut name = None;
27 | let mut auth = None;
28 |
29 | for arg in value.into_iter() {
30 | match (arg.key.as_str(), arg.value, arg.span) {
31 | ("path", val, span) => {
32 | path = Some(val.try_into().map_err(|_| (
33 | "invalid value, expected String (e.g., `path = \"/web/session/authenticate\"`)",
34 | Some(span)
35 | ))?);
36 | }
37 | ("auth", val, span) => {
38 | auth = Some(val.try_into().map_err(|_| {
39 | (
40 | "invalid value, expected String (e.g., `auth = false`)",
41 | Some(span),
42 | )
43 | })?);
44 | }
45 | ("name", val, span) => {
46 | name = Some(val.try_into().map_err(|_| {
47 | (
48 | "invalid value, expected String (e.g., `name = \"my_execute_kw\"`)",
49 | Some(span),
50 | )
51 | })?);
52 | }
53 |
54 | (key, _val, span) => Err((
55 | format!(
56 | "Invalid argument `{}`. Valid arguments are: path, name, auth",
57 | key
58 | ),
59 | Some(span),
60 | ))?,
61 | }
62 | }
63 |
64 | Ok(Self {
65 | path: path.ok_or(
66 | "The \"path\" key is required (e.g., `path = \"/web/session/authenticate\"`)",
67 | )?,
68 | name: name
69 | .ok_or("The \"name\" key is required (e.g., `name = \"session_authenticate\"`)")?,
70 | auth,
71 | })
72 | }
73 | }
74 |
75 | pub(crate) fn odoo_web(args: MacroArguments, input: ItemStructNamed) -> Result {
76 | let args: OdooWebArgs = args.try_into()?;
77 |
78 | // fetch the struct name (and some variations)
79 | let name_struct = input.item.ident.to_string();
80 | let name_response = format!("{}Response", &name_struct);
81 | let name_call = args.name.clone();
82 | let ident_struct = input.item.ident.clone();
83 | let ident_response = Ident::new(&name_response, Span::call_site());
84 | let ident_call = Ident::new(&name_call, Span::call_site());
85 |
86 | // build a quick doc-comment directing users from the function impl,
87 | // back to the struct (where we have examples/etc)
88 | let doc_call = format!(
89 | "{}\n\nSee [`{}`](crate::service::web::{}) for more info.",
90 | &input.doc_head, &name_struct, &name_struct
91 | );
92 |
93 | // build the TokenStreams
94 | let out_params = impl_params(&ident_struct, &ident_response)?;
95 | let out_method = impl_method(&ident_struct, &args)?;
96 | let out_client = impl_client(&ident_struct, &ident_call, &args, &input.fields, &doc_call)?;
97 |
98 | // output the result!
99 | Ok(quote!(
100 | #input
101 | #out_params
102 | #out_method
103 | #out_client
104 | ))
105 | }
106 |
107 | /// Output the [`JsonRpcParams`](odoo_api::jsonrpc::JsonRpcParams) impl
108 | pub(crate) fn impl_params(ident_struct: &Ident, ident_response: &Ident) -> Result {
109 | Ok(quote! {
110 | impl odoo_api::jsonrpc::JsonRpcParams for #ident_struct {
111 | type Container = odoo_api::jsonrpc::OdooWebContainer ;
112 | type Response = #ident_response;
113 |
114 | fn build(self, id: odoo_api::jsonrpc::JsonRpcId) -> odoo_api::jsonrpc::JsonRpcRequest { self._build(id) }
115 | }
116 | })
117 | }
118 |
119 | /// Output the OdooApiMethod impl
120 | fn impl_method(ident_struct: &Ident, args: &OdooWebArgs) -> Result {
121 | let path = &args.path;
122 | Ok(quote! {
123 | impl odoo_api::jsonrpc::OdooWebMethod for #ident_struct {
124 | fn endpoint(&self) -> &'static str {
125 | #path
126 | }
127 | }
128 | })
129 | }
130 |
131 | /// Output the OdooClient impl
132 | fn impl_client(
133 | ident_struct: &Ident,
134 | ident_call: &Ident,
135 | args: &OdooWebArgs,
136 | fields: &FieldsNamed,
137 | doc: &str,
138 | ) -> Result {
139 | if args.auth.is_none() {
140 | // The `auth` key wasn't passed, so we'll just skip the OdooClient impl
141 | return Ok(quote!());
142 | }
143 |
144 | let auth = args.auth.unwrap();
145 |
146 | // parse the `auth` argument options
147 | let (auth_generic, auth_type) = if auth {
148 | // no generic, we're implementing for the concrete `Authed` type
149 | (quote!(), quote!(odoo_api::client::Authed))
150 | } else {
151 | // auth not required, so we'll implement for any `impl AuthState`
152 | (quote!(S: odoo_api::client::AuthState), quote!(S))
153 | };
154 |
155 | // parse fields
156 | let mut field_assigns = Vec::new();
157 | let mut field_arguments = Vec::new();
158 | for field in fields.named.clone() {
159 | let ident = field.ident.unwrap();
160 | let ty = if let Type::Path(path) = field.ty {
161 | path
162 | } else {
163 | continue;
164 | };
165 | let name = ident.to_string();
166 | let path = ty.clone().into_token_stream().to_string();
167 | match (name.as_str(), path.as_str(), auth) {
168 | // special cases (data fetched from the `client.auth` struct)
169 | ("database", "String", true) => {
170 | field_assigns.push(quote!(database: self.auth.database.clone()));
171 | }
172 | ("db", "String", true) => {
173 | field_assigns.push(quote!(db: self.auth.database.clone()));
174 | }
175 | ("uid", "OdooId", true) => {
176 | field_assigns.push(quote!(uid: self.auth.uid));
177 | }
178 | ("login", "String", true) => {
179 | field_assigns.push(quote!(login: self.auth.login.clone()));
180 | }
181 | ("password", "String", true) => {
182 | field_assigns.push(quote!(password: self.auth.password.clone()));
183 | }
184 |
185 | // strings are passed by ref
186 | //TODO: Into would be more performant in some cases
187 | (_, "String", _) => {
188 | field_assigns.push(quote!(#ident: #ident.into()));
189 | field_arguments.push(quote!(#ident: &str));
190 | }
191 |
192 | // all other fields are passed as-is
193 | (_, _, _) => {
194 | field_assigns.push(quote!(#ident: #ident));
195 | field_arguments.push(quote!(#ident: #ty));
196 | }
197 | }
198 | }
199 |
200 | Ok(quote! {
201 | #[cfg(not(feature = "types-only"))]
202 | #[doc=#doc]
203 | impl odoo_api::client::OdooClient<#auth_type, I> {
204 | pub fn #ident_call(&mut self, #(#field_arguments),*) -> odoo_api::client::OdooRequest< #ident_struct , I> {
205 | let #ident_call = #ident_struct {
206 | #(#field_assigns),*
207 | };
208 |
209 | let endpoint = self.build_endpoint(#ident_call.endpoint());
210 | self.build_request(
211 | #ident_call,
212 | &endpoint
213 | )
214 | }
215 | }
216 | })
217 | }
218 |
--------------------------------------------------------------------------------
/odoo-api-macros/src/serialize_tuple.rs:
--------------------------------------------------------------------------------
1 | use proc_macro::TokenStream;
2 | use proc_macro2::TokenStream as TokenStream2;
3 |
4 | use crate::Result;
5 |
6 | //TODO
7 | pub(crate) fn serialize_tuple(input: TokenStream) -> Result {
8 | Ok(input.into())
9 | }
10 |
--------------------------------------------------------------------------------
/odoo-api/.gitignore:
--------------------------------------------------------------------------------
1 | /target
2 | /Cargo.lock
3 |
--------------------------------------------------------------------------------
/odoo-api/Cargo.toml:
--------------------------------------------------------------------------------
1 | [package]
2 | name = "odoo-api"
3 | version = "0.2.6"
4 | authors = ["Ryan Cole "]
5 | description = "Type-safe and full-coverage implementation of the Odoo JSON-RPC API, including ORM and Web methods. Supports sessioning, multi-database, async and blocking via reqwest, and bring-your-own requests."
6 | repository = "https://github.com/ryanc-me/odoo-api-rs"
7 | homepage = "https://github.com/ryanc-me/odoo-api-rs"
8 | documentation = "https://docs.rs/odoo-api"
9 | include = ["src/**/*.rs", "README.md", "LICENSE-APACHE", "LICENSE-MIT"]
10 | categories = ["api-bindings", ]
11 | keywords = ["odoo", "jsonrpc", "json-rpc", "api"]
12 | license = "MIT OR Apache-2.0"
13 | edition = "2021"
14 |
15 | [dependencies]
16 | serde = { version = "1.0", features = ["derive"] }
17 | serde_tuple = "0.5.0"
18 | serde_json = "1.0"
19 | thiserror = "1.0"
20 | rand = { version = "0.8.5" }
21 | reqwest = { version = "0.11", features = ["json", "cookies"], optional = true }
22 | odoo-api-macros = "0.2.1"
23 |
24 | [features]
25 | # By default, only reqwest async support is included
26 | default = ["async"]
27 |
28 | # Include async Reqwest support
29 | async = ["reqwest"]
30 |
31 | # Include blocking Reqwest support
32 | blocking = ["reqwest", "reqwest/blocking"]
33 |
34 | # Disable the "OdooClient" implementation. This is useful if you *only* need the
35 | # API method types
36 | types-only = []
37 |
38 | [package.metadata.docs.rs]
39 | features = ["async", "blocking"]
40 | targets = ["x86_64-unknown-linux-gnu"]
41 | rustdoc-args = ["--cfg", "doc_cfg"]
42 |
--------------------------------------------------------------------------------
/odoo-api/LICENSE-APACHE:
--------------------------------------------------------------------------------
1 | ../LICENSE-APACHE
--------------------------------------------------------------------------------
/odoo-api/LICENSE-MIT:
--------------------------------------------------------------------------------
1 | ../LICENSE-MIT
--------------------------------------------------------------------------------
/odoo-api/README.md:
--------------------------------------------------------------------------------
1 | # odoo-api
2 |
3 | [
](https://github.com/ryanc-me/odoo-api-rs)
4 | [
](https://crates.io/crates/odoo-api)
5 | [
](https://docs.rs/odoo-api/)
6 | [
](https://github.com/ryanc-me/odoo-api-rs/actions?query=branch%3Amaster)
7 |
8 | The `odoo-api` odoo-api is a Rust library crate that provides a user-friendly interface
9 | to interact with the Odoo JSONRPC and ORM APIs, while preserving strong typing. It
10 | includes both async and blocking support out of the box, and allows users to provide
11 | their own implementations if needed.
12 |
13 | See the [Example](#example) section below for a brief example, or the [`client`](https://docs.rs/odoo-api/latest/odoo_api/client/index.html) module for more in-depth examples.
14 |
15 | ### Features
16 |
17 | - **Strong typing**: `odoo-api` prioritizes the use of concrete types wherever
18 | possible, rather than relying on generic `json!{}` calls.
19 | - **Async and blocking support**: the library provides both async and blocking
20 | HTTP impls via [`reqwest`](https://docs.rs/reqwest/latest/reqwest/), and allows users to easily provide their own HTTP
21 | impl via a shim closure.
22 | - **JSONRPC API support**: including database management (create, duplicate, etc),
23 | translations, and generic `execute` and `execute_kw`
24 | - **ORM API support**: including user-friendly APIs for the CRUD, `search_read`,
25 | security rule checking, and more
26 | - **Types-only**: allowing you to include this library for its types only. See
27 | [Types Only](#types-only) below for more info
28 |
29 | #### Supported API Methods
30 |
31 | See the [`service`](https://docs.rs/odoo-api/latest/odoo_api/service/index.html) module for a full list of supported API methods.
32 |
33 | #### Bring Your Own Requests
34 |
35 | Do you already have an HTTP library in your dependencies (e.g., `reqwest`)?
36 |
37 | The `odoo-api` crate allows you to use your existing HTTP library by writing a
38 | simple shim closure. See [`client::ClosureAsync`](https://docs.rs/odoo-api/latest/odoo_api/client/struct.ClosureAsync.html) or [`client::ClosureBlocking`](https://docs.rs/odoo-api/latest/odoo_api/client/struct.ClosureBlocking.html)
39 | for more info.
40 |
41 | #### Types Only
42 |
43 | The crate offers a `types-only` feature. When enabled, the library only exposes
44 | the API request & response types, along with `Serialize` and `Deserialize` impls.
45 | The async/blocking impls (and the `reqwest` dependency) are dropped when this
46 | feature is active.
47 |
48 | See the [`jsonrpc`](https://docs.rs/odoo-api/latest/odoo_api/jsonrpc/index.html) module for information on `types-only`.
49 |
50 | ### Example
51 |
52 |
53 | Add the following to your `Cargo.toml`:
54 |
55 | ```toml
56 | [dependencies]
57 | odoo_api = "0.2"
58 | ```
59 |
60 | Then make your requests:
61 | ```rust
62 | use odoo_api::{OdooClient, jvec, jmap};
63 |
64 | // build the client
65 | let url = "https://odoo.example.com";
66 | let mut client = OdooClient::new_reqwest_async(url)?;
67 |
68 | // authenticate with `some-database`
69 | let mut client = client.authenticate(
70 | "some-database",
71 | "admin",
72 | "password",
73 | ).await?;
74 |
75 | // fetch a list of users with the `execute` method
76 | let users = client.execute(
77 | "res.users",
78 | "search",
79 | jvec![
80 | [["active", "=", true], ["login", "!=", "__system__"]]
81 | ]
82 | ).send().await?;
83 |
84 | // fetch the login and partner_id fields from user id=1
85 | let info = client.execute_kw(
86 | "res.users",
87 | "read",
88 | jvec![[1]],
89 | jmap!{
90 | "fields": ["login", "partner_id"]
91 | }
92 | ).send().await?;
93 |
94 | // create 2 new partners with the `create` ORM method
95 | let partners = client.create(
96 | "res.partner",
97 | jvec![{
98 | "name": "Alice",
99 | "email": "alice@example.com",
100 | "phone": "555-555-5555",
101 | }, {
102 | "name": "Bob",
103 | "email": "bob@example.com",
104 | "phone": "555-555-5555",
105 | }]
106 | ).send().await?;
107 |
108 | // fetch a list of databases
109 | let databases = client.db_list(false).send().await?;
110 |
111 | // fetch server version info
112 | let version_info = client.common_version().send().await?;
113 | ```
114 |
--------------------------------------------------------------------------------
/odoo-api/README.tpl:
--------------------------------------------------------------------------------
1 | # {{crate}}
2 |
3 | [
](https://github.com/ryanc-me/odoo-api-rs)
4 | [
](https://crates.io/crates/odoo-api)
5 | [
](https://docs.rs/odoo-api/)
6 | [
](https://github.com/ryanc-me/odoo-api-rs/actions?query=branch%3Amaster)
7 |
8 | {{readme}}
9 |
--------------------------------------------------------------------------------
/odoo-api/src/client/error.rs:
--------------------------------------------------------------------------------
1 | use crate::jsonrpc::response::JsonRpcError;
2 | use thiserror::Error;
3 |
4 | /// An error during the response parsing phase
5 | ///
6 | /// This error is used internally, and is typically parsed into either a
7 | /// [`ClosureError`] or a [`ReqwestError`].
8 | #[derive(Debug, Error)]
9 | pub enum ParseResponseError {
10 | /// A parsing error from the serde_json library
11 | ///
12 | /// This might be raised if the returned JSON data is invalid, or couldn't
13 | /// be parsed into the `XxxResponse` struct properly.
14 | #[error(transparent)]
15 | SerdeJsonError(#[from] serde_json::Error),
16 |
17 | /// The Odoo API request was not successful
18 | ///
19 | /// See [`JsonRpcError`] for more details
20 | #[error("JSON-RPC Error")]
21 | JsonRpcError(#[from] JsonRpcError),
22 | }
23 |
24 | pub type ParseResponseResult = std::result::Result;
25 |
26 | #[derive(Debug, Error)]
27 | pub enum AuthenticationError {
28 | /// A parsing error from the serde_json library
29 | ///
30 | /// This might be raised if the returned JSON data is invalid, or couldn't
31 | /// be parsed into the `XxxResponse` struct properly.
32 | #[error(transparent)]
33 | SerdeJsonError(#[from] serde_json::Error),
34 |
35 | /// An error occured while parsing the `uid` field from the authenticate
36 | /// response
37 | #[error("UID Parser Error")]
38 | UidParseError(String),
39 | }
40 |
41 | pub type AuthenticationResult = std::result::Result;
42 |
43 | /// An error sending a closure-based [`OdooRequest`](crate::client::OdooRequest)
44 | ///
45 | ///
46 | #[derive(Debug, Error)]
47 | pub enum ClosureError {
48 | /// An error occured inside the custom closure
49 | ///
50 | /// We include a blanket from Box\ here because the concrete error
51 | /// type cannot be known here (i.e., only the crate *consumer* will know the
52 | /// type). This allows `fallible()?` to correctly return the ClosureError type
53 | #[error(transparent)]
54 | ClosureError(#[from] Box),
55 |
56 | /// A parsing error from the serde_json library
57 | ///
58 | /// This might be raised if the returned JSON data is invalid, or couldn't
59 | /// be parsed into the `XxxResponse` struct properly.
60 | #[error(transparent)]
61 | SerdeJsonError(#[from] serde_json::Error),
62 |
63 | /// The Odoo API request was not successful
64 | ///
65 | /// See [`JsonRpcError`] for more details
66 | #[error("JSON-RPC Error")]
67 | JsonRpcError(#[from] JsonRpcError),
68 | }
69 |
70 | // This is nicer than having a `ParseError` variant on the `ClosureError` struct
71 | // (which would duplicate these fields anyways)
72 | impl From for ClosureError {
73 | fn from(value: ParseResponseError) -> Self {
74 | match value {
75 | ParseResponseError::JsonRpcError(err) => Self::JsonRpcError(err),
76 | ParseResponseError::SerdeJsonError(err) => Self::SerdeJsonError(err),
77 | }
78 | }
79 | }
80 |
81 | pub type ClosureResult = std::result::Result;
82 |
83 | /// An error during the `authenticate()` call
84 | #[derive(Debug, Error)]
85 | pub enum ClosureAuthError {
86 | /// An error occured during the serialization, sending, receiving, or deserialization
87 | /// of the request
88 | #[error(transparent)]
89 | ClosureError(#[from] ClosureError),
90 |
91 | /// An error occured while parsing the `uid` field from the authenticate
92 | /// response
93 | #[error("UID Parser Error")]
94 | UidParseError(String),
95 | }
96 |
97 | // As with `From`, we'd like to avoid having duplicate error fields
98 | impl From for ClosureAuthError {
99 | fn from(value: AuthenticationError) -> Self {
100 | match value {
101 | AuthenticationError::SerdeJsonError(err) => {
102 | Self::ClosureError(ClosureError::SerdeJsonError(err))
103 | }
104 | AuthenticationError::UidParseError(err) => Self::UidParseError(err),
105 | }
106 | }
107 | }
108 |
109 | pub type ClosureAuthResult = std::result::Result;
110 |
111 | #[derive(Debug, Error)]
112 | pub enum ReqwestError {
113 | /// An error from the [`reqwest`] library
114 | ///
115 | /// See [`reqwest::Error`] for more information.
116 | #[cfg(any(feature = "async", feature = "blocking"))]
117 | #[error(transparent)]
118 | ReqwestError(#[from] reqwest::Error),
119 |
120 | /// A parsing error from the serde_json library
121 | ///
122 | /// This might be raised if the returned JSON data is invalid, or couldn't
123 | /// be parsed into the `XxxResponse` struct properly.
124 | #[error(transparent)]
125 | SerdeJsonError(#[from] serde_json::Error),
126 |
127 | /// The Odoo API request was not successful
128 | ///
129 | /// See [`JsonRpcError`] for more details
130 | #[error("JSON-RPC Error")]
131 | JsonRpcError(#[from] JsonRpcError),
132 | }
133 |
134 | impl From for ReqwestError {
135 | fn from(value: ParseResponseError) -> Self {
136 | match value {
137 | ParseResponseError::JsonRpcError(err) => Self::JsonRpcError(err),
138 | ParseResponseError::SerdeJsonError(err) => Self::SerdeJsonError(err),
139 | }
140 | }
141 | }
142 |
143 | pub type ReqwestResult = std::result::Result;
144 |
145 | #[derive(Debug, Error)]
146 | pub enum ReqwestAuthError {
147 | #[error(transparent)]
148 | ReqwestError(#[from] ReqwestError),
149 |
150 | /// An error occured while parsing the `uid` field from the authenticate
151 | /// response
152 | #[error("UID Parser Error")]
153 | UidParseError(String),
154 | }
155 |
156 | // As with `From`, we'd like to avoid having duplicate error fields
157 | impl From for ReqwestAuthError {
158 | fn from(value: AuthenticationError) -> Self {
159 | match value {
160 | AuthenticationError::SerdeJsonError(err) => {
161 | Self::ReqwestError(ReqwestError::SerdeJsonError(err))
162 | }
163 | AuthenticationError::UidParseError(err) => Self::UidParseError(err),
164 | }
165 | }
166 | }
167 |
168 | pub type ReqwestAuthResult = std::result::Result;
169 |
170 | #[derive(Debug, Error)]
171 | pub enum Error {
172 | /// An error occured inside the custom closure
173 | ///
174 | /// We include a blanket from Box\ here because the concrete error
175 | /// type cannot be known here (i.e., only the crate *consumer* will know the
176 | /// type). This allows `fallible()?` to correctly return the ClosureError type
177 | #[error(transparent)]
178 | ClosureError(#[from] Box),
179 |
180 | /// An error from the [`reqwest`] library
181 | ///
182 | /// See [`reqwest::Error`] for more information.
183 | #[cfg(any(feature = "async", feature = "blocking"))]
184 | #[error(transparent)]
185 | ReqwestError(#[from] reqwest::Error),
186 |
187 | /// A parsing error from the serde_json library
188 | ///
189 | /// This might be raised if the returned JSON data is invalid, or couldn't
190 | /// be parsed into the `XxxResponse` struct properly.
191 | #[error(transparent)]
192 | SerdeJsonError(#[from] serde_json::Error),
193 |
194 | /// The Odoo API request was not successful
195 | ///
196 | /// See [`JsonRpcError`] for more details
197 | #[error("JSON-RPC Error")]
198 | JsonRpcError(#[from] JsonRpcError),
199 |
200 | /// An error occured while parsing the `uid` field from the authenticate
201 | /// response
202 | #[error("UID Parser Error")]
203 | UidParseError(String),
204 | }
205 |
206 | // This is nicer than having a `ParseError` variant on the `ClosureError` struct
207 | // (which would duplicate these fields anyways)
208 | impl From for Error {
209 | fn from(value: ParseResponseError) -> Self {
210 | match value {
211 | ParseResponseError::JsonRpcError(err) => Self::JsonRpcError(err),
212 | ParseResponseError::SerdeJsonError(err) => Self::SerdeJsonError(err),
213 | }
214 | }
215 | }
216 |
217 | // As with `From`, we'd like to avoid having duplicate error fields
218 | impl From for Error {
219 | fn from(value: AuthenticationError) -> Self {
220 | match value {
221 | AuthenticationError::SerdeJsonError(err) => Self::SerdeJsonError(err),
222 | AuthenticationError::UidParseError(err) => Self::UidParseError(err),
223 | }
224 | }
225 | }
226 |
227 | impl From for Error {
228 | fn from(value: ClosureError) -> Self {
229 | match value {
230 | ClosureError::ClosureError(err) => Self::ClosureError(err),
231 | ClosureError::JsonRpcError(err) => Self::JsonRpcError(err),
232 | ClosureError::SerdeJsonError(err) => Self::SerdeJsonError(err),
233 | }
234 | }
235 | }
236 |
237 | impl From for Error {
238 | fn from(value: ClosureAuthError) -> Self {
239 | match value {
240 | ClosureAuthError::ClosureError(err) => err.into(),
241 | ClosureAuthError::UidParseError(err) => Self::UidParseError(err),
242 | }
243 | }
244 | }
245 |
246 | impl From for Error {
247 | fn from(value: ReqwestError) -> Self {
248 | match value {
249 | ReqwestError::ReqwestError(err) => Self::ReqwestError(err),
250 | ReqwestError::JsonRpcError(err) => Self::JsonRpcError(err),
251 | ReqwestError::SerdeJsonError(err) => Self::SerdeJsonError(err),
252 | }
253 | }
254 | }
255 |
256 | impl From for Error {
257 | fn from(value: ReqwestAuthError) -> Self {
258 | match value {
259 | ReqwestAuthError::ReqwestError(err) => err.into(),
260 | ReqwestAuthError::UidParseError(err) => Self::UidParseError(err),
261 | }
262 | }
263 | }
264 |
265 | pub type Result = std::result::Result;
266 |
--------------------------------------------------------------------------------
/odoo-api/src/client/http_impl/closure_async.rs:
--------------------------------------------------------------------------------
1 | use crate::client::error::{ClosureAuthResult, ClosureError, ClosureResult};
2 | use crate::client::{AuthState, Authed, NotAuthed, OdooClient, OdooRequest, RequestImpl};
3 | use crate::jsonrpc::JsonRpcParams;
4 | use serde::Serialize;
5 | use serde_json::{to_value, Value};
6 | use std::fmt::Debug;
7 | use std::future::Future;
8 | use std::pin::Pin;
9 |
10 | /// Convenience typedef. Use this as the return value for your async closure
11 | pub type ClosureReturn = Pin)>>>>;
12 | type Closure = Box) -> ClosureReturn>;
13 |
14 | /// **TODO:** Add an example closure for `reqwest` (and some other libs - `hyper`?)
15 | pub struct ClosureAsync {
16 | closure: Closure,
17 | }
18 | impl RequestImpl for ClosureAsync {
19 | type Error = ClosureError;
20 | }
21 |
22 | impl OdooClient {
23 | pub fn new_closure_async(
24 | url: &str,
25 | closure: impl 'static
26 | + Fn(
27 | String,
28 | Value,
29 | Option,
30 | )
31 | -> Pin)>>>>,
32 | ) -> Self {
33 | Self::new(
34 | url,
35 | ClosureAsync {
36 | closure: Box::new(closure),
37 | },
38 | )
39 | }
40 | }
41 |
42 | impl OdooClient
43 | where
44 | S: AuthState,
45 | {
46 | pub async fn authenticate(
47 | mut self,
48 | db: &str,
49 | login: &str,
50 | password: &str,
51 | ) -> ClosureAuthResult> {
52 | let request = self.get_auth_request(db, login, password);
53 | let (response, session_id) = request.send_internal().await?;
54 | Ok(self.parse_auth_response(db, login, password, response, session_id)?)
55 | }
56 | }
57 |
58 | impl<'a, T> OdooRequest<'a, T, ClosureAsync>
59 | where
60 | T: JsonRpcParams + Debug + Serialize,
61 | T::Container: Debug + Serialize,
62 | {
63 | pub async fn send(self) -> ClosureResult {
64 | Ok(self.send_internal().await?.0)
65 | }
66 |
67 | async fn send_internal(self) -> ClosureResult<(T::Response, Option)> {
68 | let data = to_value(&self.data)?;
69 | let (response, session_id) = (self._impl.closure)(
70 | self.url.clone(),
71 | data,
72 | self.session_id.map(|s| s.to_string()),
73 | )
74 | .await?;
75 | Ok((self.parse_response(&response)?, session_id))
76 | }
77 | }
78 |
--------------------------------------------------------------------------------
/odoo-api/src/client/http_impl/closure_blocking.rs:
--------------------------------------------------------------------------------
1 | use crate::client::error::{ClosureAuthResult, ClosureError, ClosureResult};
2 | use crate::client::{AuthState, Authed, NotAuthed, OdooClient, OdooRequest, RequestImpl};
3 | use crate::jsonrpc::JsonRpcParams;
4 | use serde::Serialize;
5 | use serde_json::{to_value, Value};
6 | use std::fmt::Debug;
7 |
8 | /// Convenience typedef. Use this as the return value for your blocking closure
9 | pub type ClosureReturn = ClosureResult<(String, Option)>;
10 | type Closure = Box) -> ClosureReturn>;
11 |
12 | /// **TODO:** Add an example closure for `reqwest` (and some other libs - `hyper`?)
13 | pub struct ClosureBlocking {
14 | closure: Closure,
15 | }
16 | impl RequestImpl for ClosureBlocking {
17 | type Error = ClosureError;
18 | }
19 |
20 | impl OdooClient {
21 | pub fn new_closure_blocking<
22 | F: Fn(&str, Value, Option<&str>) -> ClosureResult<(String, Option)> + 'static,
23 | >(
24 | url: &str,
25 | closure: F,
26 | ) -> Self {
27 | Self::new(
28 | url,
29 | ClosureBlocking {
30 | closure: Box::new(closure),
31 | },
32 | )
33 | }
34 | }
35 |
36 | impl OdooClient
37 | where
38 | S: AuthState,
39 | {
40 | pub fn authenticate(
41 | mut self,
42 | db: &str,
43 | login: &str,
44 | password: &str,
45 | ) -> ClosureAuthResult> {
46 | let request = self.get_auth_request(db, login, password);
47 | let (response, session_id) = request.send_internal()?;
48 | Ok(self.parse_auth_response(db, login, password, response, session_id)?)
49 | }
50 | }
51 |
52 | impl<'a, T> OdooRequest<'a, T, ClosureBlocking>
53 | where
54 | T: JsonRpcParams + Debug + Serialize,
55 | T::Container: Debug + Serialize,
56 | {
57 | pub fn send(self) -> ClosureResult {
58 | Ok(self.send_internal()?.0)
59 | }
60 |
61 | fn send_internal(self) -> ClosureResult<(T::Response, Option)> {
62 | let data = to_value(&self.data)?;
63 | let (response, session_id) = self._impl.closure.as_ref()(&self.url, data, self.session_id)?;
64 | Ok((self.parse_response(&response)?, session_id))
65 | }
66 | }
67 |
--------------------------------------------------------------------------------
/odoo-api/src/client/http_impl/mod.rs:
--------------------------------------------------------------------------------
1 | pub(crate) mod closure_async;
2 | pub(crate) mod closure_blocking;
3 |
4 | #[cfg(feature = "async")]
5 | pub(crate) mod reqwest_async;
6 |
7 | #[cfg(feature = "blocking")]
8 | pub(crate) mod reqwest_blocking;
9 |
--------------------------------------------------------------------------------
/odoo-api/src/client/http_impl/reqwest_async.rs:
--------------------------------------------------------------------------------
1 | use crate::client::error::{ReqwestAuthResult, ReqwestError, ReqwestResult};
2 | use crate::client::{AuthState, Authed, NotAuthed, OdooClient, OdooRequest, RequestImpl};
3 | use crate::jsonrpc::JsonRpcParams;
4 | use reqwest::Client;
5 | use serde::Serialize;
6 | use std::fmt::Debug;
7 |
8 | pub struct ReqwestAsync {
9 | client: Client,
10 | }
11 | impl RequestImpl for ReqwestAsync {
12 | type Error = ReqwestError;
13 | }
14 |
15 | impl OdooClient {
16 | pub fn new_reqwest_async(url: &str) -> Result {
17 | let client = Client::builder().cookie_store(true).build()?;
18 |
19 | Ok(Self::new(url, ReqwestAsync { client }))
20 | }
21 | }
22 |
23 | impl OdooClient
24 | where
25 | S: AuthState,
26 | {
27 | pub async fn authenticate(
28 | mut self,
29 | db: &str,
30 | login: &str,
31 | password: &str,
32 | ) -> ReqwestAuthResult> {
33 | let request = self.get_auth_request(db, login, password);
34 | let (response, session_id) = request.send_internal().await?;
35 | Ok(self.parse_auth_response(db, login, password, response, session_id)?)
36 | }
37 | }
38 |
39 | impl<'a, T> OdooRequest<'a, T, ReqwestAsync>
40 | where
41 | T: JsonRpcParams + Debug + Serialize,
42 | T::Container: Debug + Serialize,
43 | {
44 | pub async fn send(self) -> ReqwestResult {
45 | Ok(self.send_internal().await?.0)
46 | }
47 |
48 | async fn send_internal(self) -> ReqwestResult<(T::Response, Option)> {
49 | let request = self._impl.client.post(&self.url).json(&self.data);
50 | let response = request.send().await?;
51 | Ok((self.parse_response(&response.text().await?)?, None))
52 | }
53 | }
54 |
--------------------------------------------------------------------------------
/odoo-api/src/client/http_impl/reqwest_blocking.rs:
--------------------------------------------------------------------------------
1 | use crate::client::error::{ReqwestAuthResult, ReqwestError, ReqwestResult};
2 | use crate::client::{AuthState, Authed, NotAuthed, OdooClient, OdooRequest, RequestImpl};
3 | use crate::jsonrpc::JsonRpcParams;
4 | use reqwest::blocking::Client;
5 | use serde::Serialize;
6 | use std::fmt::Debug;
7 |
8 | pub struct ReqwestBlocking {
9 | client: Client,
10 | }
11 | impl RequestImpl for ReqwestBlocking {
12 | type Error = ReqwestError;
13 | }
14 |
15 | impl OdooClient {
16 | pub fn new_reqwest_blocking(url: &str) -> Result {
17 | let client = Client::builder().cookie_store(true).build()?;
18 |
19 | Ok(Self::new(url, ReqwestBlocking { client }))
20 | }
21 | }
22 |
23 | impl OdooClient
24 | where
25 | S: AuthState,
26 | {
27 | pub fn authenticate(
28 | mut self,
29 | db: &str,
30 | login: &str,
31 | password: &str,
32 | ) -> ReqwestAuthResult> {
33 | let request = self.get_auth_request(db, login, password);
34 | let (response, session_id) = request.send_internal()?;
35 | Ok(self.parse_auth_response(db, login, password, response, session_id)?)
36 | }
37 | }
38 |
39 | impl<'a, T> OdooRequest<'a, T, ReqwestBlocking>
40 | where
41 | T: JsonRpcParams + Debug + Serialize,
42 | T::Container: Debug + Serialize,
43 | {
44 | pub fn send(self) -> ReqwestResult {
45 | Ok(self.send_internal()?.0)
46 | }
47 |
48 | fn send_internal(self) -> ReqwestResult<(T::Response, Option)> {
49 | let request = self._impl.client.post(&self.url).json(&self.data);
50 | let response = request.send()?;
51 | Ok((self.parse_response(&response.text()?)?, None))
52 | }
53 | }
54 |
--------------------------------------------------------------------------------
/odoo-api/src/client/mod.rs:
--------------------------------------------------------------------------------
1 | //! The user-facing API types
2 | //!
3 | //! This module provides a user-facing API for Odoo JSON-RPC methods.
4 | //!
5 | //! **TODO**: Proper examples for async/blocking, error handling, and authentication options
6 |
7 | pub use http_impl::closure_async::ClosureReturn as AsyncClosureReturn;
8 | pub use http_impl::closure_blocking::ClosureReturn as BlockingClosureReturn;
9 | pub use odoo_client::{AuthState, Authed, NotAuthed, OdooClient, RequestImpl};
10 | pub use odoo_request::OdooRequest;
11 |
12 | pub use error::{Error, Result};
13 | pub use http_impl::closure_async::ClosureAsync;
14 | pub use http_impl::closure_blocking::ClosureBlocking;
15 |
16 | #[cfg(feature = "async")]
17 | pub use http_impl::reqwest_async::ReqwestAsync;
18 |
19 | #[cfg(feature = "blocking")]
20 | pub use http_impl::reqwest_blocking::ReqwestBlocking;
21 |
22 | pub mod error;
23 | mod http_impl;
24 | mod odoo_client;
25 | mod odoo_request;
26 |
--------------------------------------------------------------------------------
/odoo-api/src/client/odoo_client.rs:
--------------------------------------------------------------------------------
1 | //! The [`OdooClient`] type and associated bits
2 |
3 | use super::error::{AuthenticationError, AuthenticationResult};
4 | use super::OdooRequest;
5 | use crate::jsonrpc::{JsonRpcId, JsonRpcParams, OdooId, OdooWebMethod};
6 | use crate::service::web::{SessionAuthenticate, SessionAuthenticateResponse};
7 | use serde::Serialize;
8 | use serde_json::{from_str, to_string};
9 | use std::fmt::Debug;
10 |
11 | /// The "authentication" state of a client object
12 | ///
13 | /// This is used to allow API methods to require authentication, e.g., if they
14 | /// require some piece of auth data (e.g. database, login/uid, etc).
15 | pub trait AuthState {
16 | /// Get the current stored `session_id`, if available
17 | fn get_session_id(&self) -> Option<&str>;
18 | }
19 |
20 | /// Implemented by "authenticated" clients
21 | pub struct Authed {
22 | pub(crate) database: String,
23 | pub(crate) login: String,
24 | pub(crate) uid: OdooId,
25 | pub(crate) password: String,
26 | pub(crate) session_id: Option,
27 | }
28 | impl AuthState for Authed {
29 | fn get_session_id(&self) -> Option<&str> {
30 | self.session_id.as_deref()
31 | }
32 | }
33 |
34 | /// Implemented by "non-authenticated" clients
35 | pub struct NotAuthed {}
36 | impl AuthState for NotAuthed {
37 | fn get_session_id(&self) -> Option<&str> {
38 | None
39 | }
40 | }
41 |
42 | /// The "request implementation" for a client
43 | ///
44 | /// This is used to allow different `client.authenticate()` and
45 | /// `request.send()` impls based on the chosen request provider.
46 | pub trait RequestImpl {
47 | type Error: std::error::Error;
48 | }
49 |
50 | /// An Odoo API client
51 | ///
52 | /// This is the main public interface for the `odoo-api` crate. It provides
53 | /// methods to authenticate with an Odoo instance, and to call JSON-RPC methods
54 | /// (`execute`, `create_database`, etc), "Web" methods (`/web/session/authenticate`, etc)
55 | /// and ORM methods (`read_group`, `create`, etc).
56 | ///
57 | /// ## Usage:
58 | /// ```no_run
59 | /// use odoo_api::{OdooClient, jvec, jmap};
60 | ///
61 | /// # async fn test() -> odoo_api::client::Result<()> {
62 | /// let url = "https://demo.odoo.com";
63 | /// let mut client = OdooClient::new_reqwest_async(url)?
64 | /// .authenticate(
65 | /// "test-database",
66 | /// "admin",
67 | /// "password"
68 | /// ).await?;
69 | ///
70 | /// let user_ids = client.execute(
71 | /// "res.users",
72 | /// "search",
73 | /// jvec![
74 | /// []
75 | /// ]
76 | /// ).send().await?;
77 | ///
78 | /// println!("Found user IDs: {:?}", user_ids.data);
79 | /// # Ok(())
80 | /// # }
81 | /// ```
82 | pub struct OdooClient
83 | where
84 | S: AuthState,
85 | I: RequestImpl,
86 | {
87 | pub(crate) url: String,
88 |
89 | pub(crate) auth: S,
90 | pub(crate) _impl: I,
91 |
92 | pub(crate) id: JsonRpcId,
93 | }
94 |
95 | // Base client methods
96 | impl OdooClient
97 | where
98 | S: AuthState,
99 | I: RequestImpl,
100 | {
101 | /// Validate and parse URLs
102 | ///
103 | /// We cache the "/jsonrpc" endpoint because that's used across all of
104 | /// the JSON-RPC methods. We also store the bare URL, because that's
105 | /// used for "Web" methods
106 | pub(crate) fn validate_url(url: &str) -> String {
107 | // ensure the last char isn't "/"
108 | let len = url.len();
109 | if len > 0 && &url[len - 1..] == "/" {
110 | url[0..len - 1].to_string()
111 | } else {
112 | url.to_string()
113 | }
114 | }
115 |
116 | pub(crate) fn build_endpoint(&self, endpoint: &str) -> String {
117 | format!("{}{}", self.url, endpoint)
118 | }
119 |
120 | /// Build the data `T` into a request for the fully-qualified endpoint `url`
121 | ///
122 | /// This returns an [`OdooRequest`] typed to the Clients (`self`s) [`RequestImpl`],
123 | /// and to its auth state. The returned request is bound by lifetime `'a` to the client.
124 | /// The URL is converted into a full String, so no lifetimes apply there.
125 | pub(crate) fn build_request<'a, T>(&'a mut self, data: T, url: &str) -> OdooRequest<'a, T, I>
126 | where
127 | T: JsonRpcParams + Debug,
128 | T::Container: Debug + Serialize,
129 | S: AuthState,
130 | {
131 | OdooRequest::new(
132 | data.build(self.next_id()),
133 | url.into(),
134 | self.session_id(),
135 | &self._impl,
136 | )
137 | }
138 |
139 | /// Fetch the next id
140 | pub(crate) fn next_id(&mut self) -> JsonRpcId {
141 | let id = self.id;
142 | self.id += 1;
143 | id
144 | }
145 |
146 | /// Helper method to perform the 1st stage of the authentication request
147 | ///
148 | /// Implementors of [`RequestImpl`] will use this method to build an
149 | /// [`OdooRequest`], which they will then send using their own `send()` method.
150 | ///
151 | /// This is necessary because each `RequestImpl` has its own `send()` signature
152 | /// (i.e., some are `fn send()`, some are `async fn send()`).
153 | pub(crate) fn get_auth_request(
154 | &mut self,
155 | db: &str,
156 | login: &str,
157 | password: &str,
158 | ) -> OdooRequest {
159 | let authenticate = crate::service::web::SessionAuthenticate {
160 | db: db.into(),
161 | login: login.into(),
162 | password: password.into(),
163 | };
164 | let endpoint = self.build_endpoint(authenticate.endpoint());
165 | self.build_request(authenticate, &endpoint)
166 | }
167 |
168 | /// Helper method to perform the 2nd stage of the authentication request
169 | ///
170 | /// At this point, the [`OdooRequest`] has been sent by the [`RequestImpl`],
171 | /// and the response data has been fetched and parsed.
172 | ///
173 | /// This method extracts the `uid` and `session_id` from the resulting request,
174 | /// and returns an `OdooClient`, e.g., an "authenticated" client.
175 | pub(crate) fn parse_auth_response(
176 | self,
177 | db: &str,
178 | login: &str,
179 | password: &str,
180 | response: SessionAuthenticateResponse,
181 | session_id: Option,
182 | ) -> AuthenticationResult> {
183 | let uid = response.data.get("uid").ok_or_else(|| {
184 | AuthenticationError::UidParseError(
185 | "Failed to parse UID from /web/session/authenticate call".into(),
186 | )
187 | })?;
188 |
189 | //TODO: this is a bit awkward..
190 | let uid = from_str(&to_string(uid)?)?;
191 | let auth = Authed {
192 | database: db.into(),
193 | uid,
194 | login: login.into(),
195 | password: password.into(),
196 | session_id,
197 | };
198 |
199 | Ok(OdooClient {
200 | url: self.url,
201 | auth,
202 | _impl: self._impl,
203 | id: self.id,
204 | })
205 | }
206 |
207 | pub fn session_id(&self) -> Option<&str> {
208 | self.auth.get_session_id()
209 | }
210 |
211 | pub fn authenticate_manual(
212 | self,
213 | db: &str,
214 | login: &str,
215 | uid: OdooId,
216 | password: &str,
217 | session_id: Option,
218 | ) -> OdooClient {
219 | let auth = Authed {
220 | database: db.into(),
221 | uid,
222 | login: login.into(),
223 | password: password.into(),
224 | session_id,
225 | };
226 |
227 | OdooClient {
228 | url: self.url,
229 | auth,
230 | _impl: self._impl,
231 | id: self.id,
232 | }
233 | }
234 |
235 | /// Update the URL for this client
236 | pub fn with_url(&mut self, url: &str) -> &mut Self {
237 | self.url = Self::validate_url(url);
238 | self
239 | }
240 | }
241 |
242 | /// Methods for non-authenticated clients
243 | impl OdooClient
244 | where
245 | I: RequestImpl,
246 | {
247 | /// Helper method to build a new client
248 | ///
249 | /// This isn't exposed via the public API - instead, users will call
250 | /// one of the impl-specific `new_xx()` functions, like:
251 | /// - OdooClient::new_request_blocking()
252 | /// - OdooClient::new_request_async()
253 | /// - OdooClient::new_closure_blocking()
254 | /// - OdooClient::new_closure_async()
255 | pub(crate) fn new(url: &str, _impl: I) -> Self {
256 | let url = Self::validate_url(url);
257 | Self {
258 | url,
259 | auth: NotAuthed {},
260 | _impl,
261 | id: 1,
262 | }
263 | }
264 | }
265 |
--------------------------------------------------------------------------------
/odoo-api/src/client/odoo_request.rs:
--------------------------------------------------------------------------------
1 | //! The [`OdooRequest`] type and associated bits
2 |
3 | use super::RequestImpl;
4 | use crate::client::error::ParseResponseResult;
5 | use crate::jsonrpc::{JsonRpcParams, JsonRpcRequest, JsonRpcResponse};
6 | use serde::de::DeserializeOwned;
7 | use serde::Serialize;
8 | use serde_json::from_str;
9 | use std::fmt::Debug;
10 |
11 | pub struct OdooRequest<'a, T, I>
12 | where
13 | T: JsonRpcParams + Debug + Serialize,
14 | T::Container: Debug + Serialize,
15 | I: RequestImpl,
16 | {
17 | pub(crate) data: JsonRpcRequest,
18 | pub(crate) url: String,
19 | pub(crate) session_id: Option<&'a str>,
20 | pub(crate) _impl: &'a I,
21 | }
22 |
23 | impl<'a, T, I> OdooRequest<'a, T, I>
24 | where
25 | T: JsonRpcParams + Debug + Serialize,
26 | T::Container: Debug + Serialize,
27 | I: RequestImpl,
28 | {
29 | pub(crate) fn new(
30 | data: JsonRpcRequest,
31 | url: String,
32 | session_id: Option<&'a str>,
33 | _impl: &'a I,
34 | ) -> Self {
35 | Self {
36 | data,
37 | url,
38 | session_id,
39 | _impl,
40 | }
41 | }
42 |
43 | pub(crate) fn parse_response(
44 | &self,
45 | data: &str,
46 | ) -> ParseResponseResult {
47 | let response: JsonRpcResponse = from_str(data)?;
48 |
49 | match response {
50 | JsonRpcResponse::Success(data) => Ok(data.result),
51 | JsonRpcResponse::Error(data) => Err(data.error.into()),
52 | }
53 | }
54 | }
55 |
--------------------------------------------------------------------------------
/odoo-api/src/jsonrpc/mod.rs:
--------------------------------------------------------------------------------
1 | //! The base JSON-RPC types
2 | //!
3 | //! This module exposes type structs, traits, and helper methods to build valid
4 | //! Odoo JSON-RPC requests.
5 | //!
6 | //! As a crate user, you shouldn't need to interact with these directly. Instead, see [`crate::client`].
7 | //!
8 | //! **TODO**: Add examples for the `types-only` feature
9 |
10 | pub mod request;
11 | pub mod response;
12 |
13 | use serde::{Deserialize, Serialize};
14 | use std::fmt::Debug;
15 |
16 | pub use request::{
17 | JsonRpcParams, JsonRpcRequest, OdooApiContainer, OdooApiMethod, OdooOrmContainer,
18 | OdooOrmMethod, OdooWebContainer, OdooWebMethod,
19 | };
20 | pub use response::JsonRpcResponse;
21 |
22 | /// A JSON-RPC request id
23 | pub type JsonRpcId = u32;
24 |
25 | /// An Odoo record id
26 | ///
27 | /// Note that this *is* signed, as some Odoo models (e.g. the `PurchaseBillUnion`)
28 | /// use positive ids to represent one model (`purchase.order`), and negative ids
29 | /// to represent another (`account.move`).
30 | pub type OdooId = i32;
31 |
32 | /// A vec of [`OdooId`].
33 | ///
34 | /// This type also implements `From`, which allows for flexible function
35 | /// args, e.g.:
36 | /// ```
37 | /// use odoo_api::jsonrpc::OdooIds;
38 | /// fn my_function>(ids: I) {
39 | /// // ...
40 | /// }
41 | ///
42 | /// // call with a list of ids...
43 | /// my_function(vec![1, 2, 3]);
44 | ///
45 | /// // ... or with a single id
46 | /// my_function(1);
47 | /// ```
48 | #[derive(Debug, Serialize, Deserialize)]
49 | pub struct OdooIds(Vec);
50 |
51 | impl From for OdooIds {
52 | fn from(value: OdooId) -> Self {
53 | OdooIds(vec![value])
54 | }
55 | }
56 | impl From> for OdooIds {
57 | fn from(value: Vec) -> Self {
58 | Self(value)
59 | }
60 | }
61 |
62 | /// A string representing the JSON-RPC version
63 | ///
64 | /// At the time of writing, this is always set to "2.0"
65 | #[derive(Debug, Serialize, Deserialize)]
66 | pub enum JsonRpcVersion {
67 | /// Odoo JSON-RCP API version 2.0
68 | #[serde(rename = "2.0")]
69 | V2,
70 | }
71 |
72 | /// A string representing the JSON-RPC "method"
73 | ///
74 | /// At the time of writing, this is always set to "call"
75 | #[derive(Debug, Serialize, Deserialize)]
76 | pub enum JsonRpcMethod {
77 | #[serde(rename = "call")]
78 | Call,
79 | }
80 |
--------------------------------------------------------------------------------
/odoo-api/src/jsonrpc/request.rs:
--------------------------------------------------------------------------------
1 | //! JSON-RPC Requests
2 |
3 | use super::{JsonRpcId, JsonRpcMethod, JsonRpcVersion};
4 | use serde::de::DeserializeOwned;
5 | use serde::Serialize;
6 | use std::fmt::Debug;
7 |
8 | mod api;
9 | mod orm;
10 | mod web;
11 |
12 | pub use api::{OdooApiContainer, OdooApiMethod};
13 | pub use orm::{OdooOrmContainer, OdooOrmMethod};
14 | pub use web::{OdooWebContainer, OdooWebMethod};
15 |
16 | /// Implemented by Odoo "method" types (e.g.,
17 | /// [`Execute`](crate::service::object::Execute) or
18 | /// [`SessionAuthenticate`](crate::service::web::SessionAuthenticate))
19 | ///
20 | /// When building an [`JsonRpcRequest`] object, the `params` field is actually
21 | /// set to [`JsonRpcParams::Container`], not the concrete "method" type. This
22 | /// allows for flexibility in the [`Serialize`] impl.
23 | ///
24 | /// For example, the `Execute` method uses [`OdooApiContainer`] as its container,
25 | /// which injects the `service` and `method` keys into the request struct:
26 | /// ```json
27 | /// {
28 | /// "jsonrpc": "2.0",
29 | /// "method": "call",
30 | /// "id": 1000,
31 | /// "params": {
32 | /// "service": "object",
33 | /// "method": "execute",
34 | /// "args":
35 | /// }
36 | /// }
37 | /// ```
38 | ///
39 | /// Whereas the `SessionAuthenticate` method's container ([`OdooWebContainer`])
40 | /// has a transparent Serialize impl, so the `SessionAuthenticate` data is set
41 | /// directly on the `params` key:
42 | /// ```json
43 | /// {
44 | /// "jsonrpc": "2.0",
45 | /// "method": "call",
46 | /// "id": 1000,
47 | /// "params":
48 | /// }
49 | /// ```
50 | pub trait JsonRpcParams
51 | where
52 | Self: Sized + Debug + Serialize,
53 | {
54 | type Container: Debug + Serialize;
55 | type Response: Debug + DeserializeOwned;
56 |
57 | fn build(self, id: JsonRpcId) -> JsonRpcRequest;
58 | }
59 |
60 | /// A struct representing the full JSON-RPC request body
61 | ///
62 | /// See [`JsonRpcParams`] for more info about the strange `params` field type.
63 | #[derive(Debug, Serialize)]
64 | pub struct JsonRpcRequest
65 | where
66 | T: JsonRpcParams + Serialize + Debug,
67 | T::Container: Debug + Serialize,
68 | {
69 | /// The JSON-RPC version (`2.0`)
70 | pub(crate) jsonrpc: JsonRpcVersion,
71 |
72 | /// The JSON-RPC method (`call`)
73 | pub(crate) method: JsonRpcMethod,
74 |
75 | /// The request id
76 | ///
77 | /// This is not used for any stateful behaviour on the Odoo/Python side
78 | pub(crate) id: JsonRpcId,
79 |
80 | /// The request params (service, method, and arguments)
81 | pub(crate) params: ::Container,
82 | }
83 |
--------------------------------------------------------------------------------
/odoo-api/src/jsonrpc/request/api.rs:
--------------------------------------------------------------------------------
1 | use crate::jsonrpc::JsonRpcId;
2 |
3 | use super::{JsonRpcMethod, JsonRpcParams, JsonRpcRequest, JsonRpcVersion};
4 | use serde::ser::{SerializeStruct, Serializer};
5 | use serde::Serialize;
6 | use std::fmt::Debug;
7 |
8 | /// The container type for an Odoo "API" (JSON-RPC) request
9 | ///
10 | /// For more info, see [`super::JsonRpcParams`]
11 | #[derive(Debug)]
12 | pub struct OdooApiContainer
13 | where
14 | T: OdooApiMethod + JsonRpcParams = Self>,
15 | {
16 | pub(crate) inner: T,
17 | }
18 |
19 | // Custom "man-in-the-middle" serialize impl
20 | impl Serialize for OdooApiContainer
21 | where
22 | T: OdooApiMethod + JsonRpcParams = Self>,
23 | {
24 | fn serialize(&self, serializer: S) -> ::std::result::Result
25 | where
26 | S: Serializer,
27 | {
28 | let mut state = serializer.serialize_struct("args", 3)?;
29 | let (service, method) = self.inner.describe();
30 | state.serialize_field("service", service)?;
31 | state.serialize_field("method", method)?;
32 | state.serialize_field("args", &self.inner)?;
33 | state.end()
34 | }
35 | }
36 |
37 | /// An Odoo "API" (JSON-RPC) request type
38 | pub trait OdooApiMethod
39 | where
40 | Self: Sized + Debug + Serialize + JsonRpcParams = OdooApiContainer>,
41 | Self::Container: Debug + Serialize,
42 | {
43 | /// Describe the JSON-RPC service and method for this type
44 | fn describe(&self) -> (&'static str, &'static str);
45 |
46 | /// Describe method endpoint (e.g., "/web/session/authenticate")
47 | fn endpoint(&self) -> &'static str;
48 |
49 | /// Build `self` into a full [`JsonRpcRequest`]
50 | fn _build(self, id: JsonRpcId) -> JsonRpcRequest {
51 | JsonRpcRequest {
52 | jsonrpc: JsonRpcVersion::V2,
53 | method: JsonRpcMethod::Call,
54 | id,
55 | params: OdooApiContainer { inner: self },
56 | }
57 | }
58 | }
59 |
--------------------------------------------------------------------------------
/odoo-api/src/jsonrpc/request/orm.rs:
--------------------------------------------------------------------------------
1 | use serde::ser::SerializeStruct;
2 | use serde::{Serialize, Serializer};
3 | use std::fmt::Debug;
4 |
5 | use super::{JsonRpcId, JsonRpcMethod, JsonRpcParams, JsonRpcRequest, JsonRpcVersion};
6 |
7 | /// The container type for an Odoo "ORM" request
8 | ///
9 | /// These functions are essentially just wrappers around
10 | /// [`Execute`](crate::service::object::Execute)
11 | /// and [`ExecuteKw`](crate::service::object::ExecuteKw), providing a more
12 | /// user-friendly interface (and better type checking!)
13 | ///
14 | /// For more info, see [`super::JsonRpcParams`]
15 | #[derive(Debug)]
16 | pub struct OdooOrmContainer
17 | where
18 | T: OdooOrmMethod + JsonRpcParams = Self>,
19 | {
20 | pub(crate) inner: T,
21 | }
22 |
23 | // Custom "man-in-the-middle" serialize impl
24 | impl Serialize for OdooOrmContainer
25 | where
26 | T: OdooOrmMethod + JsonRpcParams = Self>,
27 | {
28 | fn serialize(&self, serializer: S) -> ::std::result::Result
29 | where
30 | S: Serializer,
31 | {
32 | let mut state = serializer.serialize_struct("args", 3)?;
33 | state.serialize_field("service", "object")?;
34 | state.serialize_field("method", "execute_kw")?;
35 | state.serialize_field("args", &self.inner)?;
36 | state.end()
37 | }
38 | }
39 |
40 | /// An Odoo "Orm" request type
41 | pub trait OdooOrmMethod
42 | where
43 | Self: Sized + Debug + Serialize + JsonRpcParams = OdooOrmContainer>,
44 | Self::Container: Debug + Serialize,
45 | {
46 | /// Describe the "ORM" method endpoint (e.g., "/web/session/authenticate")
47 | fn endpoint(&self) -> &'static str;
48 |
49 | /// Return the model method name (e.g., "read_group" or "create")
50 | fn method(&self) -> &'static str;
51 |
52 | /// Build `self` into a full [`JsonRpcRequest`]
53 | fn _build(self, id: JsonRpcId) -> JsonRpcRequest {
54 | JsonRpcRequest {
55 | jsonrpc: JsonRpcVersion::V2,
56 | method: JsonRpcMethod::Call,
57 | id,
58 | params: OdooOrmContainer { inner: self },
59 | }
60 | }
61 | }
62 |
--------------------------------------------------------------------------------
/odoo-api/src/jsonrpc/request/web.rs:
--------------------------------------------------------------------------------
1 | use serde::Serialize;
2 | use std::fmt::Debug;
3 |
4 | use super::{JsonRpcId, JsonRpcMethod, JsonRpcParams, JsonRpcRequest, JsonRpcVersion};
5 |
6 | /// The container type for an Odoo "Web" request
7 | ///
8 | /// This type covers (almost) any request whose endpoint starts with `/web`,
9 | /// for example:
10 | /// - `/web/session/authenticate`
11 | /// - `/web/session/destroy`
12 | /// - `/web/dataset/call`
13 | /// - And many more
14 | ///
15 | /// For more info, see [`super::JsonRpcParams`]
16 | #[derive(Debug, Serialize)]
17 | #[serde(transparent)]
18 | pub struct OdooWebContainer
19 | where
20 | T: OdooWebMethod + JsonRpcParams = Self>,
21 | {
22 | pub(crate) inner: T,
23 | }
24 |
25 | /// An Odoo "Web" request type
26 | pub trait OdooWebMethod
27 | where
28 | Self: Sized + Debug + Serialize + JsonRpcParams = OdooWebContainer>,
29 | Self::Container: Debug + Serialize,
30 | {
31 | /// Describe method endpoint (e.g., "/web/session/authenticate")
32 | fn endpoint(&self) -> &'static str;
33 |
34 | /// Build `self` into a full [`JsonRpcRequest`]
35 | fn _build(self, id: JsonRpcId) -> JsonRpcRequest {
36 | JsonRpcRequest {
37 | jsonrpc: JsonRpcVersion::V2,
38 | method: JsonRpcMethod::Call,
39 | id,
40 | params: OdooWebContainer { inner: self },
41 | }
42 | }
43 | }
44 |
--------------------------------------------------------------------------------
/odoo-api/src/jsonrpc/response.rs:
--------------------------------------------------------------------------------
1 | //! JSON-RPC Responses
2 |
3 | use super::{JsonRpcId, JsonRpcVersion};
4 | use serde::{Deserialize, Serialize};
5 | use serde_json::{Map, Value};
6 | use std::fmt::Debug;
7 |
8 | /// An Odoo JSON-RPC API response
9 | ///
10 | /// This struct represents the base JSON data, and is paramterized over the
11 | /// *request* [`OdooApiMethod`](super::OdooApiMethod). The deserialization struct is chosen by
12 | /// looking at the associated type [`OdooApiMethod::Response`](super::OdooApiMethod).
13 | ///
14 | /// See: [odoo/http.py](https://github.com/odoo/odoo/blob/b6e195ccb3a6c37b0d980af159e546bdc67b1e42/odoo/http.py#L1805-L1841)
15 | #[derive(Debug, Serialize, Deserialize)]
16 | #[serde(untagged)]
17 | pub enum JsonRpcResponse
18 | where
19 | T: Debug,
20 | {
21 | Success(JsonRpcResponseSuccess),
22 | Error(JsonRpcResponseError),
23 | }
24 |
25 | /// A successful Odoo API response
26 | #[derive(Debug, Serialize, Deserialize)]
27 | pub struct JsonRpcResponseSuccess
28 | where
29 | //TODO: should we have something else here?
30 | T: Debug,
31 | {
32 | /// The JSON-RPC version (`2.0`)
33 | pub(crate) jsonrpc: JsonRpcVersion,
34 |
35 | /// The request id
36 | ///
37 | /// This is not used for any stateful behaviour on the Odoo/Python side
38 | pub(crate) id: JsonRpcId,
39 |
40 | /// The response data, parameterized on the *request* [`OdooApiMethod::Response`](super::OdooApiMethod)
41 | /// associated type.
42 | pub(crate) result: T,
43 | }
44 |
45 | /// A failed Odoo API response
46 | #[derive(Debug, Serialize, Deserialize)]
47 | pub struct JsonRpcResponseError {
48 | /// The JSON-RPC version (`2.0`)
49 | pub(crate) jsonrpc: JsonRpcVersion,
50 |
51 | /// The request id
52 | ///
53 | /// This is not used for any stateful behaviour on the Odoo/Python side
54 | pub(crate) id: JsonRpcId,
55 |
56 | /// A struct containing the error information
57 | pub(crate) error: JsonRpcError,
58 | }
59 |
60 | #[derive(Debug, Serialize, Deserialize)]
61 | pub struct JsonRpcError {
62 | /// The error code. Currently hardcoded to `200`
63 | pub code: u32,
64 |
65 | /// The error "message". This is a short string indicating the type of
66 | /// error. Some examples are:
67 | /// * `Odoo Server Error`
68 | /// * `404: Not Found`
69 | /// * `Odoo Session Expired`
70 | pub message: String,
71 |
72 | /// The actual error data
73 | pub data: JsonRpcErrorData,
74 | }
75 |
76 | impl std::fmt::Display for JsonRpcError {
77 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
78 | write!(f, "{}", self.message)
79 | }
80 | }
81 |
82 | impl std::error::Error for JsonRpcError {}
83 |
84 | #[derive(Debug, Serialize, Deserialize)]
85 | pub struct JsonRpcErrorData {
86 | /// The module? and type of the object where the exception was raised
87 | ///
88 | /// For example:
89 | /// * `builtins.TypeError`
90 | /// * `odoo.addons.account.models.account_move.AccountMove`
91 | pub name: String,
92 |
93 | /// The Python exception stack trace
94 | pub debug: String,
95 |
96 | /// The Python exception message (e.g. `str(exception)`)
97 | pub message: String,
98 |
99 | /// The Python exception arguments (e.g. `excetion.args`)
100 | pub arguments: Vec,
101 |
102 | /// The Python exception context (e.g. `excetion.context`)
103 | pub context: Map,
104 | }
105 |
--------------------------------------------------------------------------------
/odoo-api/src/lib.rs:
--------------------------------------------------------------------------------
1 | //! The `odoo-api` odoo-api is a Rust library crate that provides a user-friendly interface
2 | //! to interact with the Odoo JSONRPC and ORM APIs, while preserving strong typing. It
3 | //! includes both async and blocking support out of the box, and allows users to provide
4 | //! their own implementations if needed.
5 | //!
6 | //! See the [Example](#example) section below for a brief example, or the [`client`] module for more in-depth examples.
7 | //!
8 | //! ## Features
9 | //!
10 | //! - **Strong typing**: `odoo-api` prioritizes the use of concrete types wherever
11 | //! possible, rather than relying on generic `json!{}` calls.
12 | //! - **Async and blocking support**: the library provides both async and blocking
13 | //! HTTP impls via [`reqwest`], and allows users to easily provide their own HTTP
14 | //! impl via a shim closure.
15 | //! - **JSONRPC API support**: including database management (create, duplicate, etc),
16 | //! translations, and generic `execute` and `execute_kw`
17 | //! - **ORM API support**: including user-friendly APIs for the CRUD, `search_read`,
18 | //! security rule checking, and more
19 | //! - **Types-only**: allowing you to include this library for its types only. See
20 | //! [Types Only](#types-only) below for more info
21 | //!
22 | //! ### Supported API Methods
23 | //!
24 | //! See the [`service`] module for a full list of supported API methods.
25 | //!
26 | //! ### Bring Your Own Requests
27 | //!
28 | //! Do you already have an HTTP library in your dependencies (e.g., `reqwest`)?
29 | //!
30 | //! The `odoo-api` crate allows you to use your existing HTTP library by writing a
31 | //! simple shim closure. See [`client::ClosureAsync`] or [`client::ClosureBlocking`]
32 | //! for more info.
33 | //!
34 | //! ### Types Only
35 | //!
36 | //! The crate offers a `types-only` feature. When enabled, the library only exposes
37 | //! the API request & response types, along with `Serialize` and `Deserialize` impls.
38 | //! The async/blocking impls (and the [`reqwest`] dependency) are dropped when this
39 | //! feature is active.
40 | //!
41 | //! See the [`jsonrpc`] module for information on `types-only`.
42 | //!
43 | //! ## Example
44 | //!
45 | //!
46 | //! Add the following to your `Cargo.toml`:
47 | //!
48 | //! ```toml
49 | //! [dependencies]
50 | //! odoo_api = "0.2"
51 | //! ```
52 | //!
53 | //! Then make your requests:
54 | //! ```no_run
55 | //! # #[cfg(not(feature = "types-only"))]
56 | //! use odoo_api::{OdooClient, jvec, jmap};
57 | //!
58 | //! # #[cfg(not(feature = "types-only"))]
59 | //! # async fn test() -> odoo_api::client::Result<()> {
60 | //! // build the client
61 | //! let url = "https://odoo.example.com";
62 | //! let mut client = OdooClient::new_reqwest_async(url)?;
63 | //!
64 | //! // authenticate with `some-database`
65 | //! let mut client = client.authenticate(
66 | //! "some-database",
67 | //! "admin",
68 | //! "password",
69 | //! ).await?;
70 | //!
71 | //! // fetch a list of users with the `execute` method
72 | //! let users = client.execute(
73 | //! "res.users",
74 | //! "search",
75 | //! jvec![
76 | //! [["active", "=", true], ["login", "!=", "__system__"]]
77 | //! ]
78 | //! ).send().await?;
79 | //!
80 | //! // fetch the login and partner_id fields from user id=1
81 | //! let info = client.execute_kw(
82 | //! "res.users",
83 | //! "read",
84 | //! jvec![[1]],
85 | //! jmap!{
86 | //! "fields": ["login", "partner_id"]
87 | //! }
88 | //! ).send().await?;
89 | //!
90 | //! // create 2 new partners with the `create` ORM method
91 | //! let partners = client.create(
92 | //! "res.partner",
93 | //! jvec![{
94 | //! "name": "Alice",
95 | //! "email": "alice@example.com",
96 | //! "phone": "555-555-5555",
97 | //! }, {
98 | //! "name": "Bob",
99 | //! "email": "bob@example.com",
100 | //! "phone": "555-555-5555",
101 | //! }]
102 | //! ).send().await?;
103 | //!
104 | //! // fetch a list of databases
105 | //! let databases = client.db_list(false).send().await?;
106 | //!
107 | //! // fetch server version info
108 | //! let version_info = client.common_version().send().await?;
109 | //! # Ok(())
110 | //! # }
111 | //! ```
112 |
113 | // The `types-only` feature implies that the `client` module isn't included, so
114 | // `async` and `blocking` have no effect
115 | #[cfg(all(feature = "types-only", any(feature = "async", feature = "blocking")))]
116 | std::compile_error!(
117 | "The `types-only` feature is mutually exclusive with the `async` and `blocking` \
118 | features. Please disable the `async` and `blocking` by adding `default-features = false` \
119 | to your Cargo.toml"
120 | );
121 |
122 | pub mod service;
123 |
124 | #[macro_use]
125 | mod macros;
126 |
127 | #[cfg(not(feature = "types-only"))]
128 | pub mod client;
129 |
130 | #[cfg(not(feature = "types-only"))]
131 | pub use client::{AsyncClosureReturn, BlockingClosureReturn, OdooClient};
132 |
133 | pub mod jsonrpc;
134 | pub use jsonrpc::OdooId;
135 |
--------------------------------------------------------------------------------
/odoo-api/src/macros.rs:
--------------------------------------------------------------------------------
1 | //! Internal module to store macro_rules!() macros
2 |
3 | // Import Value and Map so the doc comments render properly
4 | #[allow(unused_imports)]
5 | use serde_json::{Map, Value};
6 |
7 | /// Helper macro to build a [`Vec`]
8 | ///
9 | /// This is useful when using any of the API methods that require a `Vec`,
10 | /// as [`serde_json`] doesn't have a way to build these.
11 | ///
12 | /// ## Example:
13 | /// ```no_run
14 | /// # #[cfg(not(feature = "types-only"))]
15 | /// # fn test() -> odoo_api::client::error::Result<()> {
16 | /// # use serde_json::{json, Value};
17 | /// # use odoo_api::{jvec, jmap};
18 | /// # use odoo_api::{OdooClient};
19 | /// # let client = OdooClient::new_reqwest_blocking("https://demo.odoo.com")?;
20 | /// # let mut client = client.authenticate_manual("", "", 1, "", None);
21 | /// // Manually
22 | /// let mut args = Vec::::new();
23 | /// args.push(json!([1, 2, 3]));
24 | /// args.push(json!(["id", "login"]));
25 | ///
26 | /// let request = client.execute(
27 | /// "res.users",
28 | /// "read",
29 | /// args,
30 | /// ).send()?;
31 | ///
32 | /// // With jvec![]:
33 | /// let request = client.execute(
34 | /// "res.users",
35 | /// "read",
36 | /// jvec![
37 | /// [1, 2, 3],
38 | /// ["id", "login"]
39 | /// ]
40 | /// ).send()?;
41 | /// # Ok(())
42 | /// # }
43 | /// ```
44 | #[macro_export]
45 | macro_rules! jvec {
46 | [$($v:tt),*] => {
47 | {
48 | let mut vec = ::std::vec::Vec::::new();
49 | $(
50 | vec.push(::serde_json::json!($v));
51 | )*
52 | vec
53 | }
54 | };
55 | () => { compiler_error!("")};
56 | }
57 |
58 | /// Helper macro to build a [`Map`]
59 | ///
60 | /// This is useful when using any of the API methods that require a `Map`,
61 | /// as [`serde_json`] doesn't have a way to build these.
62 | ///
63 | /// ## Example:
64 | /// ```no_run
65 | /// # #[cfg(not(feature = "types-only"))]
66 | /// # fn test() -> odoo_api::client::error::Result<()> {
67 | /// # use serde_json::{json, Value, Map};
68 | /// # use odoo_api::{jvec, jmap};
69 | /// # use odoo_api::{OdooClient};
70 | /// # let client = OdooClient::new_reqwest_blocking("https://demo.odoo.com")?;
71 | /// # let mut client = client.authenticate_manual("", "", 1, "", None);
72 | /// // Manually
73 | /// let mut kwargs = Map::::new();
74 | /// kwargs.insert("domain".into(), json!([["name", "ilike", "admin"]]));
75 | /// kwargs.insert("fields".into(), json!(["id", "login"]));
76 | ///
77 | /// let request = client.execute_kw(
78 | /// "res.users",
79 | /// "search_read",
80 | /// jvec![],
81 | /// kwargs,
82 | /// ).send()?;
83 | ///
84 | /// // With jmap!{}:
85 | /// let request = client.execute_kw(
86 | /// "res.users",
87 | /// "search_read",
88 | /// jvec![],
89 | /// jmap!{
90 | /// "domain": [["name", "ilike", "admin"]],
91 | /// "fields": ["id", "login"]
92 | /// }
93 | /// ).send()?;
94 | /// # Ok(())
95 | /// # }
96 | /// ```
97 | #[macro_export]
98 | macro_rules! jmap {
99 | {$($k:tt: $v:tt),*} => {
100 | {
101 | let mut map = ::serde_json::Map::::new();
102 | $(
103 | map.insert($k.into(), ::serde_json::json!($v));
104 | )*
105 | map
106 | }
107 | };
108 | () => { compiler_error!("")};
109 | }
110 |
111 | /// Helper macro to build a [`Vec`]
112 | ///
113 | /// Quite a few ORM methods take [`Vec`] as an argument. Using the built-in
114 | /// `vec![]` macro requires that each element is converted into a `String`, which
115 | /// is very cumbersome.
116 | ///
117 | /// Using this macro, we can write:
118 | /// ```
119 | /// # #[cfg(not(feature = "types-only"))]
120 | /// # fn test() {
121 | /// # use odoo_api::svec;
122 | /// let fields = svec!["string", "literals", "without", "to_string()"];
123 | /// # }
124 | /// ```
125 | #[macro_export]
126 | macro_rules! svec {
127 | [$($v:tt),*] => {
128 | {
129 | let mut vec = ::std::vec::Vec::::new();
130 | $(
131 | vec.push($v.to_string());
132 | )*
133 | vec
134 | }
135 | };
136 | () => { compiler_error!("")};
137 | }
138 |
--------------------------------------------------------------------------------
/odoo-api/src/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 |
10 | use crate as odoo_api;
11 | use crate::jsonrpc::{OdooApiMethod, OdooId};
12 | use odoo_api_macros::odoo_api;
13 | use serde::ser::SerializeTuple;
14 | use serde::{Deserialize, Serialize};
15 | use serde_json::{Map, Value};
16 | use 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> {
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 | ///
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)]
54 | pub 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)]
68 | pub 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> {
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 | ///
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)]
112 | pub 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,
124 | }
125 |
126 | /// Represents the response to an Odoo [`Authenticate`] call
127 | #[derive(Debug, Serialize, Deserialize)]
128 | #[serde(transparent)]
129 | pub 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> {
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 | ///
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)]
168 | pub struct Version {}
169 |
170 | // Version has no fields, but needs to output in JSON: `[]`
171 | impl Serialize for Version {
172 | fn serialize(&self, serializer: S) -> Result
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)]
183 | pub 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)]
202 | pub 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,
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> {
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 | ///
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)]
254 | pub 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)]
262 | pub 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)]
273 | pub 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)]
282 | pub 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)]
296 | mod 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 = 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 = 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 = 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 = 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 = 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 | }
540 |
--------------------------------------------------------------------------------
/odoo-api/src/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 |
9 | use crate as odoo_api;
10 | use crate::jsonrpc::OdooApiMethod;
11 | use odoo_api_macros::odoo_api;
12 | use serde::de::Visitor;
13 | use serde::ser::SerializeTuple;
14 | use serde::{Deserialize, Serialize};
15 | use 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> {
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 | ///
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)]
52 | pub 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,
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,
88 | }
89 |
90 | /// The response to a [`CreateDatabase`] request
91 | #[derive(Debug, Serialize, Deserialize)]
92 | #[serde(transparent)]
93 | pub 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> {
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 | ///
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)]
127 | pub 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)]
141 | pub 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> {
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 | ///
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)]
169 | pub 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)]
180 | pub 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> {
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, Box> { 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 = base64::decode(&resp.b64_bytes)?;
212 | ///
213 | /// // write the data to a file ...
214 | /// # Ok(())
215 | /// # }
216 | /// ```
217 | ///