├── .gitignore ├── CHANGELOG.md ├── Cargo.toml ├── LICENSE-APACHE ├── LICENSE-MIT ├── MIGRATION-v03x-v04x.md ├── README.md ├── examples ├── c02-rusqlite-sea-query-join.rs ├── c02-rusqlite-sea-query-select.rs ├── c03-sqlx-sea-query-join.rs └── support │ ├── mod.rs │ ├── rusqlite_utils.rs │ └── sqlx_utils.rs ├── modql-macros ├── Cargo.toml └── src │ ├── derives_field │ ├── derive_field_sea_value.rs │ ├── derive_fields.rs │ └── mod.rs │ ├── derives_filter │ ├── mod.rs │ └── utils.rs │ ├── derives_rusqlite │ ├── mod.rs │ ├── sqlite_from_row.rs │ ├── sqlite_from_value.rs │ └── sqlite_to_value.rs │ ├── lib.rs │ └── utils │ ├── mod.rs │ ├── modql_field.rs │ └── struct_modql_attr.rs ├── rustfmt.toml ├── src ├── error.rs ├── field │ ├── error.rs │ ├── field_meta.rs │ ├── field_metas.rs │ ├── has_fields.rs │ ├── mod.rs │ └── sea │ │ ├── has_sea_fields.rs │ │ ├── mod.rs │ │ ├── sea_field.rs │ │ └── sea_fields.rs ├── filter │ ├── into_sea │ │ ├── error.rs │ │ └── mod.rs │ ├── json │ │ ├── mod.rs │ │ ├── order_bys_de.rs │ │ ├── ovs_de_bool.rs │ │ ├── ovs_de_number.rs │ │ ├── ovs_de_string.rs │ │ ├── ovs_de_value.rs │ │ └── ovs_json.rs │ ├── list_options │ │ ├── mod.rs │ │ └── order_by.rs │ ├── mod.rs │ ├── nodes │ │ ├── group.rs │ │ ├── mod.rs │ │ └── node.rs │ └── ops │ │ ├── mod.rs │ │ ├── op_val_bool.rs │ │ ├── op_val_nums.rs │ │ ├── op_val_string.rs │ │ └── op_val_value.rs ├── includes.rs ├── lib.rs ├── sea_utils.rs └── sqlite │ └── mod.rs └── tests ├── support ├── mod.rs └── sqlite.rs ├── test_expand_fields.rs ├── test_expand_filter_nodes.rs ├── test_expand_filter_to_sea_condition.rs ├── test_expand_names_as_consts.rs ├── test_expand_sea_fields.rs ├── test_filter_node.rs ├── test_impl_filter_nodes.rs ├── test_json_filters.rs ├── test_readme.rs ├── test_rusqlite_derives.rs ├── test_rusqlite_join.rs ├── test_rusqlite_sea_query.rs ├── test_rusqlite_simple.rs └── test_serde_des.rs /.gitignore: -------------------------------------------------------------------------------- 1 | # By Default, Ignore any .* 2 | .* 3 | # Except .gitgnore 4 | !.gitignore 5 | 6 | 7 | # For now, disallow vscode 8 | # !.vscode 9 | .vscode/ 10 | 11 | # Rust ignore 12 | target/ 13 | Cargo.lock 14 | 15 | # nodejs 16 | !.mocharc.yaml 17 | 18 | # nodejs - Ignore node build files 19 | node_modules/ 20 | npm-debug.log 21 | report.*.json 22 | 23 | 24 | # Ignore dist/ folders 25 | dist/ 26 | 27 | # Safety net 28 | *.parquet 29 | *.map 30 | *.jpeg 31 | *.jpg 32 | *.png 33 | *.zip 34 | *.gz 35 | *.tar 36 | *.tgz 37 | *.mov 38 | *.mp4 -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | 2 | `.` minor | `+` Addition | `^` improvement | `!` Change | `*` Refactor 3 | 4 | ## 2024-11-15 - `0.4.1` 5 | 6 | - `^` Update `sea-query` and `rustsqlite` to version `0.32` 7 | - `!` Remove `cast_column_as` from filter, it's now on field 8 | 9 | ## 2024-09-23 - `0.4.0` 10 | 11 | - `.` update to sea-query-rusqlite 0.6 12 | - `.` add rustfmt.toml 13 | - `^` Update sea-query version `0.31` 14 | - `+` Add CaseInsensitive for StringOpVals (`StartsWithCt` .. ) 15 | - `+` Add ILIKE for postgres (case-insensitive LIKE) 16 | 17 | ## 2024-06-26 - `0.4.0-rc.8` 18 | 19 | - `.` update version to 0.4.0-rc.8 (with sea-query 31.0-rc.9) 20 | - `.` minor code clean 21 | 22 | ## 2024-06-13 - `0.4.0-rc.7` 23 | 24 | - `+` `T::field_metas()`, `FieldMetas`, and `FieldMeta` when `#[derive(Fields)]` 25 | - `!` `FieldRef` in favor of `FieldMeta` 26 | - `+` `SqliteFromRow::sqlite_from_row_partial(row, prop_names)` to retrieve partial objects. 27 | - `!` Sqlite types & derives rename 28 | - Traits: 29 | - Now: `SqliteFromRow`, before: `FromSqliteRow` 30 | - Now: `fn sqlite_from_row`, before: `fn from_sqlite_row...` 31 | - derive: 32 | - Now: `SqliteFromValue`, before: `FromSqliteRow` 33 | - Now: `SqliteFromValue`, before: `FromSqliteValue` 34 | - Now: `ToSqliteValue`, before: `SqliteToValue` 35 | 36 | ## 2024-05-09 - `0.4.0-rc.6 & rc.5` 37 | 38 | - `^` filter - add support for `#[modql(rel=...)]` at the Filter struct level 39 | - `.` cleanup 40 | - `-` filter - fix rel missing from FilterNode to SeaCondExpr 41 | - `.` update to v0.4.0-rc.5 42 | - `^` sea-query - impl IdenStatic for SIden (and SIden: Clone + Copy) 43 | 44 | ## 2024-04-18 - `0.4.0-rc.4` 45 | 46 | - SEE: Major refactor/cleanup (see [v0.3.x to v0.4.x document](MIGRATION-v03x-v04x.md) 47 | - `+` ToSqliteValue - added ToSqliteValue for simple enum and single tuple struct 48 | - `!` SeaField::new takes an Into as second arg now 49 | - `^` SeaField added From for SeaFields, and simple From (static str, SimpleExpr) for SeaField 50 | - `^` SeaField added ::siden(..) for static str column name 51 | - `^` SeaFields - add append and append_siden 52 | - `!` filter - rename context_path to rel 53 | - `^` SeaField - add new_concrete 54 | 55 | ## 2024-03-07 - `0.4.0-rc.2` 56 | 57 | - `!` Major refactor/cleanup (see [v0.3.x to v0.4.x document](MIGRATION-v03x-v04x.md) 58 | 59 | ## 2024-02-21 - `0.3.10` 60 | 61 | - `+` Add HasField::field_column_refs_with_rel 62 | - `+` Derive FromSqliteValue - Add support for simple tuple struct 63 | - `+` Derive Field - Add simple tuple struct support 64 | - `!` Deprecate (warning) `FieldEnum` in favor of `FieldValue` (does enum and single tuple struct) 65 | 66 | ## 2024-02-04 - `0.3.9` 67 | 68 | - `!` Rename FromSqliteRow::from_rusqlite_row to FromSqliteRow::from_sqlite_row) 69 | - `!` Change sqlite::FromRow to FromSqliteRow 70 | - `+` FromSqliteValue for enum 71 | - `+` Add `field::FieldEnum` derive to implement to seaqueryvalue for simple enum (also some code relayout) 72 | 73 | ## 2024-01-29 - `0.3.8` 74 | 75 | - `^` sea-query - use `thread-safe` feature 76 | 77 | ## 2024-01-22 - `0.3.7` 78 | 79 | - `+` `cast_as` to `filter` 80 | - `!` Potential API break for user using `FieldNode` struct constructor (e.g., `FieldNode {...}`). New property `options: FieldNodeOptions`. Use `options: FieldNodeOptions::default()`. 81 | - Using the `FieldNode::new(...)` functions and every other interface should be unchanged. 82 | 83 | ## 2024-01-20 - `0.3.6` 84 | 85 | - `+` Add `cast_as` to `field` 86 | 87 | ## 2024-01-13 - `0.3.5` 88 | 89 | - `+` first pass at the `sqlite::FromRow` trait/macro for `rusqlite` 90 | - `.` minor dependencies cleanup 91 | 92 | ## 2023-11-09 - `0.3.4` 93 | 94 | - `+` Added `OpValString::ContainsAll` 95 | - `!` For the OpValString, replace the `In` suffixes for `ContainsIn`, `StartsWithIn` with `Any` (e.g., `ContainsAny`) 96 | 97 | ## 2023-11-07 - `0.3.3` 98 | 99 | - `-` with-sea-query - Fix "in" operator issues 100 | - `-` fix OpValString containIn (was AND) 101 | 102 | ## 2023-11-06 - `0.3.2` 103 | 104 | - `-` fix map opvals ( ..) support for numbers and bool 105 | 106 | ## 2023-11-05 - `0.3.1` 107 | 108 | - `+` implements from Vec for FilterGroups 109 | 110 | ## 2023-10-24 - `0.3.0` 111 | 112 | - `!` - First v0.3.x release, with major update to API, with some breaking changes, and support for the `sea-query` and new `Fields` support. 113 | 114 | ## 2023-10-06 - `0.3.0-alpha.1` 115 | 116 | - `!` - Major update to API, with some breaking changes, and support for the `sea-query` and new `Fields` support. 117 | 118 | ## 2023-04-15 - `0.2.0` 119 | 120 | - `!` - Move `modql::filter::ListOptions` to `modql::filter::ListOptions`. 121 | - `+` - Now primitive types `u64, u32, i64, i32, f64, f32`. 122 | - `+` - Added many `From` traits. 123 | 124 | ## 2023-04-04 - `0.1.0` 125 | 126 | - `!` - Major refactoring from `0.0.5`. 127 | - `!` - Moved from raw `Vec..` to specialized type `FilterGroups` and `FilterGroup`. 128 | - `!` - Rename all of the `[Type]OpVal` to `OpVal[Type]` with full num type description. 129 | - `+` - Implemented lot of `From` traits. 130 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "modql" 3 | version = "0.4.2-WIP" 4 | edition = "2021" 5 | authors = ["Jeremy Chone "] 6 | license = "MIT OR Apache-2.0" 7 | description = "Rust implementation for Model Query Language support" 8 | categories = ["data-structures"] 9 | keywords = [ 10 | "query-language", 11 | "sea-query", 12 | "model", 13 | "data-model", 14 | "graphql" 15 | ] 16 | homepage = "https://github.com/jeremychone/rust-modql" 17 | repository = "https://github.com/jeremychone/rust-modql" 18 | resolver = "2" 19 | 20 | [workspace.lints.rust] 21 | unsafe_code = "forbid" 22 | # unused = { level = "allow", priority = -1 } # For exploratory dev. 23 | 24 | [lints.rust] 25 | unsafe_code = "forbid" 26 | unused = { level = "allow", priority = -1 } # For test files (dev) 27 | 28 | [workspace] 29 | members = [".", "modql-macros"] 30 | 31 | [features] 32 | # default = ["modql-macros", "with-sea-query", "with-rusqlite"] # for dev 33 | default = ["modql-macros"] 34 | with-sea-query = ["sea-query", "modql-macros/with-sea-query"] 35 | with-rusqlite = ["rusqlite", "modql-macros/with-rusqlite"] 36 | with-ilike = ["sea-query/backend-postgres"] 37 | 38 | [dependencies] 39 | modql-macros = { version="0.4", path = "modql-macros", optional=true} 40 | serde = { version = "1", features = ["derive"] } 41 | serde_json = "1" 42 | 43 | # -- For features 44 | sea-query = { workspace = true, optional = true } 45 | rusqlite = { workspace = true, optional = true } 46 | 47 | [workspace.dependencies] 48 | sea-query = { version = "0.32", features = ["thread-safe"] } 49 | rusqlite = { version = "0.32" } 50 | 51 | [dev-dependencies] 52 | serde_with = "3" 53 | pretty-sqlite = "0.0.2" 54 | rusqlite = {version = "0.32", features = ["bundled"]} 55 | sea-query-rusqlite = {version = "0.7"} 56 | tokio = { version = "1", features = ["full"]} 57 | sqlx = {version = "0.8", features = ["runtime-tokio"]} 58 | sea-query-binder = {version = "0.7", features = ["sqlx-sqlite"]} 59 | 60 | [[example]] 61 | name = "c02-rusqlite-sea-query-select" 62 | path = "examples/c02-rusqlite-sea-query-select.rs" 63 | required-features = ["with-rusqlite", "with-sea-query"] 64 | 65 | [[example]] 66 | name = "c02-rusqlite-sea-query-join" 67 | path = "examples/c02-rusqlite-sea-query-join.rs" 68 | required-features = ["with-rusqlite", "with-sea-query"] 69 | 70 | [[example]] 71 | name = "c03-sqlx-sea-query-join" 72 | path = "examples/c03-sqlx-sea-query-join.rs" 73 | required-features = ["with-rusqlite", "with-sea-query"] 74 | -------------------------------------------------------------------------------- /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 2024 Jeremy Chone 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. -------------------------------------------------------------------------------- /LICENSE-MIT: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2024 Jeremy Chone 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /MIGRATION-v03x-v04x.md: -------------------------------------------------------------------------------- 1 | Here are some migration information for version 0.3.x to 0.4.x. 2 | 3 | Version 0.4.x focuses primarily on cleaning up naming conventions and decoupling `sea-query` to enable certain `#[derive(Fields)]` functionalities even without the `with-sea-query` feature. 4 | 5 | > Note: The latest version is currently on the `rc` stream (e.g., `0.4.0-rc.1`), as sea-query `0.31.0` is still in `rc`. It will become the full `0.4.0` release once sea-query `0.31` is released. 6 | 7 | Key changes include: 8 | 9 | - The `HasFields` trait is now used for the common `#[derive(Fields)]` and provides `field_names() -> &'static [&'static str]` and `field_refs...` to access `rel` and `name` information. 10 | - `HasSeaFields` is the trait that implements the `sea-query` related functions. It is still generated by `#[derive(Fields)]` when the `with-sea-query` feature is enabled. 11 | - Naming within `HasSeaFields` has been cleaned up. 12 | - The `#[field(...)]` naming convention has been cleaned up (replacing `table` with `rel`), similar to the struct `#[modql(rel...)]`. 13 | 14 | | v0.3.x | v0.4.x | 15 | |-------------------------------------------------------|-----------------------------------------------------------------------| 16 | | **Refactor HasFields without sea-query dependencies** | | 17 | | `field::HasFields::field_names()` | `field::HasFields::field_names()` | 18 | | | `field::HasFields::field_refs()` with new `FieldRef` struct | 19 | | **Rename Field/s to SeaField/s** | | 20 | | `field::Field` | `field::SeaField` | 21 | | | `SeaField::new(iden, value)` both have `impl into..` so, no `.into()` | 22 | | `field::Fields` | `field::SeaFields` | 23 | | `#[derive(FieldValue)]` | `#[derive(SeaFieldValue)]` | 24 | | **Refactor HasFields to HasSeaFields** | | 25 | | `field::HasFields::not_none_fields` | `field::HasSeaFields::not_none_sea_fields` | 26 | | `field::HasFields::all_fields` | `field::HasSeaFields::all_sea_fields` | 27 | | `field::HasFields::field_idens` | `field::HasSeaFields::sea_idens` | 28 | | `field::HasFields::field_column_refs` | `field::HasSeaFields::sea_column_refs` | 29 | | `field::HasFields::field_column_refs_with_rel` | `field::HasSeaFields::sea_column_refs_with_rel` | 30 | | **Attributes** | | 31 | | `#[field(table="table_name",name="col_name")]` | `#[field(rel="table_name",name="col_name")]` | 32 | | **SQLite** | | 33 | | | `ToSqliteValue` | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # modql 2 | 3 | **modql** is a set of types and utilities designed to structurally express model query filters (e.g., `$eq: ..` `$startsWith: ..`, `$containsIn: [..]`) and list options (e.g., `offset`, `limit`, `order_bys`). These can be easily represented in JSON. 4 | 5 | In essence, it offers a MongoDB-like filter syntax that is storage-agnostic, has built-in support for [sea-query](https://crates.io/crates/sea-query), and can be expressed either in JSON or Rust types. 6 | 7 | > **IMPORTANT**: **v0.4.0 is RELEASED**: This release includes significant refactoring (retaining the same functionality but with cleaner naming and decoupled from sea-query) to allow `derive(Fields)` to provide `field_names` and `field_refs` without requiring the `with-sea-query` feature. 8 | > 9 | > For more information, see [CHANGELOG.md](CHANGELOG.md) and [MIGRATION-v03x-v04x.md](MIGRATION-v03x-v04x.md). 10 | 11 | ## Thanks 12 | 13 | - [Chetan Bhasin](https://github.com/ChetanBhasin) for [PR #8 Use sea-query 0.32](https://github.com/jeremychone/rust-modql/pull/8) 14 | - [Andrii Yermakov](https://github.com/andriy-yermakov) for [PR #2 Fix an errata in null operator for numbers](https://github.com/jeremychone/rust-modql/pull/2) 15 | - [Marc Cámara](https://github.com/mcamara) for [PR #9 Add Column Casting Support to Filter Nodes](https://github.com/jeremychone/rust-modql/pull/9) 16 | - [Marc Cámara](https://github.com/mcamara) for adding the Case Insensitive support and the `with-ilike` support for Postgresql. [PR #3](https://github.com/jeremychone/rust-modql/pull/3) 17 | - [Andrii Yermakov](https://github.com/andriy-yermakov) for fixing a "null operator for numbers" [PR #2](https://github.com/jeremychone/rust-modql/pull/2) 18 | 19 | 20 | ## Quick Overview 21 | 22 | ```rs 23 | /// This is the model entity, annotated with Fields. 24 | #[derive(Debug, Clone, modql::field::Fields, FromRow, Serialize)] 25 | pub struct Task { 26 | pub id: i64, 27 | pub project_id: i64, 28 | 29 | pub title: String, 30 | pub done: bool, 31 | } 32 | 33 | /// This is a Filter, with the modql::filter::OpVals... properties 34 | #[derive(modql::filter::FilterNodes, Deserialize, Default, Debug)] 35 | pub struct TaskFilter { 36 | project_id: Option, 37 | title: Option, 38 | done: Option, 39 | } 40 | 41 | // -- Parsing JSON representation to TaskFilter 42 | // This condition requires all of these rules to match (AND). 43 | let list_filter: TaskFilter = serde_json::from_value(json! ({ 44 | "project_id": 123, 45 | "title": {"$startsWith": "Hello", "$contains": "World"} , 46 | }))?; 47 | 48 | // -- modql ListOptions 49 | let list_options: modql::filter::ListOptions = 50 | serde_json::from_value(json! ({ 51 | "offset": 0, 52 | "limit": 2, 53 | "order_bys": "!title" // ! for descending 54 | }))?; 55 | 56 | // -- Building a sea-query select query with those condition 57 | // Convert the TaskFilter into sea-query condition 58 | let cond: sea_query::Condition = filter.try_into()?; 59 | let mut query = sea_query::Query::select(); 60 | 61 | // Select only the columns corresponding to the task type. 62 | // This is determined by the modql::field::Fields annotation. 63 | query.from(task_table).columns(Task::sea_column_refs()); 64 | 65 | // Add the condition from the filter 66 | query.cond_where(cond); 67 | 68 | // Apply the list options 69 | list_options.apply_to_sea_query(&mut query); 70 | 71 | // and execute query 72 | let (sql, values) = query.build_sqlx(PostgresQueryBuilder); 73 | let entities = sqlx::query_as_with::<_, E, _>(&sql, values) 74 | .fetch_all(db) 75 | .await?; 76 | ``` 77 | 78 | This crate is instrumental for JSON-RPC or other types of model APIs (e.g., the [joql pattern](https://joql.org)). 79 | 80 | **IMPORTANT** v0.3.x represents the new version of modql, featuring the `with-sea-query` feature set. It is utilized in the [rust10x web-app production code blueprint Episode 02](https://rust10x.com/web-app). 81 | This version is somewhat incompatible with v0.2.x, mainly due to module reorganization. If you are using the rust10x/awesomeapp desktop app, please stick with v0.2.x for the time being. I plan to upgrade the codebase to v0.3.x soon. 82 | 83 | [changelog](CHANGELOG.md) 84 | 85 | 86 | ## `OpVal[Type]` Conditional Operators 87 | 88 | `OpVal[Type]` is a filter unit that allows the expression of an operator on a given value for a specified type. 89 | 90 | The corresponding `OpVals[Type]`, with an "s", is typically used in filter properties, as it permits multiple operators for the same field. 91 | 92 | The basic JSON representation of an `OpVal[Type]` follows the `{field_name: {$operator1: value1, $operator2: value2}}` format. For example: 93 | 94 | ```js 95 | { 96 | "title": {"$startsWith": "Hello", "$contains": "World"} 97 | } 98 | ``` 99 | 100 | This expresses the conditions that both "startsWith" and "contains" must be met. 101 | 102 | The following tables show the list of possible operators for each type. 103 | 104 | ### `OpValString` Operators 105 | 106 | | Operator | Meaning | Example | 107 | |---------------------|--------------------------------------------------------------------------------------------------------|----------------------------------------------------------| 108 | | `$eq` | Exact match with one value | `{name: {"$eq": "Jon Doe"}}` same as `{name: "Jon Doe"}` | 109 | | `$in` | Exact match with within a list of values (or) | `{name: {"$in": ["Alice", "Jon Doe"]}}` | 110 | | `$not` | Exclude any exact match | `{name: {"$not": "Jon Doe"}}` | 111 | | `$notIn` | Exclude any exact withing a list | `{name: {"$notIn": ["Jon Doe"]}}` | 112 | | `$contains` | For string, does a contains | `{name: {"$contains": "Doe"}}` | 113 | | `$containsAny` | For string, match if contained in any of items | `{name: {"$containsAny": ["Doe", "Ali"]}}` | 114 | | `$containsAll` | For string, match if all items are in the src | `{name: {"$containsAll": ["Hello", "World"]}}` | 115 | | `$notContains` | Does not contain | `{name: {"$notContains": "Doe"}}` | 116 | | `$notContainsAny` | Does not call any of (none is contained) | `{name: {"$notContainsAny": ["Doe", "Ali"]}}` | 117 | | `$startsWith` | For string, does a startsWith | `{name: {"$startsWith": "Jon"}}` | 118 | | `$startsWithAny` | For string, match if startsWith in any of items | `{name: {"$startsWithAny": ["Jon", "Al"]}}` | 119 | | `$notStartsWith` | Does not start with | `{name: {"$notStartsWith": "Jon"}}` | 120 | | `$notStartsWithAny` | Does not start with any of the items | `{name: {"$notStartsWithAny": ["Jon", "Al"]}}` | 121 | | `$endsWith` | For string, does and end with | `{name: {"$endsWithAny": "Doe"}}` | 122 | | `$endsWithAny` | For string, does a contains (or) | `{name: {"$endsWithAny": ["Doe", "ice"]}}` | 123 | | `$notEndsWith` | Does not end with | `{name: {"$notEndsWithAny": "Doe"}}` | 124 | | `$notEndsWithAny` | Does not end with any of the items | `{name: {"$notEndsWithAny": ["Doe", "ice"]}}` | 125 | | `$lt` | Lesser Than | `{name: {"$lt": "C"}}` | 126 | | `$lte` | Lesser Than or = | `{name: {"$lte": "C"}}` | 127 | | `$gt` | Greater Than | `{name: {"$gt": "J"}}` | 128 | | `$gte` | Greater Than or = | `{name: {"$gte": "J"}}` | 129 | | `$null` | If the value is null | `{name: {"$null": true}}` | 130 | | `$containsCi` | For string, does a contains in a case-insensitive way | `{name: {"$containsCi": "doe"}}` | 131 | | `$notContainsCi` | Does not contain in a case-insensitive way | `{name: {"$notContainsCi": "doe"}}` | 132 | | `$startsWithCi` | For string, does a startsWith in a case-insensitive way | `{name: {"$startsWithCi": "jon"}}` | 133 | | `$notStartsWithCi` | Does not start with in a case-insensitive way | `{name: {"$notStartsWithCi": "jon"}}` | 134 | | `$endsWithCi` | For string, does an endsWith in a case-insensitive way | `{name: {"$endsWithCi": "doe"}}` | 135 | | `$notEndsWithCi` | Does not end with in a case-insensitive way | `{name: {"$notEndsWithCi": "doe"}}` | 136 | | `$ilike` | For string, does a contains in a case-insensitive way. Needs `with-ilike` flag enabled in `Cargo.toml` | `{name: {"$ilike": "DoE"}}` | 137 | 138 | ### `OpValInt32, OpValInt64, OpValFloat64` Operators 139 | 140 | | Operator | Meaning | Example | 141 | |----------|-----------------------------------------------|------------------------------------------| 142 | | `$eq` | Exact match with one value | `{age: {"$eq": 24}}` same as `{age: 24}` | 143 | | `$in` | Exact match with within a list of values (or) | `{age: {"$in": [23, 24]}}` | 144 | | `$not` | Exclude any exact match | `{age: {"$not": 24}}` | 145 | | `$notIn` | Exclude any exact withing a list | `{age: {"$notIn": [24]}}` | 146 | | `$lt` | Lesser Than | `{age: {"$lt": 30}}` | 147 | | `$lte` | Lesser Than or = | `{age: {"$lte": 30}}` | 148 | | `$gt` | Greater Than | `{age: {"$gt": 30}}` | 149 | | `$gte` | Greater Than or = | `{age: {"$gte": 30}}` | 150 | | `$null` | If the value is null | `{name: {"$null": true}}` | 151 | 152 | ### `OpValBool` Operators 153 | 154 | | Operator | Meaning | Example | 155 | |----------|----------------------------|----------------------------------------------| 156 | | `$eq` | Exact match with one value | `{dev: {"$eq": true}}` same as `{dev: true}` | 157 | | `$not` | Exclude any exact match | `{dev: {"$not": false}}` | 158 | | `$null` | If the value is null | `{name: {"$null": true}}` | 159 | 160 | 161 | ## More Info 162 | 163 | - `modql::filter` - Delivers a declarative structure that can be deserialized from JSON. 164 | - `modql::field` - Provides a method get field information on a struct. The `with-sea-query` feature add `sea-query` compatible data structure from standard structs and derive. 165 | 166 | ## `#[derive(modql::field::Fields)` provide the following 167 | 168 | - `Task::field_names()` returns the property names of the struct. It can be overridden with the `#[field(name="another_name")]` property attribute. 169 | - `Task::field_refs()` returns `FieldRef { name: &'static str, rel: Option<&'static str>}` for the properties. `rel` acts like the table name. It can be set as `#[modql(rel="some_table_name")]` at the struct level, or `#[field(rel="special_rel_name")]` at the field level. 170 | 171 | When compiled with the `with-sea-query` feature, these additional functions are available on the struct: 172 | 173 | - `Task::sea_column_refs() -> Vec`: Constructs `sea-query` select queries (with `rel` as the table, and `name` as the column name). 174 | - `Task::sea_idens() -> Vec`: Constructs `sea-query` select queries, suited for simpler cases. (similar to `::field_names()` but returns the sea-query `DynIden`). 175 | - `task.all_sea_fields().for_sea_insert() -> (Vec, Vec)`: Used for `sea-query` inserts. 176 | - `task.all_sea_fields().for_sea_update() -> impl Iterator`: Used for `sea-query` updates. 177 | 178 | Additionally, it offers: 179 | 180 | - `task.not_none_fields()`: Operates similarly to the above, but only for fields where their `Option` is not `None`. 181 | 182 | ### Rust types 183 | 184 | On the Rust side, this can be expressed like this: 185 | 186 | ```rs 187 | pub type Result = core::result::Result; 188 | pub type Error = Box; // For early dev. 189 | use modql::filter::{FilterGroups, FilterNode, OpValtring}; 190 | 191 | fn main() -> Result<()> { 192 | let filter_nodes: Vec = vec![ 193 | ( 194 | "title", 195 | OpValtring::ContainsAny(vec!["Hello".to_string(), "welcome".to_string()]), 196 | ) 197 | .into(), 198 | ("done", true).into(), 199 | ]; 200 | let filter_groups: FilterGroups = filter_nodes.into(); 201 | 202 | println!("filter_groups:\n{filter_groups:#?}"); 203 | 204 | Ok(()) 205 | } 206 | ``` 207 | 208 | 209 | A Model or Store layer can take the `filter_groups` and serialize them into their DSL (e.g., SQL for databases). 210 | 211 | The Filter structure is as follows: 212 | 213 | - `FilterGroups` is the top level and consists of multiple `FilterGroup` elements. `FilterGroup` elements are intended to be executed with an `OR` operation between them. 214 | - Each `FilterGroup` contains a vector of `FilterNode` elements, which are intended to be executed with an `AND` operation. 215 | - `FilterNode` contains a `rel` (not used yet), `name` which represents the property name from where the value originates, and a `Vec`, representing the Operator Value. 216 | - `OpVal` is an enum for type-specific `OpVal[Type]` entities, such as `OpValString` that holds the specific operation for that type along with the associated pattern value. 217 | 218 |
219 | 220 | [GitHub Repo](https://github.com/jeremychone/rust-modql) 221 | -------------------------------------------------------------------------------- /examples/c02-rusqlite-sea-query-join.rs: -------------------------------------------------------------------------------- 1 | mod support; 2 | 3 | use crate::support::rusqlite_utils::{create_schema, seed_data}; 4 | use crate::support::Result; 5 | use modql::field::HasFields; 6 | use modql::{SIden, SqliteFromRow}; 7 | use modql_macros::Fields; 8 | use pretty_sqlite::pretty_table; 9 | use rusqlite::Connection; 10 | use sea_query::{Expr, IntoColumnRef, JoinType, Query, SqliteQueryBuilder}; 11 | use sea_query_rusqlite::RusqliteBinder; 12 | 13 | // cargo run --example c02-rusqlite-sea-query-join --all-features 14 | 15 | fn main() -> Result<()> { 16 | let conn = Connection::open_in_memory()?; // for file: Connection::open(path)? 17 | create_schema(&conn)?; 18 | seed_data(&conn)?; 19 | 20 | // let content = pretty_table(&conn, "project")?; 21 | // println!("Project table:\n{content}"); 22 | 23 | // let content = pretty_table(&conn, "task")?; 24 | // println!("Task table:\n{content}"); 25 | 26 | let mut query = Query::select(); 27 | let task_iden = SIden("task"); 28 | let project_iden = SIden("project"); 29 | query.from(task_iden).join( 30 | JoinType::LeftJoin, 31 | project_iden, 32 | Expr::col((task_iden, SIden("project_id")).into_column_ref()) 33 | .equals((project_iden, SIden("id")).into_column_ref()), 34 | ); 35 | let metas = Task::field_metas(); 36 | for &meta in metas.iter() { 37 | meta.sea_apply_select_column(&mut query); 38 | } 39 | 40 | let (sql, values) = query.build_rusqlite(SqliteQueryBuilder); 41 | println!("SQL: {sql}\n"); 42 | 43 | let mut stmt = conn.prepare(&sql)?; 44 | let iter = stmt.query_and_then(&*values.as_params(), Task::sqlite_from_row)?; 45 | let tasks = iter.collect::, _>>()?; 46 | for task in tasks { 47 | println!("Task: {task:?}"); 48 | } 49 | 50 | Ok(()) 51 | } 52 | 53 | #[derive(Debug, Fields, SqliteFromRow)] 54 | #[modql(rel = "project")] 55 | struct Project { 56 | id: i64, 57 | name: String, 58 | } 59 | 60 | #[derive(Debug, Fields, SqliteFromRow)] 61 | #[modql(rel = "task")] 62 | struct Task { 63 | title: String, 64 | desc: String, 65 | project_id: i64, 66 | 67 | #[field(rel = "project", name = "name")] 68 | project_name: String, 69 | } 70 | -------------------------------------------------------------------------------- /examples/c02-rusqlite-sea-query-select.rs: -------------------------------------------------------------------------------- 1 | mod support; 2 | 3 | use crate::support::rusqlite_utils::{create_schema, seed_data}; 4 | use crate::support::Result; 5 | use modql::field::HasFields; 6 | use modql::{SIden, SqliteFromRow}; 7 | use modql_macros::Fields; 8 | use pretty_sqlite::pretty_table; 9 | use rusqlite::Connection; 10 | use sea_query::{Query, SqliteQueryBuilder}; 11 | use sea_query_rusqlite::RusqliteBinder; 12 | 13 | // cargo run --example c02-rusqlite-sea-query-select --all-features 14 | 15 | fn main() -> Result<()> { 16 | let conn = Connection::open_in_memory()?; // for file: Connection::open(path)? 17 | create_schema(&conn)?; 18 | seed_data(&conn)?; 19 | 20 | // let content = pretty_table(&conn, "project")?; 21 | // println!("Project table:\n{content}"); 22 | 23 | // let content = pretty_table(&conn, "task")?; 24 | // println!("Task table:\n{content}"); 25 | 26 | let mut query = Query::select(); 27 | query.from(SIden("task")); 28 | let metas = Task::field_metas(); 29 | for &meta in metas.iter() { 30 | meta.sea_apply_select_column(&mut query); 31 | } 32 | 33 | let (sql, values) = query.build_rusqlite(SqliteQueryBuilder); 34 | println!("Sql: {sql}\n"); 35 | 36 | let mut stmt = conn.prepare(&sql)?; 37 | let iter = stmt.query_and_then(&*values.as_params(), Task::sqlite_from_row)?; 38 | let tasks = iter.collect::, _>>()?; 39 | for task in tasks { 40 | println!("Task: {task:?}"); 41 | } 42 | 43 | Ok(()) 44 | } 45 | 46 | #[derive(Debug, Fields, SqliteFromRow)] 47 | struct Project { 48 | id: i64, 49 | name: String, 50 | } 51 | 52 | #[derive(Debug, Fields, SqliteFromRow)] 53 | struct Task { 54 | title: String, 55 | desc: String, 56 | project_id: i64, 57 | } 58 | -------------------------------------------------------------------------------- /examples/c03-sqlx-sea-query-join.rs: -------------------------------------------------------------------------------- 1 | mod support; 2 | 3 | use crate::support::sqlx_utils::{create_schema, seed_data}; 4 | use crate::support::Result; 5 | use modql::field::HasFields; 6 | use modql::{SIden, SqliteFromRow}; 7 | use modql_macros::Fields; 8 | use pretty_sqlite::pretty_table; 9 | use rusqlite::Connection; 10 | use sea_query::{Expr, IntoColumnRef, JoinType, Query, SqliteQueryBuilder}; 11 | use sea_query_binder::SqlxBinder; 12 | use sea_query_rusqlite::RusqliteBinder; 13 | use sqlx::{FromRow, Row, SqlitePool}; 14 | 15 | // cargo run --example c03-sqlx-sea-query-join --all-features 16 | 17 | #[tokio::main] 18 | async fn main() -> Result<()> { 19 | let sqlx_pool = SqlitePool::connect("sqlite::memory:").await.unwrap(); 20 | 21 | create_schema(&sqlx_pool).await?; 22 | seed_data(&sqlx_pool).await?; 23 | 24 | let mut query = Query::select(); 25 | let task_iden = SIden("task"); 26 | let project_iden = SIden("project"); 27 | query.from(task_iden).join( 28 | JoinType::LeftJoin, 29 | project_iden, 30 | Expr::col((task_iden, SIden("project_id")).into_column_ref()) 31 | .equals((project_iden, SIden("id")).into_column_ref()), 32 | ); 33 | let metas = Task::field_metas(); 34 | for &meta in metas.iter() { 35 | meta.sea_apply_select_column(&mut query); 36 | } 37 | 38 | let (sql, values) = query.build_sqlx(SqliteQueryBuilder); 39 | println!("Sql: {sql}\n"); 40 | 41 | let tasks = sqlx::query_as_with::<_, Task, _>(&sql, values).fetch_all(&sqlx_pool).await?; 42 | 43 | for task in tasks { 44 | println!("Task: {:?}", task); 45 | } 46 | 47 | Ok(()) 48 | } 49 | 50 | #[derive(Debug, Fields, FromRow)] 51 | #[modql(rel = "project")] 52 | struct Project { 53 | id: i64, 54 | name: String, 55 | } 56 | 57 | #[derive(Debug, Fields, FromRow)] 58 | #[modql(rel = "task")] 59 | struct Task { 60 | title: String, 61 | desc: String, 62 | project_id: i64, 63 | 64 | #[field(rel = "project", name = "name")] 65 | project_name: String, 66 | } 67 | -------------------------------------------------------------------------------- /examples/support/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod rusqlite_utils; 2 | pub mod sqlx_utils; 3 | 4 | pub type Result = core::result::Result; 5 | pub type Error = Box; // For early dev. 6 | -------------------------------------------------------------------------------- /examples/support/rusqlite_utils.rs: -------------------------------------------------------------------------------- 1 | use super::Result; 2 | use rusqlite::Connection; 3 | 4 | pub fn create_schema(conn: &Connection) -> Result<()> { 5 | conn.execute( 6 | "CREATE TABLE IF NOT EXISTS project ( 7 | id INTEGER PRIMARY KEY AUTOINCREMENT, 8 | name TEXT 9 | ) STRICT", 10 | (), // empty list of parameters. 11 | )?; 12 | 13 | conn.execute( 14 | "CREATE TABLE IF NOT EXISTS task ( 15 | id INTEGER PRIMARY KEY AUTOINCREMENT, 16 | project_id INTEGER, 17 | title TEXT, 18 | desc TEXT 19 | ) STRICT", 20 | (), // empty list of parameters. 21 | )?; 22 | 23 | Ok(()) 24 | } 25 | 26 | pub fn seed_data(conn: &Connection) -> Result<()> { 27 | let suffixes = &["A", "B"]; 28 | 29 | for suffix in suffixes { 30 | let project_name = format!("Project {suffix}"); 31 | let mut stmt = conn.prepare("INSERT INTO project (name) VALUES (?1) RETURNING id")?; 32 | let project_id = stmt.query_row((&project_name,), |r| r.get::<_, i64>(0))?; 33 | 34 | for i in 1..=3 { 35 | let title = format!("Task {suffix}.{i}"); 36 | let desc = format!("Description {suffix}.{i}"); 37 | conn.execute( 38 | "INSERT INTO task (project_id, title, desc) VALUES (?1, ?2, ?3)", 39 | (project_id, &title, &desc), 40 | )?; 41 | } 42 | } 43 | 44 | Ok(()) 45 | } 46 | -------------------------------------------------------------------------------- /examples/support/sqlx_utils.rs: -------------------------------------------------------------------------------- 1 | use sqlx::{sqlite::SqlitePool, Result}; 2 | 3 | pub async fn create_schema(pool: &SqlitePool) -> Result<()> { 4 | sqlx::query( 5 | "CREATE TABLE IF NOT EXISTS project ( 6 | id INTEGER PRIMARY KEY AUTOINCREMENT, 7 | name TEXT 8 | ) STRICT", 9 | ) 10 | .execute(pool) 11 | .await?; 12 | 13 | sqlx::query( 14 | "CREATE TABLE IF NOT EXISTS task ( 15 | id INTEGER PRIMARY KEY AUTOINCREMENT, 16 | project_id INTEGER, 17 | title TEXT, 18 | desc TEXT 19 | ) STRICT", 20 | ) 21 | .execute(pool) 22 | .await?; 23 | 24 | Ok(()) 25 | } 26 | 27 | pub async fn seed_data(pool: &SqlitePool) -> Result<()> { 28 | let suffixes = &["A", "B"]; 29 | 30 | for suffix in suffixes { 31 | let project_name = format!("Project {suffix}"); 32 | let row: (i64,) = sqlx::query_as("INSERT INTO project (name) VALUES (?) RETURNING id") 33 | .bind(&project_name) 34 | .fetch_one(pool) 35 | .await?; 36 | let project_id = row.0; 37 | 38 | for i in 1..=3 { 39 | let title = format!("Task {suffix}.{i}"); 40 | let desc = format!("Description {suffix}.{i}"); 41 | sqlx::query("INSERT INTO task (project_id, title, desc) VALUES (?, ?, ?)") 42 | .bind(project_id) 43 | .bind(&title) 44 | .bind(&desc) 45 | .execute(pool) 46 | .await?; 47 | } 48 | } 49 | 50 | Ok(()) 51 | } 52 | -------------------------------------------------------------------------------- /modql-macros/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "modql-macros" 3 | version = "0.4.1" 4 | authors = ["jeremy.chone@gmail.com"] 5 | edition = "2021" 6 | description = "Macros for modql. Not intended to be used directly." 7 | license = "MIT OR Apache-2.0" 8 | categories = ["data-structures"] 9 | homepage = "https://github.com/modql/rust-modql" 10 | repository = "https://github.com/modql/rust-modql" 11 | 12 | [lints] 13 | workspace = true 14 | 15 | [features] 16 | with-sea-query = ["sea-query"] 17 | with-rusqlite = ["rusqlite"] 18 | 19 | [dependencies] 20 | quote = "1" 21 | syn = {version = "2", features = ["full"]} 22 | proc-macro2 = "1" 23 | 24 | # -- For features 25 | sea-query = { workspace = true, optional = true } 26 | rusqlite = { workspace = true, optional = true } 27 | 28 | [lib] 29 | proc-macro = true 30 | -------------------------------------------------------------------------------- /modql-macros/src/derives_field/derive_field_sea_value.rs: -------------------------------------------------------------------------------- 1 | use proc_macro::TokenStream; 2 | use proc_macro2::Ident; 3 | use quote::quote; 4 | use syn::{parse_macro_input, DataEnum, DataStruct, DeriveInput, Fields, Type}; 5 | 6 | // TODO: Needs to assert that variants do not have any data 7 | pub(crate) fn derive_field_sea_value_inner(input: TokenStream) -> TokenStream { 8 | // Parse the input tokens into a syntax tree 9 | let input = parse_macro_input!(input as DeriveInput); 10 | 11 | let name: Ident = input.ident; 12 | 13 | // Build the match arms and get the first variant 14 | // Note: At this point, we do not nseed the first_variant anymore since we return 15 | // `sea_query::Value::String(None)` for nullable, but we keep the code for future 16 | // reference. 17 | let expanded = match input.data { 18 | syn::Data::Enum(data) => process_enum(name, data), 19 | syn::Data::Struct(data) => process_struct(name, data), 20 | _ => panic!("Field can only be used with enums and tuple struct for now"), 21 | }; 22 | 23 | // Return the generated token stream 24 | TokenStream::from(expanded) 25 | } 26 | 27 | fn process_struct(name: Ident, data: DataStruct) -> proc_macro2::TokenStream { 28 | let first_tuple_field = match data.fields { 29 | Fields::Unnamed(fields) if fields.unnamed.len() == 1 => fields.unnamed.into_iter().next().unwrap(), 30 | _ => panic!("Expected a tuple struct with one field"), 31 | }; 32 | 33 | let field_type = first_tuple_field.ty; 34 | 35 | // NOTE: It is important to extract the single TypePath from a potential TypeGroup 36 | // to increase resilience with certain declarative macros (e.g., derive_aliases_...!{ struct ...}). 37 | let field_type_path = match field_type { 38 | Type::Group(group) => match *group.elem { 39 | Type::Path(p) => p, 40 | _ => panic!("Unsupported type... TypeGroup.elem is not a path"), 41 | }, 42 | Type::Path(p) => p, 43 | _ => panic!("Unsupported type... not Type::Path or Type::Group"), 44 | }; 45 | 46 | let value_variant = match field_type_path.path.get_ident() { 47 | Some(ident) => match ident.to_string().as_str() { 48 | "bool" => quote! { Bool }, 49 | "i8" => quote! { TinyInt }, 50 | "i16" => quote! { SmallInt }, 51 | "i32" => quote! { Int }, 52 | "i64" => quote! { BigInt }, 53 | "u8" => quote! { TinyUnsigned }, 54 | "u16" => quote! { SmallUnsigned }, 55 | "u32" => quote! { Unsigned }, 56 | "u64" => quote! { BigUnsigned }, 57 | "f32" => quote! { Float }, 58 | "f64" => quote! { Double }, 59 | "String" => quote! { String }, 60 | "char" => quote! { Char }, 61 | // TODO: add more type support 62 | _ => panic!("Unsupported type... {:?}", ident), 63 | }, 64 | None => panic!("Unsupported type... no ident found"), 65 | }; 66 | 67 | // Determine the appropriate Value variant based on the type of the field 68 | // let value_variant = match field_type { 69 | // Type::Path(p) if p.path.is_ident("bool") => quote! { Bool }, 70 | // Type::Path(p) if p.path.is_ident("i8") => quote! { TinyInt }, 71 | // Type::Path(p) if p.path.is_ident("i16") => quote! { SmallInt }, 72 | // Type::Path(p) if p.path.is_ident("i32") => quote! { Int }, 73 | // Type::Path(p) if p.path.is_ident("i64") => quote! { BigInt }, 74 | // Type::Path(p) if p.path.is_ident("u8") => quote! { TinyUnsigned }, 75 | // Type::Path(p) if p.path.is_ident("u16") => quote! { SmallUnsigned }, 76 | // Type::Path(p) if p.path.is_ident("u32") => quote! { Unsigned }, 77 | // Type::Path(p) if p.path.is_ident("u64") => quote! { BigUnsigned }, 78 | // Type::Path(p) if p.path.is_ident("f32") => quote! { Float }, 79 | // Type::Path(p) if p.path.is_ident("f64") => quote! { Double }, 80 | // Type::Path(p) if p.path.is_ident("String") => quote! { String }, 81 | // Type::Path(p) if p.path.is_ident("char") => quote! { Char }, 82 | // Type::Group(g) => panic!("Unsupported type group... {:?} ...", g), 83 | // // TODO: Add more sea-query types 84 | // _ => panic!("Unsupported type... {:?} ...", field_type), 85 | // }; 86 | 87 | let expanded = quote! { 88 | impl From<#name> for sea_query::Value { 89 | fn from(value: #name) -> Self { 90 | Self::#value_variant(Some(value.0)) 91 | } 92 | } 93 | 94 | impl sea_query::Nullable for #name { 95 | fn null() -> sea_query::Value { 96 | sea_query::Value::#value_variant(None) 97 | } 98 | } 99 | }; 100 | 101 | expanded 102 | } 103 | 104 | fn process_enum(name: Ident, data_enum: DataEnum) -> proc_macro2::TokenStream { 105 | let mut first_variant = None; 106 | let arms = data_enum 107 | .variants 108 | .iter() 109 | .map(|variant| { 110 | let variant_name = &variant.ident; 111 | let variant_name_str = variant_name.to_string(); 112 | if first_variant.is_none() { 113 | first_variant = Some(variant_name.clone()); 114 | } 115 | quote! { 116 | #name::#variant_name => #variant_name_str.into(), 117 | } 118 | }) 119 | .collect::>(); 120 | 121 | // Note: Note needed anymore, but keep for code example. 122 | // let first_variant = first_variant.expect("Enum must have at least one variant"); 123 | 124 | // Generate the final token stream 125 | let expanded = quote! { 126 | impl From<#name> for sea_query::Value { 127 | fn from(val: #name) -> Self { 128 | match val { 129 | #(#arms)* 130 | } 131 | } 132 | } 133 | 134 | impl sea_query::Nullable for #name { 135 | fn null() -> sea_query::Value { 136 | // #name::#first_variant.into() 137 | sea_query::Value::String(None) 138 | } 139 | } 140 | }; 141 | 142 | expanded 143 | } 144 | -------------------------------------------------------------------------------- /modql-macros/src/derives_field/derive_fields.rs: -------------------------------------------------------------------------------- 1 | use crate::utils::modql_field::ModqlFieldProp; 2 | use crate::utils::struct_modql_attr::{get_struct_modql_props, StructModqlFieldProps}; 3 | use crate::utils::{get_struct_fields, modql_field}; 4 | use proc_macro::TokenStream; 5 | use proc_macro2::{Ident, Span}; 6 | use quote::quote; 7 | use syn::{parse_macro_input, DeriveInput}; 8 | 9 | pub(crate) fn derive_fields_inner(input: TokenStream) -> TokenStream { 10 | let ast = parse_macro_input!(input as DeriveInput); 11 | let fields = get_struct_fields(&ast); 12 | 13 | let struct_name = &ast.ident; 14 | 15 | // -- Collect Elements 16 | // Properties for all fields (with potential additional info with #[field(...)]) 17 | let field_props = modql_field::get_modql_field_props(fields); 18 | let struct_modql_prop = get_struct_modql_props(&ast).unwrap(); 19 | 20 | // Will be "" if none (this if for the struct #[modql(table = ...)]) 21 | 22 | let impl_has_fields = impl_has_fields(struct_name, &struct_modql_prop, &field_props); 23 | 24 | let impl_names_as_consts = if let Some(names_as_consts) = struct_modql_prop.names_as_consts.as_deref() { 25 | // 26 | impl_names_as_consts(struct_name, &field_props, names_as_consts) 27 | } else { 28 | quote! {} 29 | }; 30 | 31 | let impl_sea_fields = if cfg!(feature = "with-sea-query") { 32 | impl_has_sea_fields(struct_name, &struct_modql_prop, &field_props) 33 | } else { 34 | quote! {} 35 | }; 36 | 37 | let output = quote! { 38 | #impl_has_fields 39 | 40 | #impl_names_as_consts 41 | 42 | #impl_sea_fields 43 | }; 44 | 45 | output.into() 46 | } 47 | 48 | fn impl_names_as_consts( 49 | struct_name: &Ident, 50 | field_props: &[ModqlFieldProp<'_>], 51 | prop_name_prefix: &str, 52 | ) -> proc_macro2::TokenStream { 53 | // If prefix not empty, amek sure it ends with `_` 54 | let prop_name_prefix = if !prop_name_prefix.is_empty() && !prop_name_prefix.ends_with('_') { 55 | format!("{prop_name_prefix}_") 56 | } else { 57 | prop_name_prefix.to_string() 58 | }; 59 | 60 | let consts = field_props.iter().map(|field| { 61 | let prop_name = &field.prop_name; 62 | let const_name = format!("{}{}", prop_name_prefix, prop_name.to_uppercase()); 63 | let const_name = Ident::new(&const_name, Span::call_site()); 64 | 65 | let name = &field.name; 66 | quote! { 67 | pub const #const_name: &'static str = #name; 68 | } 69 | }); 70 | 71 | quote! { 72 | impl #struct_name { 73 | #(#consts)* 74 | } 75 | } 76 | } 77 | 78 | fn impl_has_fields( 79 | struct_name: &Ident, 80 | struct_modql_prop: &StructModqlFieldProps, 81 | field_props: &[ModqlFieldProp<'_>], 82 | ) -> proc_macro2::TokenStream { 83 | let props_all_names: Vec<&String> = field_props.iter().map(|p| &p.name).collect(); 84 | 85 | let struct_rel = struct_modql_prop.rel.as_ref(); 86 | 87 | // -- Build FieldRef quotes 88 | let props_field_refs = field_props.iter().map(|field_prop| { 89 | let name = field_prop.name.to_string(); 90 | let rel = field_prop.rel.as_ref().or(struct_rel); 91 | let rel = match rel { 92 | Some(rel) => quote! { Some(#rel)}, 93 | None => quote! { None }, 94 | }; 95 | quote! {&modql::field::FieldRef{rel: #rel, name: #name}} 96 | }); 97 | 98 | // -- Build the FieldMeta quotes 99 | let props_field_metas = field_props.iter().map(|field_prop| { 100 | // This below is resolved in the FieldMeta implemntation (same logic) 101 | // let name = field_prop.name.to_string(); 102 | 103 | let prop_name = field_prop.prop_name.to_string(); 104 | 105 | let attr_name = match field_prop.attr_name.as_ref() { 106 | Some(attr_name) => quote! { Some(#attr_name)}, 107 | None => quote! { None }, 108 | }; 109 | 110 | let field_rel = field_prop.rel.as_ref(); 111 | 112 | let is_struct_rel = match (struct_rel, field_rel) { 113 | (Some(_), None) => true, 114 | (Some(struct_rel), Some(field_rel)) => struct_rel == field_rel, 115 | _ => false, 116 | }; 117 | 118 | let rel = field_prop.rel.as_ref().or(struct_rel); 119 | let rel = match rel { 120 | Some(rel) => quote! { Some(#rel)}, 121 | None => quote! { None }, 122 | }; 123 | let cast_as = match &field_prop.cast_as { 124 | Some(cast_as) => quote! { Some(#cast_as)}, 125 | None => quote! { None }, 126 | }; 127 | let is_option = field_prop.is_option; 128 | 129 | quote! {&modql::field::FieldMeta{ 130 | rel: #rel, 131 | is_struct_rel: #is_struct_rel, 132 | prop_name: #prop_name, 133 | attr_name: #attr_name, 134 | cast_as: #cast_as, 135 | is_option: #is_option, 136 | } 137 | } 138 | }); 139 | 140 | let output = quote! { 141 | 142 | impl modql::field::HasFields for #struct_name { 143 | 144 | fn field_names() -> &'static [&'static str] { 145 | &[#( 146 | #props_all_names, 147 | )*] 148 | } 149 | 150 | fn field_refs() -> &'static [&'static modql::field::FieldRef] { 151 | &[#( 152 | #props_field_refs, 153 | )*] 154 | } 155 | 156 | fn field_metas() -> &'static modql::field::FieldMetas { 157 | static METAS: &[&modql::field::FieldMeta] = &[#( 158 | #props_field_metas, 159 | )*]; 160 | 161 | static METAS_HOLDER: modql::field::FieldMetas = modql::field::FieldMetas::new(METAS); 162 | 163 | &METAS_HOLDER 164 | } 165 | 166 | } 167 | }; 168 | 169 | output 170 | } 171 | 172 | fn impl_has_sea_fields( 173 | struct_name: &Ident, 174 | struct_modql_prop: &StructModqlFieldProps, 175 | field_props: &[ModqlFieldProp<'_>], 176 | ) -> proc_macro2::TokenStream { 177 | let prop_all_names: Vec<&String> = field_props.iter().map(|p| &p.name).collect(); 178 | 179 | // this will repeat the struct table name for all fields. 180 | let prop_all_rels: Vec = field_props 181 | .iter() 182 | .map(|p| { 183 | p.rel 184 | .as_ref() 185 | .map(|t| t.to_string()) 186 | .unwrap_or_else(|| struct_modql_prop.rel.as_ref().map(|s| s.to_string()).unwrap_or_default()) 187 | }) 188 | .collect(); 189 | 190 | fn field_options_quote(mfield_prop: &ModqlFieldProp) -> proc_macro2::TokenStream { 191 | if let Some(cast_as) = &mfield_prop.cast_as { 192 | quote! { modql::field::FieldOptions { cast_as: Some(#cast_as.to_string()) } } 193 | } else { 194 | quote! { modql::field::FieldOptions { cast_as: None } } 195 | } 196 | } 197 | 198 | // -- all_fields() quotes! 199 | let all_fields_quotes = field_props.iter().map(|p| { 200 | let name = &p.name; 201 | let field_options_q = field_options_quote(p); 202 | let ident = p.ident; 203 | 204 | quote! { 205 | ff.push( 206 | modql::field::SeaField::new_with_options(modql::SIden(#name), self.#ident.into(), #field_options_q) 207 | ); 208 | } 209 | }); 210 | 211 | // -- The not_none_sea_fields quotes! 212 | let not_none_fields_quotes = field_props.iter().map(|p| { 213 | let name = &p.name; 214 | let field_options_q = field_options_quote(p); 215 | let ident = p.ident; 216 | 217 | if p.is_option { 218 | quote! { 219 | if let Some(val) = self.#ident { 220 | ff.push( 221 | modql::field::SeaField::new_with_options(modql::SIden(#name), val.into(), #field_options_q) 222 | ); 223 | } 224 | } 225 | } else { 226 | quote! { 227 | ff.push( 228 | modql::field::SeaField::new_with_options(modql::SIden(#name), self.#ident.into(), #field_options_q) 229 | ); 230 | } 231 | } 232 | }); 233 | 234 | // -- Compose the final code 235 | let output = quote! { 236 | 237 | impl modql::field::HasSeaFields for #struct_name { 238 | 239 | fn not_none_sea_fields(self) -> modql::field::SeaFields { 240 | let mut ff: Vec = Vec::new(); 241 | #(#not_none_fields_quotes)* 242 | modql::field::SeaFields::new(ff) 243 | } 244 | 245 | fn all_sea_fields(self) -> modql::field::SeaFields { 246 | let mut ff: Vec = Vec::new(); 247 | #(#all_fields_quotes)* 248 | modql::field::SeaFields::new(ff) 249 | } 250 | 251 | fn sea_idens() -> Vec> { 252 | vec![#( 253 | sea_query::IntoIden::into_iden(modql::SIden(#prop_all_names)), 254 | )*] 255 | } 256 | 257 | fn sea_column_refs() -> Vec { 258 | use sea_query::IntoIden; 259 | use sea_query::ColumnRef; 260 | use modql::SIden; 261 | 262 | let mut v = Vec::new(); 263 | 264 | // NOTE: There's likely a more elegant solution, but this approach is semantically correct. 265 | #( 266 | let col_ref = if #prop_all_rels == "" { 267 | ColumnRef::Column(SIden(#prop_all_names).into_iden()) 268 | } else { 269 | ColumnRef::TableColumn( 270 | SIden(#prop_all_rels).into_iden(), 271 | SIden(#prop_all_names).into_iden()) 272 | }; 273 | v.push(col_ref); 274 | )* 275 | v 276 | } 277 | 278 | fn sea_column_refs_with_rel(rel_iden: impl sea_query::IntoIden) -> Vec { 279 | use sea_query::IntoIden; 280 | use sea_query::ColumnRef; 281 | use modql::SIden; 282 | 283 | let rel_iden = rel_iden.into_iden(); 284 | 285 | let mut v = Vec::new(); 286 | 287 | // NOTE: There's likely a more elegant solution, but this approach is semantically correct. 288 | #( 289 | let col_ref = 290 | ColumnRef::TableColumn( 291 | rel_iden.clone(), 292 | SIden(#prop_all_names).into_iden()); 293 | 294 | v.push(col_ref); 295 | )* 296 | v 297 | } 298 | } 299 | }; 300 | 301 | output 302 | } 303 | -------------------------------------------------------------------------------- /modql-macros/src/derives_field/mod.rs: -------------------------------------------------------------------------------- 1 | mod derive_fields; 2 | pub(crate) use derive_fields::*; 3 | 4 | #[cfg(feature = "with-sea-query")] 5 | mod derive_field_sea_value; 6 | #[cfg(feature = "with-sea-query")] 7 | pub(crate) use derive_field_sea_value::*; 8 | -------------------------------------------------------------------------------- /modql-macros/src/derives_filter/mod.rs: -------------------------------------------------------------------------------- 1 | mod utils; 2 | 3 | use crate::derives_filter::utils::get_filter_field_attr; 4 | use crate::utils::struct_modql_attr::get_struct_modql_props; 5 | use crate::utils::{get_struct_fields, get_type_name}; 6 | use proc_macro::TokenStream; 7 | use quote::quote; 8 | use syn::{parse_macro_input, DeriveInput, Ident}; 9 | 10 | pub fn derive_filter_nodes_inner(input: TokenStream) -> TokenStream { 11 | let ast = parse_macro_input!(input as DeriveInput); 12 | 13 | //// get struct name and fields 14 | let struct_name = &ast.ident; 15 | let fields = get_struct_fields(&ast); 16 | 17 | let struct_attrs = get_struct_modql_props(&ast).unwrap(); 18 | 19 | //// Properties to be collected 20 | let mut props: Vec<&Option> = Vec::new(); // not needed for now. 21 | let mut props_opval_idents: Vec<&Ident> = Vec::new(); 22 | let mut props_opval_rels: Vec = Vec::new(); 23 | let mut props_opval_to_sea_holder_fn_build: Vec = Vec::new(); 24 | let mut props_filter_node_options: Vec = Vec::new(); 25 | 26 | for field in fields.named.iter() { 27 | // NOTE: By macro limitation, we can do only type name match and it would not support type alias 28 | // For now, assume Option is use as is, not even in a fully qualified way. 29 | // We can add other variants of Option if proven needed 30 | let type_name = get_type_name(field); 31 | 32 | // NOTE: For now only convert the properties of types with option and OpVal 33 | if type_name.starts_with("Option ") && type_name.contains("OpVal") { 34 | if let Some(ident) = field.ident.as_ref() { 35 | props_opval_idents.push(ident); 36 | 37 | // -- Extract the attributes 38 | let modql_field_attr = get_filter_field_attr(field).unwrap(); 39 | 40 | // -- rel 41 | let block_rel = if let Some(rel) = modql_field_attr.rel { 42 | quote! { 43 | Some(#rel.to_string()) 44 | } 45 | } else if let Some(struct_rel) = struct_attrs.rel.as_ref() { 46 | quote! { Some(#struct_rel.to_string()) } 47 | } else { 48 | quote! { None } 49 | }; 50 | props_opval_rels.push(block_rel); 51 | 52 | // -- options: FilterNodeOptions 53 | let quote_filter_node_options_cast_as = if let Some(cast_as) = modql_field_attr.cast_as { 54 | quote! { Some(#cast_as.to_string()) } 55 | } else { 56 | quote! { None } 57 | }; 58 | 59 | let quote_filter_node_options_cast_column_as = if let Some(cast_column_as) = modql_field_attr.cast_column_as { 60 | quote! { Some(#cast_column_as.to_string()) } 61 | } else { 62 | quote! { None } 63 | }; 64 | props_filter_node_options.push(quote! { 65 | modql::filter::FilterNodeOptions { 66 | cast_as: #quote_filter_node_options_cast_as, 67 | cast_column_as: #quote_filter_node_options_cast_column_as, 68 | } 69 | }); 70 | 71 | // -- to_sea_holder_build 72 | if cfg!(feature = "with-sea-query") { 73 | // TODO: Fail if both to_sea_condition_fn and to_sea_value_fn are defined 74 | 75 | let block = if let Some(to_sea_condition_fn) = modql_field_attr.to_sea_condition_fn { 76 | let to_sea_condition_fn = syn::Ident::new(&to_sea_condition_fn, proc_macro2::Span::call_site()); 77 | quote! { 78 | // None 79 | let fn_holder = modql::filter::ToSeaConditionFnHolder::new(#to_sea_condition_fn); 80 | let fn_holder = Some(fn_holder.into()); 81 | } 82 | } else if let Some(to_sea_value_fn) = modql_field_attr.to_sea_value_fn { 83 | let to_sea_value_fn = syn::Ident::new(&to_sea_value_fn, proc_macro2::Span::call_site()); 84 | quote! { 85 | // None 86 | let fn_holder = modql::filter::ToSeaValueFnHolder::new(#to_sea_value_fn); 87 | let fn_holder = Some(fn_holder.into()); 88 | } 89 | } else { 90 | quote! { 91 | let fn_holder = None; 92 | } 93 | }; 94 | props_opval_to_sea_holder_fn_build.push(block); 95 | } 96 | } 97 | } else { 98 | props.push(&field.ident); 99 | } 100 | } 101 | 102 | let ff_opt_node_pushes = if cfg!(feature = "with-sea-query") { 103 | quote! { 104 | #( 105 | if let Some(val) = self.#props_opval_idents { 106 | let op_vals: Vec = val.0.into_iter().map(|n| n.into()).collect(); 107 | #props_opval_to_sea_holder_fn_build 108 | let node = modql::filter::FilterNode{ 109 | rel: #props_opval_rels, 110 | name: stringify!(#props_opval_idents).to_string(), 111 | opvals: op_vals, 112 | options: #props_filter_node_options, 113 | for_sea_condition: fn_holder, 114 | }; 115 | nodes.push(node); 116 | } 117 | )* 118 | } 119 | } else { 120 | quote! { 121 | #( 122 | if let Some(val) = self.#props_opval_idents { 123 | let op_vals: Vec = val.0.into_iter().map(|n| n.into()).collect(); 124 | let node = modql::filter::FilterNode{ 125 | rel: #props_opval_rels, 126 | name: stringify!(#props_opval_idents).to_string(), 127 | opvals: op_vals, 128 | options: #props_filter_node_options, 129 | }; 130 | nodes.push(node); 131 | } 132 | )* 133 | } 134 | }; 135 | 136 | //// Out code for the impl IntoFilterNodes 137 | let out_impl_into_filter_nodes = quote! { 138 | impl modql::filter::IntoFilterNodes for #struct_name { 139 | fn filter_nodes(self, rel: Option) -> Vec { 140 | let mut nodes = Vec::new(); 141 | #ff_opt_node_pushes 142 | nodes 143 | } 144 | } 145 | }; 146 | 147 | //// Out code for the from struct for Vec 148 | let out_into_filter_node = quote! { 149 | impl From<#struct_name> for Vec { 150 | fn from(val: #struct_name) -> Self { 151 | modql::filter::IntoFilterNodes::filter_nodes(val, None) 152 | } 153 | } 154 | }; 155 | 156 | let out_into_op_group = quote! { 157 | impl From<#struct_name> for modql::filter::FilterGroup { 158 | fn from(val: #struct_name) -> Self { 159 | let nodes: Vec = val.into(); 160 | nodes.into() 161 | } 162 | } 163 | }; 164 | 165 | //// Out code for from struct for FilterGroups 166 | let out_into_op_groups = quote! { 167 | impl From<#struct_name> for modql::filter::FilterGroups { 168 | fn from(val: #struct_name) -> Self { 169 | let nodes: Vec = val.into(); 170 | nodes.into() 171 | } 172 | } 173 | }; 174 | 175 | let out_sea_filter = if cfg!(feature = "with-sea-query") { 176 | quote! { 177 | impl TryFrom<#struct_name> for sea_query::Condition { 178 | type Error = modql::filter::IntoSeaError; 179 | 180 | fn try_from(val: #struct_name) -> modql::filter::SeaResult { 181 | modql::filter::FilterGroup::from(val).try_into() 182 | } 183 | } 184 | } 185 | } else { 186 | quote! {} 187 | }; 188 | 189 | //// Final out code 190 | let output = quote! { 191 | #out_impl_into_filter_nodes 192 | #out_into_filter_node 193 | #out_into_op_group 194 | #out_into_op_groups 195 | #out_sea_filter 196 | }; 197 | 198 | output.into() 199 | } 200 | -------------------------------------------------------------------------------- /modql-macros/src/derives_filter/utils.rs: -------------------------------------------------------------------------------- 1 | use crate::utils::{get_field_attribute, get_meta_value_string}; 2 | use syn::punctuated::Punctuated; 3 | use syn::Field; 4 | use syn::{Meta, Token}; 5 | 6 | pub struct MoqlFilterFieldAttr { 7 | pub rel: Option, 8 | pub to_sea_condition_fn: Option, 9 | pub to_sea_value_fn: Option, 10 | pub cast_as: Option, 11 | pub cast_column_as: Option, 12 | } 13 | 14 | pub fn get_filter_field_attr(field: &Field) -> Result { 15 | let attribute = get_field_attribute(field, "modql"); 16 | 17 | let mut rel: Option = None; 18 | let mut to_sea_condition_fn: Option = None; 19 | let mut to_sea_value_fn: Option = None; 20 | let mut cast_as: Option = None; 21 | let mut cast_column_as: Option = None; 22 | if let Some(attribute) = attribute { 23 | let nested = attribute.parse_args_with(Punctuated::::parse_terminated)?; 24 | 25 | for meta in nested { 26 | match meta { 27 | // #[modql(rel= "project", to_sea_condition_fn = "my_sea_cond_fn_name")] 28 | Meta::NameValue(nv) => { 29 | if nv.path.is_ident("to_sea_condition_fn") { 30 | to_sea_condition_fn = get_meta_value_string(nv); 31 | } else if nv.path.is_ident("to_sea_value_fn") { 32 | to_sea_value_fn = get_meta_value_string(nv); 33 | } else if nv.path.is_ident("cast_as") { 34 | cast_as = get_meta_value_string(nv); 35 | } else if nv.path.is_ident("rel") { 36 | rel = get_meta_value_string(nv); 37 | } else if nv.path.is_ident("cast_column_as") { 38 | cast_column_as = get_meta_value_string(nv); 39 | } 40 | } 41 | 42 | /* ... */ 43 | _ => { 44 | let msg = "unrecognized modql attribute value"; 45 | return Err(syn::Error::new_spanned(meta, msg)); 46 | } 47 | } 48 | } 49 | } 50 | 51 | Ok(MoqlFilterFieldAttr { 52 | rel, 53 | to_sea_condition_fn, 54 | to_sea_value_fn, 55 | cast_as, 56 | cast_column_as, 57 | }) 58 | } 59 | -------------------------------------------------------------------------------- /modql-macros/src/derives_rusqlite/mod.rs: -------------------------------------------------------------------------------- 1 | mod sqlite_from_row; 2 | mod sqlite_from_value; 3 | mod sqlite_to_value; 4 | 5 | pub(crate) use sqlite_from_row::*; 6 | pub(crate) use sqlite_from_value::*; 7 | pub(crate) use sqlite_to_value::*; 8 | -------------------------------------------------------------------------------- /modql-macros/src/derives_rusqlite/sqlite_from_row.rs: -------------------------------------------------------------------------------- 1 | use crate::utils::modql_field::{ModqlFieldProp, ModqlFieldsAndSkips}; 2 | use crate::utils::{get_struct_fields, modql_field}; 3 | use proc_macro::TokenStream; 4 | use quote::quote; 5 | use syn::{parse_macro_input, DeriveInput, Field}; 6 | 7 | // FromSqliteRow (aliased to FromRow) 8 | pub fn derive_sqlite_from_row_inner(input: TokenStream) -> TokenStream { 9 | let ast = parse_macro_input!(input as DeriveInput); 10 | 11 | // -- Prep the fields and mfields (modql fields) 12 | let fields = get_struct_fields(&ast); 13 | let struct_name = &ast.ident; 14 | 15 | let ModqlFieldsAndSkips { 16 | modql_fields, 17 | skipped_fields, 18 | } = modql_field::get_modql_field_props_and_skips(fields); 19 | let mfields_slice: Vec<&ModqlFieldProp> = modql_fields.iter().collect(); 20 | 21 | let fn_sqlite_from_row_quote = impl_fn_sqlite_from_row(&mfields_slice, &skipped_fields); 22 | 23 | let fn_sqlite_from_row_partial_quote = impl_fn_sqlite_from_row_partial(&mfields_slice, &skipped_fields); 24 | 25 | // -- Compose the final code 26 | let output = quote! { 27 | impl modql::SqliteFromRow for #struct_name { 28 | 29 | #fn_sqlite_from_row_quote 30 | 31 | #fn_sqlite_from_row_partial_quote 32 | } 33 | }; 34 | 35 | output.into() 36 | } 37 | 38 | fn impl_fn_sqlite_from_row(mfield_props: &[&ModqlFieldProp], skipped_fields: &[&Field]) -> proc_macro2::TokenStream { 39 | let getters_quotes = mfield_props.iter().map(|mf| { 40 | let ident = mf.ident; 41 | 42 | // NOTE: Here we assume the select column has been aliased to the name of the property 43 | let col_name = &mf.prop_name; 44 | 45 | quote! { 46 | #ident: val.get(#col_name)?, 47 | } 48 | }); 49 | 50 | // for skipped 51 | let skipped_fields_quotes = skipped_fields.iter().map(|field| { 52 | let ident = field.ident.as_ref().unwrap(); 53 | quote! { 54 | #ident: Default::default(), 55 | } 56 | }); 57 | 58 | let output = quote! { 59 | fn sqlite_from_row(val: &rusqlite::Row<'_>) -> rusqlite::Result { 60 | 61 | let entity = Self { 62 | #(#getters_quotes)* 63 | #(#skipped_fields_quotes)* 64 | }; 65 | 66 | Ok(entity) 67 | } 68 | }; 69 | 70 | output 71 | } 72 | 73 | fn impl_fn_sqlite_from_row_partial( 74 | mfield_props: &[&ModqlFieldProp], 75 | skipped_fields: &[&Field], 76 | ) -> proc_macro2::TokenStream { 77 | let getters_quotes = mfield_props.iter().map(|mf| { 78 | let ident = mf.ident; 79 | 80 | // NOTE: Here we assume the select column has been aliased to the name of the property 81 | let col_name = &mf.prop_name; 82 | // let is_option = mf.is_option; 83 | 84 | if mf.is_option { 85 | quote! { 86 | #ident: if prop_names.contains(&#col_name) { val.get(#col_name)? } else { None }, 87 | } 88 | } 89 | // Otherwise, it's required 90 | // (later we have something like `#[field(partial_absent_as_default)]`) 91 | else { 92 | quote! { 93 | #ident: val.get(#col_name)?, 94 | } 95 | } 96 | }); 97 | 98 | // for skipped 99 | let skipped_fields_quotes = skipped_fields.iter().map(|field| { 100 | let ident = field.ident.as_ref().unwrap(); 101 | quote! { 102 | #ident: Default::default(), 103 | } 104 | }); 105 | 106 | let output = quote! { 107 | 108 | fn sqlite_from_row_partial(val: &rusqlite::Row<'_>, prop_names: &[&str]) -> rusqlite::Result { 109 | let entity = Self { 110 | #(#getters_quotes)* 111 | #(#skipped_fields_quotes)* 112 | }; 113 | 114 | Ok(entity) 115 | } 116 | 117 | }; 118 | 119 | output 120 | } 121 | -------------------------------------------------------------------------------- /modql-macros/src/derives_rusqlite/sqlite_from_value.rs: -------------------------------------------------------------------------------- 1 | // lib.rs 2 | extern crate proc_macro; 3 | 4 | use proc_macro::TokenStream; 5 | use proc_macro2::Ident; 6 | use quote::quote; 7 | use syn::{parse_macro_input, Data, DataEnum, DataStruct, DeriveInput, Fields}; 8 | 9 | pub fn derive_from_sqlite_value_inner(input: TokenStream) -> TokenStream { 10 | // Parse the input tokens into a syntax tree 11 | let input = parse_macro_input!(input as DeriveInput); 12 | 13 | // Get the identifier of the enum (e.g., "Model") 14 | let name = input.ident; 15 | 16 | // Build the match arms 17 | let expanded = match input.data { 18 | Data::Enum(data) => process_enum(name, data), 19 | syn::Data::Struct(data) => process_struct(name, data), 20 | _ => panic!("FromSqliteValue can only be used with enums or simple tuple struct for now (see FromRow)"), 21 | }; 22 | 23 | // Return the generated token stream 24 | TokenStream::from(expanded) 25 | } 26 | 27 | fn process_struct(name: Ident, data: DataStruct) -> proc_macro2::TokenStream { 28 | let first_tuple_field = match data.fields { 29 | Fields::Unnamed(fields) if fields.unnamed.len() == 1 => fields.unnamed.into_iter().next().unwrap(), 30 | _ => panic!("Expected a tuple struct with one field"), 31 | }; 32 | 33 | let field_type = &first_tuple_field.ty; 34 | 35 | let expanded = quote! { 36 | impl rusqlite::types::FromSql for #name { 37 | fn column_result(value: rusqlite::types::ValueRef<'_>) -> rusqlite::types::FromSqlResult { 38 | let val = #field_type::column_result(value)?; 39 | Ok(#name(val)) 40 | } 41 | } 42 | }; 43 | 44 | expanded 45 | } 46 | 47 | fn process_enum(name: Ident, data: DataEnum) -> proc_macro2::TokenStream { 48 | let arms = data 49 | .variants 50 | .iter() 51 | .map(|variant| { 52 | let variant_name = &variant.ident; 53 | let variant_name_str = variant_name.to_string(); 54 | quote! { 55 | #variant_name_str => Ok(#name::#variant_name), 56 | } 57 | }) 58 | .collect::>(); 59 | 60 | // Generate the final token stream 61 | let expanded = quote! { 62 | impl rusqlite::types::FromSql for #name { 63 | fn column_result(value: rusqlite::types::ValueRef<'_>) -> rusqlite::types::FromSqlResult { 64 | let txt: String = rusqlite::types::FromSql::column_result(value)?; 65 | match txt.as_str() { 66 | #(#arms)* 67 | _ => Err(rusqlite::types::FromSqlError::Other( 68 | format!("Invalid enum variant string '{}'", txt).into(), 69 | )), 70 | } 71 | } 72 | } 73 | }; 74 | 75 | expanded 76 | } 77 | -------------------------------------------------------------------------------- /modql-macros/src/derives_rusqlite/sqlite_to_value.rs: -------------------------------------------------------------------------------- 1 | // lib.rs 2 | extern crate proc_macro; 3 | 4 | use proc_macro::TokenStream; 5 | use proc_macro2::Ident; 6 | use quote::quote; 7 | use syn::{parse_macro_input, Data, DataEnum, DataStruct, DeriveInput}; 8 | 9 | pub fn derive_sqlite_to_value_inner(input: TokenStream) -> TokenStream { 10 | // Parse the input tokens into a syntax tree 11 | let input = parse_macro_input!(input as DeriveInput); 12 | 13 | // Get the identifier of the enum (e.g., "Model") 14 | let name = input.ident; 15 | 16 | // Build the match arms 17 | let expanded = match input.data { 18 | Data::Enum(data) => process_enum(name, data), 19 | syn::Data::Struct(data) => process_struct(name, data), 20 | _ => panic!("ToSqliteValue can only be used with enums or simple tuple struct for now"), 21 | }; 22 | 23 | // Return the generated token stream 24 | TokenStream::from(expanded) 25 | } 26 | 27 | /// For a type annotated like: 28 | /// ```rust,notest 29 | /// #[derive(modql::ToSqliteValue)] 30 | /// struct SId(i64); 31 | /// ``` 32 | /// Will generate something like: 33 | /// ```rust,notest 34 | /// impl rusqlite::types::ToSql for SId { 35 | /// fn to_sql(&self) -> rusqlite::Result> { 36 | /// Ok(rusqlite::types::ToSqlOutput::Owned(self.0.into())) 37 | /// } 38 | /// } 39 | /// ``` 40 | fn process_struct(name: Ident, _data: DataStruct) -> proc_macro2::TokenStream { 41 | // let first_tuple_field = match data.fields { 42 | // Fields::Unnamed(fields) if fields.unnamed.len() == 1 => fields.unnamed.into_iter().next().unwrap(), 43 | // _ => panic!("Expected a tuple struct with one field"), 44 | // }; 45 | 46 | // let field_type = &first_tuple_field.ty; 47 | // let field_ident = &first_tuple_field.ident; 48 | 49 | #[rustfmt::skip] 50 | let expanded = quote! { 51 | impl rusqlite::types::ToSql for #name { 52 | fn to_sql(&self) -> rusqlite::Result> { 53 | Ok(rusqlite::types::ToSqlOutput::Owned(self.0.into())) 54 | } 55 | } 56 | }; 57 | 58 | expanded 59 | } 60 | 61 | /// For an enum type annotated like: 62 | /// ```rust,notest 63 | /// #[derive(ToSqliteValue)] 64 | /// pub enum DItemKind { 65 | /// Md, 66 | /// Pdf, 67 | /// Unknown, 68 | /// } 69 | /// ``` 70 | /// Will expand to something like: 71 | /// ```rust,notest 72 | /// impl ToSql for DItemKind { 73 | /// fn to_sql(&self) -> rusqlite::Result> { 74 | /// let val = match self { 75 | /// DItemKind::Md => "Md", 76 | /// DItemKind::Pdf => "Pdf", 77 | /// DItemKind::Unknown => "Unknown", 78 | /// } 79 | /// .to_string(); 80 | /// 81 | /// Ok(rusqlite::types::ToSqlOutput::Owned(val.into())) 82 | /// } 83 | /// } 84 | /// ``` 85 | 86 | fn process_enum(name: Ident, data: DataEnum) -> proc_macro2::TokenStream { 87 | let arms = data 88 | .variants 89 | .iter() 90 | .map(|variant| { 91 | let variant_ident = &variant.ident; 92 | let variant_name_str = variant_ident.to_string(); 93 | quote! { 94 | #name::#variant_ident => #variant_name_str, 95 | } 96 | }) 97 | .collect::>(); 98 | 99 | // Generate the final token stream 100 | #[rustfmt::skip] 101 | let expanded = quote! { 102 | 103 | impl rusqlite::types::ToSql for #name { 104 | fn to_sql(&self) -> rusqlite::Result> { 105 | let val = match self { 106 | #(#arms)* 107 | }.to_string(); 108 | 109 | Ok(rusqlite::types::ToSqlOutput::Owned(val.into())) 110 | } 111 | } 112 | }; 113 | 114 | expanded 115 | } 116 | -------------------------------------------------------------------------------- /modql-macros/src/lib.rs: -------------------------------------------------------------------------------- 1 | // region: --- Modules 2 | 3 | mod derives_field; 4 | mod derives_filter; 5 | mod utils; 6 | 7 | use crate::derives_filter::derive_filter_nodes_inner; 8 | use proc_macro::TokenStream; 9 | 10 | // endregion: --- Modules 11 | 12 | #[proc_macro_derive(FilterNodes, attributes(modql))] 13 | pub fn derive_filter_nodes(input: TokenStream) -> TokenStream { 14 | derive_filter_nodes_inner(input) 15 | } 16 | 17 | // region: --- with-seaquery 18 | 19 | #[proc_macro_derive(Fields, attributes(field, modql))] 20 | pub fn derive_fields(input: TokenStream) -> TokenStream { 21 | derives_field::derive_fields_inner(input) 22 | } 23 | 24 | /// Implements `From for sea_query::Value` and `sea_query::Nullable for T` 25 | /// where T is the struct or enum annotated with `#[derive(Field)]` for simple 26 | /// tuple structs or enums. 27 | /// 28 | /// For more complex types, implement both of these traits for the type. 29 | /// 30 | /// For example: 31 | /// 32 | /// - On simple type and single element tuple struct 33 | /// ```rust,norun 34 | /// #[derive(modql::field::Field)] 35 | /// pub struct EpochTime(pub(in crate::time) i64); 36 | /// ``` 37 | /// Will generate something like 38 | /// ```rust,norun 39 | /// impl From for sea_query::Value { 40 | /// fn from(value: EpochTime) -> Self { 41 | /// Self::BigInt(Some(value.0)) 42 | /// } 43 | /// } 44 | /// impl sea_query::Nullable for EpochTime { 45 | /// fn null() -> sea_query::Value { 46 | /// sea_query::Value::BigInt(None) 47 | /// } 48 | /// } 49 | /// ``` 50 | /// Notes: 51 | /// - Supports only primitive types (no array yet) 52 | /// - Supports only one tuple field. 53 | /// 54 | /// - On Simple enum (plain variant only). 55 | /// ```rust,norun 56 | /// #[derive(modql::field::SeaFieldValue)] 57 | /// pub enum Kind { 58 | /// Md, 59 | /// Pdf, 60 | /// Unknown, 61 | /// } 62 | /// ``` 63 | /// Notes: 64 | /// - Will be treated a sea_query::Value::String with the name of the variant. 65 | /// - No rename for now. 66 | #[cfg(feature = "with-sea-query")] 67 | #[proc_macro_derive(SeaFieldValue)] 68 | pub fn derive_field_sea_value(input: TokenStream) -> TokenStream { 69 | derives_field::derive_field_sea_value_inner(input) 70 | } 71 | 72 | // endregion: --- with-seaquery 73 | 74 | // region: --- with-rusqlite 75 | 76 | #[cfg(feature = "with-rusqlite")] 77 | mod derives_rusqlite; 78 | 79 | #[cfg(feature = "with-rusqlite")] 80 | #[proc_macro_derive(SqliteFromRow, attributes(field, fields))] 81 | pub fn derive_sqlite_from_row(input: TokenStream) -> TokenStream { 82 | derives_rusqlite::derive_sqlite_from_row_inner(input) 83 | } 84 | 85 | /// Will implement the `rusqlite::types::FromSql` for the annotated type. 86 | /// 87 | /// For example: 88 | /// 89 | /// - For simple enum (with variant name only) 90 | /// ```rust,norun 91 | /// pub enum Kind { 92 | /// Md, 93 | /// Pdf, 94 | /// Unknown, 95 | /// } 96 | /// ``` 97 | /// Will generate something like: 98 | /// ```rust,norun 99 | /// impl rusqlite::types::FromSql for Kind { 100 | /// fn column_result(value: rusqlite::types::ValueRef<'_>) -> rusqlite::types::FromSqlResult { 101 | /// let txt: String = rusqlite::types::FromSql::column_result(value)?; 102 | /// match txt.as_str() { 103 | /// "Md" => Ok(Kind::Md), 104 | /// "Pdf" => Ok(Kind::Pdf), 105 | /// "Unknown" => Ok(Kind::Unknown), 106 | /// _ => Err(rusqlite::types::FromSqlError::Other( 107 | /// format!("Invalid enum variant string '{}'", txt).into(), 108 | /// )), 109 | /// } 110 | /// } 111 | /// } 112 | /// ``` 113 | /// 114 | /// - For simple tuple struct (one value that already implement the FromSqlt) 115 | /// ```rust,norun 116 | /// #[derive(modql::FromSqliteType)] 117 | /// pub struct EpochTime(i64); 118 | /// ``` 119 | /// Will generate something like: 120 | /// ```rust,norun 121 | /// impl rusqlite::types::FromSql for EpochTime { 122 | /// fn column_result(value: rusqlite::types::ValueRef<'_>) -> rusqlite::types::FromSqlResult { 123 | /// let val = i64::column_result(value)?; 124 | /// Ok(EpochTime(val)) 125 | /// } 126 | /// } 127 | /// ```` 128 | /// 129 | #[cfg(feature = "with-rusqlite")] 130 | #[proc_macro_derive(SqliteFromValue)] 131 | pub fn derive_sqlite_from_value(input: TokenStream) -> TokenStream { 132 | derives_rusqlite::derive_from_sqlite_value_inner(input) 133 | } 134 | 135 | #[cfg(feature = "with-rusqlite")] 136 | #[proc_macro_derive(SqliteToValue)] 137 | pub fn derive_sqlite_to_value(input: TokenStream) -> TokenStream { 138 | derives_rusqlite::derive_sqlite_to_value_inner(input) 139 | } 140 | 141 | // endregion: --- with-rusqlite 142 | 143 | // region: --- with-rusqlite Deprecated 144 | 145 | #[deprecated(note = "use SqliteFromRow")] 146 | #[cfg(feature = "with-rusqlite")] 147 | #[proc_macro_derive(FromSqliteRow, attributes(field, fields))] 148 | pub fn derive_sqlite_from_row_deprecated(input: TokenStream) -> TokenStream { 149 | derives_rusqlite::derive_sqlite_from_row_inner(input) 150 | } 151 | 152 | #[deprecated(note = "use SqliteFromValue")] 153 | #[cfg(feature = "with-rusqlite")] 154 | #[proc_macro_derive(FromSqliteValue)] 155 | pub fn derive_sqlite_from_value_deprecated(input: TokenStream) -> TokenStream { 156 | derives_rusqlite::derive_from_sqlite_value_inner(input) 157 | } 158 | 159 | #[deprecated(note = "use SqliteToValue")] 160 | #[cfg(feature = "with-rusqlite")] 161 | #[proc_macro_derive(ToSqliteValue)] 162 | pub fn derive_sqlite_to_value_depcreated(input: TokenStream) -> TokenStream { 163 | derives_rusqlite::derive_sqlite_to_value_inner(input) 164 | } 165 | // endregion: --- with-rusqlite Deprecated 166 | -------------------------------------------------------------------------------- /modql-macros/src/utils/mod.rs: -------------------------------------------------------------------------------- 1 | // region: --- Modules 2 | pub mod modql_field; 3 | pub mod struct_modql_attr; 4 | 5 | use quote::ToTokens; 6 | use syn::{Attribute, DeriveInput, Expr, Field, FieldsNamed, Lit, MetaNameValue}; 7 | 8 | // endregion: --- Modules 9 | 10 | /// Returns the syn:: fields named of a struct 11 | pub(crate) fn get_struct_fields(ast: &DeriveInput) -> &FieldsNamed { 12 | let syn::Data::Struct(syn::DataStruct { 13 | fields: syn::Fields::Named(ref fields), 14 | .. 15 | }) = ast.data 16 | else { 17 | panic!("Only support Struct") 18 | }; 19 | fields 20 | } 21 | 22 | /// Returns the type_name of a field 23 | pub(crate) fn get_type_name(field: &Field) -> String { 24 | format!("{}", &field.ty.to_token_stream()) 25 | } 26 | 27 | pub fn get_field_attribute<'a>(field: &'a Field, name: &str) -> Option<&'a Attribute> { 28 | field.attrs.iter().find(|a| a.path().is_ident(name)) 29 | } 30 | 31 | pub fn get_dinput_attribute<'a>(dinput: &'a DeriveInput, name: &str) -> Option<&'a Attribute> { 32 | dinput.attrs.iter().find(|a| a.path().is_ident(name)) 33 | } 34 | 35 | pub fn get_meta_value_string(nv: MetaNameValue) -> Option { 36 | if let Expr::Lit(exp_lit) = nv.value { 37 | if let Lit::Str(lit_str) = exp_lit.lit { 38 | return Some(lit_str.value()); 39 | } 40 | } 41 | None 42 | } 43 | -------------------------------------------------------------------------------- /modql-macros/src/utils/modql_field.rs: -------------------------------------------------------------------------------- 1 | use crate::utils::{get_field_attribute, get_meta_value_string}; 2 | use proc_macro2::Ident; 3 | use quote::ToTokens; 4 | use syn::punctuated::Punctuated; 5 | use syn::{Field, FieldsNamed, Meta, Token}; 6 | 7 | // region: --- Field Prop (i.e., sqlb Field) 8 | pub struct ModqlFieldProp<'a> { 9 | pub prop_name: String, // property name 10 | pub attr_name: Option, // The eventual `#[field(name=..._)]` 11 | pub name: String, // resolved name attr_name or prop name; 12 | pub rel: Option, 13 | pub cast_as: Option, 14 | pub is_option: bool, 15 | pub ident: &'a Option, 16 | } 17 | 18 | pub fn get_modql_field_props(fields: &FieldsNamed) -> Vec { 19 | let modql_fields_and_skips = get_modql_field_props_and_skips(fields); 20 | modql_fields_and_skips.modql_fields 21 | } 22 | 23 | pub struct ModqlFieldsAndSkips<'a> { 24 | pub modql_fields: Vec>, 25 | #[allow(unused)] // For early development. 26 | pub skipped_fields: Vec<&'a Field>, 27 | } 28 | 29 | pub fn get_modql_field_props_and_skips(fields: &FieldsNamed) -> ModqlFieldsAndSkips { 30 | let mut modql_fields = Vec::new(); 31 | let mut skipped_fields = Vec::new(); 32 | 33 | for field in fields.named.iter() { 34 | // -- Get the FieldAttr 35 | let mfield_attr = get_mfield_prop_attr(field); 36 | 37 | // TODO: Need to check better handling. 38 | let mfield_attr = mfield_attr.unwrap(); 39 | if mfield_attr.skip { 40 | skipped_fields.push(field); 41 | continue; 42 | } 43 | 44 | // -- ident 45 | let ident = &field.ident; 46 | 47 | // -- is_option 48 | // NOTE: By macro limitation, we can do only type name match and it would not support type alias 49 | // For now, assume Option is used as is or type name contains it. 50 | // We can add other variants of Option if proven needed. 51 | let type_name = format!("{}", &field.ty.to_token_stream()); 52 | let is_option = type_name.contains("Option "); 53 | 54 | // -- name 55 | let prop_name = ident.as_ref().map(|i| i.to_string()).unwrap(); 56 | let attr_name = mfield_attr.name; 57 | let name = attr_name.clone().unwrap_or_else(|| prop_name.clone()); 58 | 59 | // -- cast_as 60 | let cast_as = mfield_attr.cast_as; 61 | 62 | // -- Add to array. 63 | modql_fields.push(ModqlFieldProp { 64 | rel: mfield_attr.rel, 65 | name, 66 | prop_name, 67 | attr_name, 68 | ident, 69 | cast_as, 70 | is_option, 71 | }) 72 | } 73 | 74 | ModqlFieldsAndSkips { 75 | modql_fields, 76 | skipped_fields, 77 | } 78 | } 79 | 80 | // endregion: --- Field Prop (i.e., sqlb Field) 81 | 82 | // region: --- Field Prop Attribute 83 | struct ModqlFieldPropAttr { 84 | pub rel: Option, 85 | pub name: Option, 86 | pub skip: bool, 87 | pub cast_as: Option, 88 | } 89 | 90 | // #[field(skip)] 91 | // #[field(name = "new_name")] 92 | fn get_mfield_prop_attr(field: &Field) -> Result { 93 | let attribute = get_field_attribute(field, "field"); 94 | 95 | let mut skip = false; 96 | let mut rel: Option = None; 97 | let mut column: Option = None; 98 | let mut cast_as: Option = None; 99 | 100 | if let Some(attribute) = attribute { 101 | let nested = attribute.parse_args_with(Punctuated::::parse_terminated)?; 102 | 103 | for meta in nested { 104 | match meta { 105 | // #[field(skip)] 106 | Meta::Path(path) if path.is_ident("skip") => { 107 | skip = true; 108 | } 109 | 110 | // #[field(name=value)] 111 | Meta::NameValue(nv) => { 112 | if nv.path.is_ident("rel") { 113 | rel = get_meta_value_string(nv); 114 | } else if nv.path.is_ident("name") { 115 | column = get_meta_value_string(nv); 116 | } else if nv.path.is_ident("cast_as") { 117 | cast_as = get_meta_value_string(nv); 118 | } 119 | } 120 | 121 | /* ... */ 122 | _ => { 123 | return Err(syn::Error::new_spanned( 124 | meta, 125 | r#" 126 | Unrecognized #[field...] attribute. Accepted attribute 127 | #[field(skip)] 128 | or 129 | #[field(rel="table_name}, name="some_col_name", cast_as="sea query cast as type")] 130 | "#, 131 | )); 132 | } 133 | } 134 | } 135 | } 136 | 137 | Ok(ModqlFieldPropAttr { 138 | skip, 139 | rel, 140 | name: column, 141 | cast_as, 142 | }) 143 | } 144 | 145 | // endregion: --- Field Prop Attribute 146 | -------------------------------------------------------------------------------- /modql-macros/src/utils/struct_modql_attr.rs: -------------------------------------------------------------------------------- 1 | use crate::utils::{get_dinput_attribute, get_meta_value_string}; 2 | use syn::punctuated::Punctuated; 3 | use syn::{DeriveInput, Meta, Token}; 4 | 5 | // region: --- Struct Prop Attribute 6 | pub struct StructModqlFieldProps { 7 | pub rel: Option, 8 | pub names_as_consts: Option, 9 | } 10 | 11 | pub fn get_struct_modql_props(dinput: &DeriveInput) -> Result { 12 | // FIXME: We should remove this, 'sqlb' should not be a thing anymore. 13 | let sqlb_attr = get_dinput_attribute(dinput, "modql"); 14 | let mut rel = None; 15 | let mut names_as_consts = None; 16 | 17 | if let Some(attribute) = sqlb_attr { 18 | let nested = attribute.parse_args_with(Punctuated::::parse_terminated)?; 19 | 20 | for meta in nested { 21 | match meta { 22 | // #[modql(rel=value)] 23 | Meta::NameValue(nv) => 24 | { 25 | #[allow(clippy::if_same_then_else)] 26 | if nv.path.is_ident("rel") { 27 | rel = get_meta_value_string(nv); 28 | } else if nv.path.is_ident("names_as_consts") { 29 | names_as_consts = get_meta_value_string(nv); 30 | } 31 | } 32 | 33 | Meta::Path(path) => { 34 | if let Some(path) = path.get_ident() { 35 | let path = path.to_string(); 36 | if path == "names_as_consts" { 37 | names_as_consts = Some("".to_string()) 38 | } 39 | } 40 | } 41 | 42 | /* ... */ 43 | _ => { 44 | let msg = "unrecognized modql attribute value"; 45 | return Err(syn::Error::new_spanned(meta, msg)); 46 | // return Err(syn::Error::new_spanned(meta, "unrecognized field")); 47 | } 48 | } 49 | } 50 | } 51 | 52 | Ok(StructModqlFieldProps { rel, names_as_consts }) 53 | } 54 | 55 | // endregion: --- Struct Prop Attribute 56 | -------------------------------------------------------------------------------- /rustfmt.toml: -------------------------------------------------------------------------------- 1 | # rustfmt doc - https://rust-lang.github.io/rustfmt/ 2 | 3 | # JC Update 4 | hard_tabs = true 5 | edition = "2021" 6 | max_width = 120 7 | chain_width = 80 8 | array_width = 80 9 | 10 | # imports_granularity = "Module" # no effect on rust analyzer 11 | -------------------------------------------------------------------------------- /src/error.rs: -------------------------------------------------------------------------------- 1 | //! Main Crate Error 2 | 3 | use serde_json::Value; 4 | 5 | /// modql Result 6 | pub type Result = core::result::Result; 7 | 8 | /// modql Error 9 | #[derive(Debug)] 10 | pub enum Error { 11 | // region: --- Json Errors 12 | JsonValNotOfType(&'static str), 13 | 14 | JsonValArrayWrongType { 15 | actual_value: Value, 16 | }, 17 | JsonValArrayItemNotOfType { 18 | expected_type: &'static str, 19 | actual_value: Value, 20 | }, 21 | 22 | JsonOpValNotSupported { 23 | operator: String, 24 | value: Value, 25 | }, 26 | // endregion: --- Json Errors 27 | } 28 | 29 | // region: --- Error Boilerpate 30 | impl std::fmt::Display for Error { 31 | fn fmt(&self, fmt: &mut std::fmt::Formatter) -> core::result::Result<(), std::fmt::Error> { 32 | write!(fmt, "{self:?}") 33 | } 34 | } 35 | 36 | impl std::error::Error for Error {} 37 | // endregion: --- Error Boilerpate 38 | -------------------------------------------------------------------------------- /src/field/error.rs: -------------------------------------------------------------------------------- 1 | pub type Result = core::result::Result; 2 | 3 | #[derive(Debug)] 4 | pub enum Error { 5 | FieldValueNotSeaValue, 6 | FieldValueIntoTypeError { field_name: String }, 7 | } 8 | 9 | // region: --- Error Boilerplate 10 | impl core::fmt::Display for Error { 11 | fn fmt(&self, fmt: &mut core::fmt::Formatter) -> core::result::Result<(), core::fmt::Error> { 12 | write!(fmt, "{self:?}") 13 | } 14 | } 15 | 16 | impl std::error::Error for Error {} 17 | // endregion: --- Error Boilerplate 18 | -------------------------------------------------------------------------------- /src/field/field_meta.rs: -------------------------------------------------------------------------------- 1 | #[derive(Debug, Clone)] 2 | pub struct FieldMeta { 3 | /// rel from either the struct `#[modql(rel=...)]` or from property `#[field(rel=...)]` 4 | pub rel: Option<&'static str>, 5 | 6 | /// If the rel is a struct rel or the field rel matches the struct rel 7 | pub is_struct_rel: bool, 8 | 9 | /// Name of the struct property no matter what. 10 | pub prop_name: &'static str, 11 | 12 | /// The attribute name (i.e., `#[field(name=...)]`) 13 | pub attr_name: Option<&'static str>, 14 | 15 | /// `#[field(cast_as=...)` 16 | pub cast_as: Option<&'static str>, 17 | 18 | /// if it is an Option type 19 | pub is_option: bool, 20 | } 21 | 22 | impl FieldMeta { 23 | pub fn name(&self) -> &'static str { 24 | self.attr_name.unwrap_or(self.prop_name) 25 | } 26 | 27 | /// return the alias name if there should be one 28 | /// (if prop_name != name, it will return prop_name) 29 | pub fn alias(&self) -> Option<&'static str> { 30 | let attr_name = self.attr_name?; 31 | if self.prop_name != attr_name { 32 | Some(self.prop_name) 33 | } else { 34 | None 35 | } 36 | } 37 | 38 | /// Return the quote column ref with the eventual ref and eventual 39 | /// alias `AS "prop_name"` if prop_name does not match a provide attr_name (i.e. #[field(name=...)]) 40 | /// e.g., `"conv"."desc" AS "description"` 41 | pub fn sql_col_ref(&self) -> String { 42 | let mut col_ref = if let Some(rel) = self.rel { 43 | format!("\"{}\".\"{}\"", rel, self.name()) 44 | } else { 45 | format!("\"{}\"", self.name()) 46 | }; 47 | 48 | if let Some(alias_name) = self.alias() { 49 | col_ref.push_str(&format!(" AS \"{}\"", alias_name)); 50 | } 51 | col_ref 52 | } 53 | } 54 | 55 | // when with-sea-query 56 | #[cfg(feature = "with-sea-query")] 57 | mod with_sea_query { 58 | use super::*; 59 | use crate::SIden; 60 | use sea_query::{Alias, ColumnRef, IntoIden, SelectStatement}; 61 | 62 | impl FieldMeta { 63 | pub fn sea_column_ref(&self) -> ColumnRef { 64 | match self.rel { 65 | Some(rel) => ColumnRef::TableColumn(SIden(rel).into_iden(), SIden(self.name()).into_iden()), 66 | None => ColumnRef::Column(SIden(self.name()).into_iden()), 67 | } 68 | } 69 | 70 | pub fn sea_apply_select_column(&self, sea_select: &mut SelectStatement) { 71 | let col_ref = self.sea_column_ref(); 72 | 73 | if let Some(alias_name) = self.alias() { 74 | sea_select.expr_as(col_ref, Alias::new(alias_name)); 75 | } else { 76 | sea_select.column(col_ref); 77 | } 78 | } 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /src/field/field_metas.rs: -------------------------------------------------------------------------------- 1 | use crate::field::FieldMeta; 2 | 3 | pub struct FieldMetas(&'static [&'static FieldMeta]); 4 | 5 | impl FieldMetas { 6 | pub const fn new(metas: &'static [&'static FieldMeta]) -> Self { 7 | Self(metas) 8 | } 9 | 10 | pub fn iter(&self) -> core::slice::Iter<'_, &'static FieldMeta> { 11 | self.0.iter() 12 | } 13 | 14 | pub fn sql_col_refs(&self) -> String { 15 | let cols = self.0.iter().map(|meta| meta.sql_col_ref()).collect::>(); 16 | cols.join(", ") 17 | } 18 | 19 | pub fn sql_col_refs_for(&self, prop_names: &[&str]) -> String { 20 | let cols = self 21 | .0 22 | .iter() 23 | .filter(|m| prop_names.contains(&m.prop_name)) 24 | .map(|meta| meta.sql_col_ref()) 25 | .collect::>(); 26 | cols.join(", ") 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/field/has_fields.rs: -------------------------------------------------------------------------------- 1 | use crate::field::FieldMetas; 2 | 3 | pub trait HasFields { 4 | /// Returns the array of all field names (can be customize with `#[field(rel=..., name=...], #[field(ignore)]`) 5 | fn field_names() -> &'static [&'static str]; 6 | 7 | #[allow(deprecated)] 8 | #[deprecated(note = "use field_metas")] 9 | fn field_refs() -> &'static [&'static FieldRef]; 10 | 11 | fn field_metas() -> &'static FieldMetas; 12 | } 13 | 14 | /// To deprecate in favor of FieldMeta 15 | #[deprecated(note = "use FieldMeta")] 16 | #[derive(Debug)] 17 | pub struct FieldRef { 18 | /// Eventual relation (e.g., table name) 19 | pub rel: Option<&'static str>, 20 | /// The name of the field (e.g., column name) 21 | pub name: &'static str, 22 | } 23 | -------------------------------------------------------------------------------- /src/field/mod.rs: -------------------------------------------------------------------------------- 1 | //! Requires feature `with-sea-query` and provides convenient sea-query serialization for field names and values. 2 | 3 | mod error; 4 | mod field_meta; 5 | mod field_metas; 6 | mod has_fields; 7 | #[cfg(feature = "with-sea-query")] 8 | mod sea; 9 | 10 | pub use self::error::{Error, Result}; 11 | pub use field_meta::*; 12 | pub use field_metas::*; 13 | pub use has_fields::*; 14 | pub use modql_macros::Fields; 15 | 16 | #[cfg(feature = "with-sea-query")] 17 | pub use modql_macros::SeaFieldValue; 18 | 19 | #[cfg(feature = "with-sea-query")] 20 | pub use sea::*; 21 | -------------------------------------------------------------------------------- /src/field/sea/has_sea_fields.rs: -------------------------------------------------------------------------------- 1 | use crate::field::{HasFields, SeaFields}; 2 | use sea_query::{ColumnRef, DynIden, IntoIden, SelectStatement}; 3 | 4 | pub trait HasSeaFields: HasFields { 5 | /// Returns the `Fields` containing the `Field` items that have non-`None` values. 6 | fn not_none_sea_fields(self) -> SeaFields; 7 | 8 | /// Returns the `Fields` containing all of the `Field`. 9 | fn all_sea_fields(self) -> SeaFields; 10 | 11 | /// Return the sea_query::DynIden for each field (just matching the field name) 12 | fn sea_idens() -> Vec; 13 | 14 | /// Returns the list of column refs (takes the eventual #[field(rel = "table_name")]) 15 | /// WARNING: This won't have the aliases if there need to be some. 16 | /// Use `sea_apply_select_columns(select)` or `T::field_metas()` to build manually. 17 | /// TODO: Need to use the `field_metas().. meta.sea_column_ref()`` 18 | fn sea_column_refs() -> Vec; 19 | 20 | /// Returns the list of column refs with the given relation (e.g., table name) and IntoIden (.e.g., StringIden or SIden) 21 | fn sea_column_refs_with_rel(rel: impl IntoIden) -> Vec; 22 | 23 | fn sea_apply_select_columns(&self, sea_select: &mut SelectStatement) { 24 | for meta in Self::field_metas().iter() { 25 | meta.sea_apply_select_column(sea_select); 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/field/sea/mod.rs: -------------------------------------------------------------------------------- 1 | // region: --- Modules 2 | 3 | mod has_sea_fields; 4 | mod sea_field; 5 | mod sea_fields; 6 | 7 | // - Flatten 8 | pub use has_sea_fields::*; 9 | pub use sea_field::*; 10 | pub use sea_fields::*; 11 | 12 | // endregion: --- Modules 13 | -------------------------------------------------------------------------------- /src/field/sea/sea_field.rs: -------------------------------------------------------------------------------- 1 | use crate::field::{Error, Result}; 2 | use crate::sea_utils::StringIden; 3 | use crate::SIden; 4 | use sea_query::{ColumnRef, DynIden, SimpleExpr, Value}; 5 | use sea_query::{IntoIden, ValueType}; 6 | 7 | #[derive(Debug, Clone)] 8 | pub struct SeaField { 9 | pub iden: DynIden, 10 | pub column_ref: ColumnRef, 11 | pub value: SimpleExpr, 12 | } 13 | 14 | impl SeaField { 15 | pub fn sea_value(&self) -> Option<&Value> { 16 | if let SimpleExpr::Value(value) = &self.value { 17 | Some(value) 18 | } else { 19 | None 20 | } 21 | } 22 | 23 | pub fn value_into(self) -> Result 24 | where 25 | T: ValueType, 26 | { 27 | let SimpleExpr::Value(value) = self.value else { 28 | return Err(Error::FieldValueNotSeaValue); 29 | }; 30 | 31 | T::try_from(value).map_err(|_| Error::FieldValueIntoTypeError { 32 | field_name: self.iden.to_string(), 33 | }) 34 | } 35 | } 36 | 37 | #[derive(Default, Debug)] 38 | pub struct FieldOptions { 39 | pub cast_as: Option, 40 | } 41 | 42 | impl SeaField { 43 | /// Create a new SeaField from an `IntoIden` and `Into` for the value 44 | pub fn new(iden: impl IntoIden, value: impl Into) -> Self { 45 | Self::new_concrete(iden.into_iden(), value.into()) 46 | } 47 | 48 | /// The concrete version of the new. 49 | pub fn new_concrete(iden: DynIden, value: SimpleExpr) -> Self { 50 | let column_ref = ColumnRef::Column(iden.clone()); 51 | SeaField { 52 | iden, 53 | column_ref, 54 | value, 55 | } 56 | } 57 | 58 | /// Create a new SeaField for a static column name and a `Into` for the value 59 | pub fn siden(iden: &'static str, value: impl Into) -> Self { 60 | let iden = SIden(iden).into_iden(); 61 | let column_ref = ColumnRef::Column(iden.clone()); 62 | SeaField { 63 | iden, 64 | column_ref, 65 | value: value.into(), 66 | } 67 | } 68 | 69 | pub fn new_with_options(iden: impl IntoIden, value: SimpleExpr, options: FieldOptions) -> Self { 70 | let iden = iden.into_iden(); 71 | let column_ref = ColumnRef::Column(iden.clone()); 72 | let mut value = value; 73 | if let Some(cast_as) = options.cast_as { 74 | value = value.cast_as(StringIden(cast_as)) 75 | } 76 | 77 | SeaField { 78 | iden, 79 | column_ref, 80 | value, 81 | } 82 | } 83 | } 84 | 85 | // region: --- Froms 86 | 87 | // From (DynIden, SimpleExpr) 88 | impl From<(DynIden, SimpleExpr)> for SeaField { 89 | fn from(val: (DynIden, SimpleExpr)) -> Self { 90 | SeaField::new(val.0, val.1) 91 | } 92 | } 93 | 94 | impl From<(&'static str, SimpleExpr)> for SeaField { 95 | fn from(val: (&'static str, SimpleExpr)) -> Self { 96 | SeaField::new(SIden(val.0), val.1) 97 | } 98 | } 99 | 100 | // endregion: --- Froms 101 | -------------------------------------------------------------------------------- /src/field/sea/sea_fields.rs: -------------------------------------------------------------------------------- 1 | use crate::field::SeaField; 2 | use sea_query::{DynIden, SimpleExpr}; 3 | 4 | #[derive(Debug, Clone)] 5 | pub struct SeaFields(Vec); 6 | 7 | // Constructor 8 | impl SeaFields { 9 | pub fn new(fields: Vec) -> Self { 10 | SeaFields(fields) 11 | } 12 | } 13 | 14 | // Api 15 | impl SeaFields { 16 | /// Simple api to append a SeaField to the list. 17 | pub fn push(&mut self, field: SeaField) { 18 | self.0.push(field); 19 | } 20 | 21 | /// The consuming builder API equivalent to `push(..)` 22 | pub fn append(mut self, field: SeaField) -> Self { 23 | self.push(field); 24 | self 25 | } 26 | 27 | /// The static 'str for iden version of the `append(..)` 28 | pub fn append_siden(mut self, iden: &'static str, value: impl Into) -> Self { 29 | let field = SeaField::siden(iden, value); 30 | self.push(field); 31 | self 32 | } 33 | 34 | pub fn into_vec(self) -> Vec { 35 | self.0 36 | } 37 | 38 | /// Alias to self.unzip() 39 | pub fn for_sea_insert(self) -> (Vec, Vec) { 40 | self.unzip() 41 | } 42 | 43 | /// returns a tuble: (Vec_of_column_idens, Vec_of_value_exprs) 44 | pub fn unzip(self) -> (Vec, Vec) { 45 | self.0.into_iter().map(|f| (f.iden, f.value)).unzip() 46 | } 47 | 48 | /// Alias to self.zip() 49 | pub fn for_sea_update(self) -> impl Iterator { 50 | self.zip() 51 | } 52 | 53 | /// returns Iterator of (column_iden, value_expr) 54 | /// Useful for sea query update. 55 | pub fn zip(self) -> impl Iterator { 56 | self.0.into_iter().map(|f| (f.iden, f.value)) 57 | } 58 | } 59 | 60 | impl IntoIterator for SeaFields { 61 | type Item = SeaField; 62 | type IntoIter = std::vec::IntoIter; 63 | 64 | fn into_iter(self) -> Self::IntoIter { 65 | self.0.into_iter() 66 | } 67 | } 68 | 69 | // region: --- Froms 70 | 71 | impl From> for SeaFields { 72 | fn from(val: Vec) -> Self { 73 | SeaFields(val) 74 | } 75 | } 76 | 77 | impl From for SeaFields { 78 | fn from(val: SeaField) -> Self { 79 | SeaFields(vec![val]) 80 | } 81 | } 82 | 83 | // endregion: --- Froms 84 | -------------------------------------------------------------------------------- /src/filter/into_sea/error.rs: -------------------------------------------------------------------------------- 1 | pub type SeaResult = core::result::Result; 2 | 3 | /// Error for FilterNode to Sea Condition 4 | #[derive(Debug)] 5 | pub enum IntoSeaError { 6 | // For now, just Custom. Might have more variants later. 7 | Custom(String), 8 | SerdeJson(serde_json::Error), 9 | } 10 | 11 | // region: --- Froms 12 | impl From for IntoSeaError { 13 | fn from(val: serde_json::Error) -> Self { 14 | Self::SerdeJson(val) 15 | } 16 | } 17 | // endregion: --- Froms 18 | 19 | impl IntoSeaError { 20 | pub fn custom(message: impl Into) -> Self { 21 | IntoSeaError::Custom(message.into()) 22 | } 23 | } 24 | 25 | // region: --- Error Boilerplate 26 | impl core::fmt::Display for IntoSeaError { 27 | fn fmt(&self, fmt: &mut core::fmt::Formatter) -> core::result::Result<(), core::fmt::Error> { 28 | write!(fmt, "{self:?}") 29 | } 30 | } 31 | 32 | impl std::error::Error for IntoSeaError {} 33 | // endregion: --- Error Boilerplate 34 | -------------------------------------------------------------------------------- /src/filter/into_sea/mod.rs: -------------------------------------------------------------------------------- 1 | // region: --- Modules 2 | 3 | mod error; 4 | 5 | pub use self::error::{IntoSeaError, SeaResult}; 6 | 7 | use crate::filter::OpValValue; 8 | use sea_query::{ColumnRef, ConditionExpression}; 9 | 10 | // endregion: --- Modules 11 | 12 | #[derive(Clone, Debug)] 13 | pub enum ForSeaCondition { 14 | ToSeaValue(ToSeaValueFnHolder), 15 | ToSeaCondition(ToSeaConditionFnHolder), 16 | } 17 | 18 | impl From for ForSeaCondition { 19 | fn from(val: ToSeaValueFnHolder) -> Self { 20 | ForSeaCondition::ToSeaValue(val) 21 | } 22 | } 23 | 24 | impl From for ForSeaCondition { 25 | fn from(val: ToSeaConditionFnHolder) -> Self { 26 | ForSeaCondition::ToSeaCondition(val) 27 | } 28 | } 29 | 30 | // region: --- ToSeaValueFn 31 | pub type JsonToSeaValueFn = fn(serde_json::Value) -> SeaResult; 32 | 33 | #[derive(Clone, Debug)] 34 | pub struct ToSeaValueFnHolder { 35 | fun: JsonToSeaValueFn, 36 | } 37 | 38 | impl ToSeaValueFnHolder { 39 | pub fn new(fun: JsonToSeaValueFn) -> Self { 40 | ToSeaValueFnHolder { fun } 41 | } 42 | 43 | pub fn call(&self, json_value: serde_json::Value) -> SeaResult { 44 | (self.fun)(json_value) 45 | } 46 | } 47 | // endregion: --- ToSeaValueFn 48 | 49 | // region: --- ToSeaConditionFn 50 | pub type ToSeaConditionFn = fn(col: &ColumnRef, op_value: OpValValue) -> SeaResult; 51 | 52 | #[derive(Clone, Debug)] 53 | pub struct ToSeaConditionFnHolder { 54 | fun: ToSeaConditionFn, 55 | } 56 | 57 | impl ToSeaConditionFnHolder { 58 | pub fn new(fun: ToSeaConditionFn) -> Self { 59 | ToSeaConditionFnHolder { fun } 60 | } 61 | 62 | pub fn call(&self, col: &ColumnRef, op_value: OpValValue) -> SeaResult { 63 | (self.fun)(col, op_value) 64 | } 65 | } 66 | // endregion: --- ToSeaConditionFn 67 | -------------------------------------------------------------------------------- /src/filter/json/mod.rs: -------------------------------------------------------------------------------- 1 | // -- Sub-Modules 2 | mod order_bys_de; 3 | mod ovs_de_bool; 4 | mod ovs_de_number; 5 | mod ovs_de_string; 6 | mod ovs_de_value; 7 | mod ovs_json; 8 | 9 | pub use ovs_json::OpValueToOpValType; 10 | -------------------------------------------------------------------------------- /src/filter/json/order_bys_de.rs: -------------------------------------------------------------------------------- 1 | use crate::filter::{OrderBy, OrderBys}; 2 | use serde::{de, Deserialize, Deserializer}; 3 | use std::fmt; 4 | 5 | impl<'de> Deserialize<'de> for OrderBys { 6 | fn deserialize(deserializer: D) -> Result 7 | where 8 | D: Deserializer<'de>, 9 | { 10 | deserializer.deserialize_any(OrderBysVisitor) 11 | } 12 | } 13 | 14 | struct OrderBysVisitor; 15 | 16 | impl<'de> de::Visitor<'de> for OrderBysVisitor { 17 | type Value = OrderBys; // for deserialize 18 | 19 | fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { 20 | write!(formatter, "OrderBysVisitor visitor not implemented for this type.") 21 | } 22 | 23 | fn visit_string(self, v: String) -> Result 24 | where 25 | E: de::Error, 26 | { 27 | Ok(OrderBy::from(v).into()) 28 | } 29 | 30 | fn visit_str(self, v: &str) -> Result 31 | where 32 | E: de::Error, 33 | { 34 | Ok(OrderBy::from(v.to_string()).into()) 35 | } 36 | 37 | fn visit_seq(self, mut seq: A) -> Result 38 | where 39 | A: de::SeqAccess<'de>, 40 | { 41 | let mut order_bys: Vec = Vec::new(); 42 | 43 | while let Some(string) = seq.next_element::()? { 44 | order_bys.push(OrderBy::from(string)); 45 | } 46 | 47 | Ok(OrderBys::new(order_bys)) 48 | } 49 | // FIXME: Needs to add support for visit_seq 50 | } 51 | -------------------------------------------------------------------------------- /src/filter/json/ovs_de_bool.rs: -------------------------------------------------------------------------------- 1 | use super::ovs_json::OpValueToOpValType; 2 | use crate::filter::{OpValBool, OpValsBool}; 3 | use serde::{de::MapAccess, de::Visitor, Deserialize, Deserializer}; 4 | use serde_json::Value; 5 | use std::fmt; 6 | 7 | impl<'de> Deserialize<'de> for OpValsBool { 8 | fn deserialize(deserializer: D) -> Result 9 | where 10 | D: Deserializer<'de>, 11 | { 12 | deserializer.deserialize_any(BoolOpValsVisitor) 13 | } 14 | } 15 | 16 | struct BoolOpValsVisitor; 17 | 18 | impl<'de> Visitor<'de> for BoolOpValsVisitor { 19 | type Value = OpValsBool; // for deserialize 20 | 21 | fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { 22 | write!(formatter, "BoolOpValsVisitor visitor not implemented for this type.") 23 | } 24 | 25 | fn visit_bool(self, v: bool) -> Result 26 | where 27 | E: serde::de::Error, 28 | { 29 | Ok(OpValBool::Eq(v).into()) 30 | } 31 | 32 | fn visit_map(self, mut map: M) -> Result 33 | where 34 | M: MapAccess<'de>, 35 | { 36 | let mut opvals: Vec = Vec::new(); 37 | 38 | while let Some(k) = map.next_key::()? { 39 | // Note: Important to always call next_value 40 | let value = map.next_value::()?; 41 | let opval = OpValBool::op_value_to_op_val_type(&k, value).map_err(serde::de::Error::custom)?; 42 | opvals.push(opval) 43 | } 44 | 45 | Ok(OpValsBool(opvals)) 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/filter/json/ovs_de_number.rs: -------------------------------------------------------------------------------- 1 | use super::ovs_json::OpValueToOpValType; 2 | use crate::filter::{OpValFloat64, OpValInt32, OpValInt64, OpValsFloat64, OpValsInt32, OpValsInt64}; 3 | use serde::{de::MapAccess, de::Visitor, Deserialize, Deserializer}; 4 | use serde_json::Value; 5 | use std::fmt; 6 | 7 | // region: --- OpValsInt64 8 | impl<'de> Deserialize<'de> for OpValsInt64 { 9 | fn deserialize(deserializer: D) -> Result 10 | where 11 | D: Deserializer<'de>, 12 | { 13 | deserializer.deserialize_any(Int64OpValsVisitor) 14 | } 15 | } 16 | 17 | struct Int64OpValsVisitor; 18 | 19 | impl<'de> Visitor<'de> for Int64OpValsVisitor { 20 | type Value = OpValsInt64; // for deserialize 21 | 22 | fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { 23 | write!(formatter, "OpValsInt64 visitor not implemented for this type.") 24 | } 25 | 26 | fn visit_i64(self, v: i64) -> Result 27 | where 28 | E: serde::de::Error, 29 | { 30 | Ok(OpValInt64::Eq(v).into()) 31 | } 32 | 33 | fn visit_u64(self, v: u64) -> Result 34 | where 35 | E: serde::de::Error, 36 | { 37 | Ok(OpValInt64::Eq(v as i64).into()) 38 | } 39 | 40 | fn visit_map(self, mut map: M) -> Result 41 | where 42 | M: MapAccess<'de>, 43 | { 44 | let mut opvals: Vec = Vec::new(); 45 | while let Some(k) = map.next_key::()? { 46 | let value = map.next_value::()?; 47 | let opval = OpValInt64::op_value_to_op_val_type(&k, value).map_err(serde::de::Error::custom)?; 48 | opvals.push(opval) 49 | } 50 | 51 | Ok(OpValsInt64(opvals)) 52 | } 53 | } 54 | // endregion: --- OpValsInt64 55 | 56 | // region: --- OpValsInt32 57 | impl<'de> Deserialize<'de> for OpValsInt32 { 58 | fn deserialize(deserializer: D) -> Result 59 | where 60 | D: Deserializer<'de>, 61 | { 62 | deserializer.deserialize_any(Int32OpValsVisitor) 63 | } 64 | } 65 | 66 | struct Int32OpValsVisitor; 67 | 68 | impl<'de> Visitor<'de> for Int32OpValsVisitor { 69 | type Value = OpValsInt32; // for deserialize 70 | 71 | fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { 72 | write!(formatter, "OpValsInt32 visitor not implemented for this type.") 73 | } 74 | 75 | fn visit_i32(self, v: i32) -> Result 76 | where 77 | E: serde::de::Error, 78 | { 79 | Ok(OpValInt32::Eq(v).into()) 80 | } 81 | 82 | fn visit_map(self, mut map: M) -> Result 83 | where 84 | M: MapAccess<'de>, 85 | { 86 | let mut opvals: Vec = Vec::new(); 87 | 88 | while let Some(k) = map.next_key::()? { 89 | // Note: Important to always 90 | let value = map.next_value::()?; 91 | let opval = OpValInt32::op_value_to_op_val_type(&k, value).map_err(serde::de::Error::custom)?; 92 | opvals.push(opval) 93 | } 94 | 95 | Ok(OpValsInt32(opvals)) 96 | } 97 | } 98 | // endregion: --- OpValsInt64 99 | 100 | // region: --- OpValsFloat64 101 | impl<'de> Deserialize<'de> for OpValsFloat64 { 102 | fn deserialize(deserializer: D) -> Result 103 | where 104 | D: Deserializer<'de>, 105 | { 106 | deserializer.deserialize_any(FloatOpValsVisitor) 107 | } 108 | } 109 | 110 | struct FloatOpValsVisitor; 111 | 112 | impl<'de> Visitor<'de> for FloatOpValsVisitor { 113 | type Value = OpValsFloat64; // for deserialize 114 | 115 | fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { 116 | write!(formatter, "OpValsFloat64 visitor not implemented for this type.") 117 | } 118 | 119 | fn visit_f64(self, v: f64) -> Result 120 | where 121 | E: serde::de::Error, 122 | { 123 | Ok(OpValFloat64::Eq(v).into()) 124 | } 125 | 126 | fn visit_u64(self, v: u64) -> Result 127 | where 128 | E: serde::de::Error, 129 | { 130 | Ok(OpValFloat64::Eq(v as f64).into()) 131 | } 132 | 133 | fn visit_map(self, mut map: M) -> Result 134 | where 135 | M: MapAccess<'de>, 136 | { 137 | let mut opvals: Vec = Vec::new(); 138 | 139 | while let Some(k) = map.next_key::()? { 140 | // Note: Important to always 141 | let value = map.next_value::()?; 142 | let opval = OpValFloat64::op_value_to_op_val_type(&k, value).map_err(serde::de::Error::custom)?; 143 | opvals.push(opval) 144 | } 145 | 146 | Ok(OpValsFloat64(opvals)) 147 | } 148 | } 149 | // endregion: --- OpValsFloat64 150 | -------------------------------------------------------------------------------- /src/filter/json/ovs_de_string.rs: -------------------------------------------------------------------------------- 1 | use super::ovs_json::OpValueToOpValType; 2 | use crate::filter::{OpValString, OpValsString}; 3 | use serde::{de::MapAccess, de::Visitor, Deserialize, Deserializer}; 4 | use serde_json::Value; 5 | use std::fmt; 6 | 7 | impl<'de> Deserialize<'de> for OpValsString { 8 | fn deserialize(deserializer: D) -> Result 9 | where 10 | D: Deserializer<'de>, 11 | { 12 | deserializer.deserialize_any(StringOpValsVisitor) 13 | } 14 | } 15 | 16 | struct StringOpValsVisitor; 17 | 18 | impl<'de> Visitor<'de> for StringOpValsVisitor { 19 | type Value = OpValsString; // for deserialize 20 | 21 | fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { 22 | write!(formatter, "StringOpValsVisitor visitor not implemented for this type.") 23 | } 24 | 25 | fn visit_str(self, v: &str) -> Result 26 | where 27 | E: serde::de::Error, 28 | { 29 | Ok(OpValString::Eq(v.to_string()).into()) 30 | } 31 | 32 | fn visit_string(self, v: String) -> Result 33 | where 34 | E: serde::de::Error, 35 | { 36 | Ok(OpValString::Eq(v).into()) 37 | } 38 | 39 | fn visit_map(self, mut map: M) -> Result 40 | where 41 | M: MapAccess<'de>, 42 | { 43 | let mut opvals: Vec = Vec::new(); 44 | 45 | // Note: If use next_key::<&str>, error "invalid type: string \"$contains\", expected a borrowed string" 46 | // so using String for now. 47 | while let Some(k) = map.next_key::()? { 48 | // Note: Important to always call next_value 49 | let value = map.next_value::()?; 50 | let opval = OpValString::op_value_to_op_val_type(&k, value).map_err(serde::de::Error::custom)?; 51 | opvals.push(opval) 52 | } 53 | 54 | Ok(OpValsString(opvals)) 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/filter/json/ovs_de_value.rs: -------------------------------------------------------------------------------- 1 | use crate::filter::json::ovs_json::OpValueToOpValType; 2 | use crate::filter::{OpValValue, OpValsValue}; 3 | use serde::{Deserialize, Deserializer}; 4 | use serde_json::Value; 5 | 6 | impl<'de> Deserialize<'de> for OpValsValue { 7 | fn deserialize(deserializer: D) -> Result 8 | where 9 | D: Deserializer<'de>, 10 | { 11 | let v: Value = Deserialize::deserialize(deserializer)?; 12 | 13 | let op_vals_value: OpValsValue = if v.is_number() || v.is_boolean() || v.is_string() { 14 | OpValValue::Eq(v).into() 15 | } else if v.is_object() { 16 | let mut opvals: Vec = Vec::new(); 17 | let Value::Object(obj) = v else { 18 | return Err(serde::de::Error::custom("OpValValue should be object")); 19 | }; 20 | 21 | for (key, value) in obj.into_iter() { 22 | let op_val = OpValValue::op_value_to_op_val_type(&key, value).map_err(serde::de::Error::custom)?; 23 | opvals.push(op_val); 24 | } 25 | OpValsValue(opvals) 26 | } else { 27 | return Err(serde::de::Error::custom( 28 | "OpValJson value mut be either number, bool, string, or an Object", 29 | )); 30 | }; 31 | 32 | Ok(op_vals_value) 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/filter/json/ovs_json.rs: -------------------------------------------------------------------------------- 1 | use crate::Result; 2 | use serde_json::Value; 3 | 4 | pub trait OpValueToOpValType { 5 | /// e.g., `{"$contains": "World", "$startsWith": "Hello"} 6 | fn op_value_to_op_val_type(op: &str, value: Value) -> Result 7 | where 8 | Self: Sized; 9 | } 10 | -------------------------------------------------------------------------------- /src/filter/list_options/mod.rs: -------------------------------------------------------------------------------- 1 | mod order_by; 2 | 3 | pub use order_by::*; 4 | use serde::Deserialize; 5 | 6 | #[derive(Default, Debug, Clone, Deserialize)] 7 | pub struct ListOptions { 8 | pub limit: Option, 9 | pub offset: Option, 10 | pub order_bys: Option, 11 | } 12 | 13 | // region: --- Constructors 14 | 15 | impl ListOptions { 16 | pub fn from_limit(limit: i64) -> Self { 17 | Self { 18 | limit: Some(limit), 19 | ..Default::default() 20 | } 21 | } 22 | 23 | pub fn from_offset_limit(offset: i64, limit: i64) -> Self { 24 | Self { 25 | limit: Some(limit), 26 | offset: Some(offset), 27 | ..Default::default() 28 | } 29 | } 30 | 31 | pub fn from_order_bys(order_bys: impl Into) -> Self { 32 | Self { 33 | order_bys: Some(order_bys.into()), 34 | ..Default::default() 35 | } 36 | } 37 | } 38 | 39 | // endregion: --- Constructors 40 | 41 | // region: --- Froms 42 | 43 | impl From for ListOptions { 44 | fn from(val: OrderBys) -> Self { 45 | Self { 46 | order_bys: Some(val), 47 | ..Default::default() 48 | } 49 | } 50 | } 51 | 52 | impl From for Option { 53 | fn from(val: OrderBys) -> Self { 54 | Some(ListOptions { 55 | order_bys: Some(val), 56 | ..Default::default() 57 | }) 58 | } 59 | } 60 | 61 | impl From for ListOptions { 62 | fn from(val: OrderBy) -> Self { 63 | Self { 64 | order_bys: Some(OrderBys::from(val)), 65 | ..Default::default() 66 | } 67 | } 68 | } 69 | 70 | impl From for Option { 71 | fn from(val: OrderBy) -> Self { 72 | Some(ListOptions { 73 | order_bys: Some(OrderBys::from(val)), 74 | ..Default::default() 75 | }) 76 | } 77 | } 78 | 79 | // endregion: --- Froms 80 | 81 | // region: --- with-sea-query 82 | #[cfg(feature = "with-sea-query")] 83 | mod with_sea_query { 84 | use super::*; 85 | use sea_query::SelectStatement; 86 | 87 | impl ListOptions { 88 | pub fn apply_to_sea_query(self, select_query: &mut SelectStatement) { 89 | fn as_positive_u64(num: i64) -> u64 { 90 | if num < 0 { 91 | 0 92 | } else { 93 | num as u64 94 | } 95 | } 96 | if let Some(limit) = self.limit { 97 | select_query.limit(as_positive_u64(limit)); // Note: Negative == 0 98 | } 99 | 100 | if let Some(offset) = self.offset { 101 | select_query.offset(as_positive_u64(offset)); // Note: Negative == 0 102 | } 103 | 104 | if let Some(order_bys) = self.order_bys { 105 | for (col, order) in order_bys.into_sea_col_order_iter() { 106 | select_query.order_by(col, order); 107 | } 108 | } 109 | } 110 | } 111 | } 112 | // endregion: --- with-sea-query 113 | -------------------------------------------------------------------------------- /src/filter/list_options/order_by.rs: -------------------------------------------------------------------------------- 1 | // region: --- OrderBy 2 | #[derive(Debug, Clone)] 3 | pub enum OrderBy { 4 | Asc(String), 5 | Desc(String), 6 | } 7 | 8 | impl core::fmt::Display for OrderBy { 9 | fn fmt(&self, fmt: &mut core::fmt::Formatter) -> core::fmt::Result { 10 | match self { 11 | OrderBy::Asc(val) => { 12 | fmt.write_str(val)?; 13 | fmt.write_str(" ")?; 14 | fmt.write_str("ASC")?; 15 | } 16 | OrderBy::Desc(val) => { 17 | fmt.write_str(val)?; 18 | fmt.write_str(" ")?; 19 | fmt.write_str("DESC")?; 20 | } 21 | }; 22 | 23 | Ok(()) 24 | } 25 | } 26 | 27 | impl> From for OrderBy { 28 | fn from(val: T) -> Self { 29 | let raw: &str = val.as_ref(); 30 | 31 | if let Some(stripped) = raw.strip_prefix('!') { 32 | OrderBy::Desc(stripped.to_string()) 33 | } else { 34 | OrderBy::Asc(raw.to_string()) 35 | } 36 | } 37 | } 38 | 39 | // endregion: --- OrderBy 40 | 41 | // region: --- OrderBys 42 | #[derive(Debug, Clone)] 43 | pub struct OrderBys(Vec); 44 | 45 | impl OrderBys { 46 | pub fn new(v: Vec) -> Self { 47 | OrderBys(v) 48 | } 49 | pub fn order_bys(self) -> Vec { 50 | self.0 51 | } 52 | } 53 | 54 | // This will allow us to iterate over &OrderBys 55 | impl<'a> IntoIterator for &'a OrderBys { 56 | type Item = &'a OrderBy; 57 | type IntoIter = std::slice::Iter<'a, OrderBy>; 58 | 59 | fn into_iter(self) -> Self::IntoIter { 60 | self.0.iter() 61 | } 62 | } 63 | 64 | // This will allow us to iterate over OrderBys directly (consuming it) 65 | impl IntoIterator for OrderBys { 66 | type Item = OrderBy; 67 | type IntoIter = std::vec::IntoIter; 68 | 69 | fn into_iter(self) -> Self::IntoIter { 70 | self.0.into_iter() 71 | } 72 | } 73 | 74 | // NOTE: If we want the Vec and T, we have to make the individual from 75 | // specific to the type. Otherwise, conflict. 76 | 77 | impl From<&str> for OrderBys { 78 | fn from(val: &str) -> Self { 79 | OrderBys(vec![val.into()]) 80 | } 81 | } 82 | impl From<&String> for OrderBys { 83 | fn from(val: &String) -> Self { 84 | OrderBys(vec![val.into()]) 85 | } 86 | } 87 | impl From for OrderBys { 88 | fn from(val: String) -> Self { 89 | OrderBys(vec![val.into()]) 90 | } 91 | } 92 | 93 | impl From for OrderBys { 94 | fn from(val: OrderBy) -> Self { 95 | OrderBys(vec![val]) 96 | } 97 | } 98 | 99 | impl> From> for OrderBys { 100 | fn from(val: Vec) -> Self { 101 | let d = val.into_iter().map(|o| OrderBy::from(o)).collect::>(); 102 | OrderBys(d) 103 | } 104 | } 105 | 106 | // endregion: --- OrderBys 107 | 108 | // region: --- with-sea-query 109 | #[cfg(feature = "with-sea-query")] 110 | mod with_sea_query { 111 | use super::*; 112 | use crate::sea_utils::StringIden; 113 | use sea_query::IntoColumnRef; 114 | 115 | impl OrderBys { 116 | pub fn into_sea_col_order_iter(self) -> impl Iterator { 117 | self.0.into_iter().map(OrderBy::into_sea_col_order) 118 | } 119 | } 120 | 121 | impl OrderBy { 122 | pub fn into_sea_col_order(self) -> (sea_query::ColumnRef, sea_query::Order) { 123 | let (col, order) = match self { 124 | OrderBy::Asc(col) => (StringIden(col), sea_query::Order::Asc), 125 | OrderBy::Desc(col) => (StringIden(col), sea_query::Order::Desc), 126 | }; 127 | 128 | (col.into_column_ref(), order) 129 | } 130 | } 131 | } 132 | // endregion: --- with-sea-query 133 | -------------------------------------------------------------------------------- /src/filter/mod.rs: -------------------------------------------------------------------------------- 1 | //! modql::filter enables an expressive filtering language as described in [https://joql.org](https://joql.org). 2 | //! It's serialization-agnostic but also provides JSON deserialization for convenience. 3 | 4 | // -- Sub-Module 5 | #[cfg(feature = "with-sea-query")] 6 | mod into_sea; 7 | mod json; 8 | mod list_options; 9 | pub(crate) mod nodes; 10 | pub(crate) mod ops; 11 | 12 | // -- Re-Exports 13 | pub use list_options::*; 14 | pub use modql_macros::FilterNodes; 15 | pub use nodes::group::*; 16 | pub use nodes::node::*; 17 | pub use ops::op_val_bool::*; 18 | pub use ops::op_val_nums::*; 19 | pub use ops::op_val_string::*; 20 | pub use ops::op_val_value::*; 21 | pub use ops::*; 22 | 23 | #[cfg(feature = "with-sea-query")] 24 | pub use into_sea::*; 25 | -------------------------------------------------------------------------------- /src/filter/nodes/group.rs: -------------------------------------------------------------------------------- 1 | use crate::filter::{FilterNode, IntoFilterNodes}; 2 | 3 | // region: --- Filter Group 4 | /// A FilterGroup is a vector of FilterNode that are intended to be interpreted as AND. 5 | #[derive(Debug, Clone)] 6 | pub struct FilterGroup(Vec); 7 | 8 | impl FilterGroup { 9 | pub fn nodes(&self) -> &Vec { 10 | &self.0 11 | } 12 | } 13 | 14 | impl IntoIterator for FilterGroup { 15 | type Item = FilterNode; 16 | type IntoIter = std::vec::IntoIter; 17 | 18 | fn into_iter(self) -> Self::IntoIter { 19 | self.0.into_iter() 20 | } 21 | } 22 | 23 | impl From> for FilterGroup { 24 | fn from(val: Vec) -> Self { 25 | FilterGroup(val) 26 | } 27 | } 28 | 29 | impl From for FilterGroup { 30 | fn from(val: FilterNode) -> Self { 31 | FilterGroup(vec![val]) 32 | } 33 | } 34 | 35 | // endregion: --- Filter Group 36 | 37 | // region: --- FilterGroups 38 | 39 | /// A FilterGroups is a vector of FilterGroup, and each groups are intended to be OR between them, 40 | /// and inside the group, that will be the And 41 | #[derive(Debug, Clone)] 42 | pub struct FilterGroups(Vec); 43 | 44 | impl FilterGroups { 45 | /// Add a new or group (`Vec`). 46 | /// It will be OR with its peer groups, and the content of the vector should interpreted as AND. 47 | pub fn add_group(&mut self, group: Vec) -> &mut Self { 48 | self.0.push(FilterGroup(group)); 49 | self 50 | } 51 | 52 | pub fn groups(&self) -> &Vec { 53 | &self.0 54 | } 55 | 56 | pub fn into_vec(self) -> Vec { 57 | self.0 58 | } 59 | } 60 | 61 | /// Create a FilterGroups from a vec or vec of filternode 62 | impl From>> for FilterGroups { 63 | fn from(val: Vec>) -> Self { 64 | FilterGroups(val.into_iter().map(FilterGroup::from).collect()) 65 | } 66 | } 67 | 68 | /// Create a FilterGroups of single FilterNode vector (group of one) 69 | impl From> for FilterGroups { 70 | fn from(val: Vec) -> Self { 71 | FilterGroups(vec![val.into()]) 72 | } 73 | } 74 | 75 | /// Create a FilterGroups from a single FilterNode 76 | impl From for FilterGroups { 77 | fn from(val: FilterNode) -> Self { 78 | FilterGroups(vec![val.into()]) 79 | } 80 | } 81 | 82 | impl From for Option { 83 | fn from(val: FilterNode) -> Self { 84 | Some(val.into()) 85 | } 86 | } 87 | 88 | impl From for FilterGroups { 89 | fn from(val: FilterGroup) -> Self { 90 | FilterGroups(vec![val]) 91 | } 92 | } 93 | 94 | impl From for Option { 95 | fn from(val: FilterGroup) -> Self { 96 | Some(val.into()) 97 | } 98 | } 99 | 100 | impl From> for FilterGroups 101 | where 102 | F: IntoFilterNodes, 103 | { 104 | fn from(filters: Vec) -> Self { 105 | let filters: Vec<_> = filters.into_iter().map(|f| f.filter_nodes(None)).collect(); 106 | filters.into() 107 | } 108 | } 109 | 110 | // endregion: --- FilterGroups 111 | 112 | // region: --- with-sea-query 113 | #[cfg(feature = "with-sea-query")] 114 | mod with_sea_query { 115 | use super::*; 116 | use crate::filter::{IntoSeaError, SeaResult}; 117 | use sea_query::{Condition, ConditionExpression}; 118 | 119 | impl TryFrom for Condition { 120 | type Error = IntoSeaError; 121 | fn try_from(val: FilterGroup) -> SeaResult { 122 | // Note: this will fail on first, error found 123 | let exprs: SeaResult>> = 124 | val.into_iter().map(|node| node.into_sea_cond_expr_list()).collect(); 125 | // We flattlen the Vec> to a Vec 126 | let exprs_flat = exprs?.into_iter().flatten(); 127 | 128 | let mut cond = Condition::all(); 129 | for cond_item in exprs_flat { 130 | cond = cond.add(cond_item); 131 | } 132 | Ok(cond) 133 | } 134 | } 135 | 136 | impl TryFrom for Condition { 137 | type Error = IntoSeaError; 138 | fn try_from(val: FilterGroups) -> SeaResult { 139 | let mut cond = Condition::any(); 140 | 141 | for group in val.0.into_iter() { 142 | cond = cond.add(Condition::try_from(group)?); 143 | } 144 | 145 | Ok(cond) 146 | } 147 | } 148 | 149 | impl FilterGroups { 150 | pub fn into_sea_condition(self) -> SeaResult { 151 | let mut cond = Condition::any(); 152 | 153 | for group in self.0.into_iter() { 154 | cond = cond.add(Condition::try_from(group)?); 155 | } 156 | 157 | Ok(cond) 158 | } 159 | } 160 | } 161 | // endregion: --- with-sea-query 162 | -------------------------------------------------------------------------------- /src/filter/nodes/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod group; 2 | pub mod node; 3 | -------------------------------------------------------------------------------- /src/filter/nodes/node.rs: -------------------------------------------------------------------------------- 1 | use crate::filter::ops::OpVal; 2 | use crate::filter::{OpValBool, OpValFloat64, OpValInt32, OpValInt64, OpValString}; 3 | 4 | pub trait IntoFilterNodes { 5 | fn filter_nodes(self, rel: Option) -> Vec; 6 | } 7 | 8 | #[derive(Debug, Clone, Default)] 9 | pub struct FilterNodeOptions { 10 | pub cast_as: Option, // for db casting. e.g., Will be applied to sea-query value. 11 | pub cast_column_as: Option, // for db casting. e.g., Will be applied to sea-query column. 12 | } 13 | 14 | #[derive(Debug, Clone)] 15 | pub struct FilterNode { 16 | pub rel: Option, // would be for the project.title (project in this case) 17 | pub name: String, 18 | pub opvals: Vec, 19 | pub options: FilterNodeOptions, 20 | 21 | #[cfg(feature = "with-sea-query")] 22 | pub for_sea_condition: Option, 23 | } 24 | 25 | impl FilterNode { 26 | pub fn new(name: impl Into, opvals: impl Into>) -> FilterNode { 27 | FilterNode { 28 | rel: None, 29 | name: name.into(), 30 | opvals: opvals.into(), 31 | options: FilterNodeOptions::default(), 32 | 33 | #[cfg(feature = "with-sea-query")] 34 | for_sea_condition: None, 35 | } 36 | } 37 | 38 | pub fn new_with_rel(rel: Option, name: impl Into, opvals: impl Into>) -> FilterNode { 39 | FilterNode { 40 | rel, 41 | name: name.into(), 42 | opvals: opvals.into(), 43 | options: FilterNodeOptions::default(), 44 | 45 | #[cfg(feature = "with-sea-query")] 46 | for_sea_condition: None, 47 | } 48 | } 49 | } 50 | 51 | // region: --- From Tuples (OpValType) 52 | // Implements the From trait from tuples to FilterNode 53 | macro_rules! from_tuples_opval { 54 | ($($OV:ident),+) => { 55 | $( 56 | /// From trait from (prop_name, OpVal) for FilterNode 57 | /// (e.g., `let node: FilterNode = ("id", IntOpVal::Gt(1)).into()`) 58 | impl From<(&str, $OV)> for FilterNode { 59 | fn from((name, ov): (&str, $OV)) -> Self { 60 | let opvals = vec![ov.into()]; 61 | FilterNode::new(name, opvals) 62 | } 63 | } 64 | 65 | /// From `trait from (prop_name, Vec)` for FilterNode 66 | /// (e.g., `let node: FilterNode = (prop_name, Vec).into()`) 67 | impl From<(&str, Vec<$OV>)> for FilterNode { 68 | fn from((name, ovs): (&str, Vec<$OV>)) -> Self { 69 | let opvals: Vec = ovs.into_iter().map(|v| OpVal::from(v)).collect(); 70 | FilterNode::new(name, opvals) 71 | } 72 | } 73 | )+ 74 | }; 75 | } 76 | from_tuples_opval!( 77 | // String 78 | OpValString, 79 | // Nums 80 | // OpValUint64, 81 | // OpValUint32, 82 | OpValInt64, 83 | // OpValInt32, 84 | OpValFloat64, 85 | // OpValFloat32, 86 | // Bool 87 | OpValBool 88 | ); 89 | // endregion: --- From Tuples (OpValType) 90 | 91 | // region: --- Froms Tuples (String val) 92 | 93 | impl From<(&str, &str)> for FilterNode { 94 | fn from((name, ov): (&str, &str)) -> Self { 95 | let opvals = vec![OpValString::Eq(ov.to_string()).into()]; 96 | FilterNode::new(name.to_string(), opvals) 97 | } 98 | } 99 | 100 | impl From<(&str, &String)> for FilterNode { 101 | fn from((name, ov): (&str, &String)) -> Self { 102 | let opvals = vec![OpValString::Eq(ov.to_string()).into()]; 103 | FilterNode::new(name.to_string(), opvals) 104 | } 105 | } 106 | 107 | impl From<(&str, String)> for FilterNode { 108 | fn from((name, ov): (&str, String)) -> Self { 109 | let opvals = vec![OpValString::Eq(ov).into()]; 110 | FilterNode::new(name.to_string(), opvals) 111 | } 112 | } 113 | 114 | // endregion: --- Froms Tuples (String val) 115 | 116 | // region: --- From Tuples (num val) 117 | // - `nt` e.g., `u64` 118 | // - `ov` e.g., `OpValUint64` 119 | macro_rules! from_tuples_num{ 120 | ($(($nt:ty, $ov:ident)),+) => { 121 | $( 122 | 123 | impl From<(&str, $nt)> for FilterNode { 124 | fn from((name, ov): (&str, $nt)) -> Self { 125 | let opvals = vec![$ov::Eq(ov).into()]; 126 | FilterNode::new(name.to_string(), opvals) 127 | } 128 | } 129 | )+ 130 | }; 131 | } 132 | 133 | from_tuples_num!( 134 | // (u64, OpValUint64), 135 | // (u32, OpValUint32), 136 | (i64, OpValInt64), 137 | (i32, OpValInt32), 138 | // (f32, OpValFloat32), 139 | (f64, OpValFloat64) 140 | ); 141 | 142 | // endregion: --- From Tuples (num val) 143 | 144 | // region: --- From Tuples (bool val) 145 | impl From<(&str, bool)> for FilterNode { 146 | fn from((name, ov): (&str, bool)) -> Self { 147 | let opvals = vec![OpValBool::Eq(ov).into()]; 148 | FilterNode::new(name.to_string(), opvals) 149 | } 150 | } 151 | 152 | // endregion: --- From Tuples (bool val) 153 | 154 | // region: --- with-sea-query 155 | #[cfg(feature = "with-sea-query")] 156 | mod with_sea_query { 157 | use super::*; 158 | use crate::filter::{ForSeaCondition, IntoSeaError, OpValValue, SeaResult}; 159 | use crate::sea_utils::StringIden; 160 | use sea_query::{ColumnRef, ConditionExpression, IntoColumnRef, IntoIden}; 161 | 162 | impl FilterNode { 163 | pub fn into_sea_cond_expr_list(self) -> SeaResult> { 164 | let col: ColumnRef = match self.rel { 165 | Some(rel) => ColumnRef::TableColumn(StringIden(rel).into_iden(), StringIden(self.name).into_iden()), 166 | None => StringIden(self.name).into_column_ref(), 167 | }; 168 | let mut node_sea_exprs: Vec = Vec::new(); 169 | let for_sea_cond = self.for_sea_condition; 170 | let node_options = &self.options; 171 | 172 | for op_val in self.opvals.into_iter() { 173 | let cond_expr = match op_val { 174 | OpVal::String(ov) => ov.into_sea_cond_expr(&col, node_options)?, 175 | OpVal::Int64(ov) => ov.into_sea_cond_expr(&col, node_options)?, 176 | OpVal::Int32(ov) => ov.into_sea_cond_expr(&col, node_options)?, 177 | OpVal::Float64(ov) => ov.into_sea_cond_expr(&col, node_options)?, 178 | OpVal::Bool(ov) => ov.into_sea_cond_expr(&col, node_options)?, 179 | OpVal::Value(ov) => { 180 | let Some(for_sea_cond) = for_sea_cond.as_ref() else { 181 | return Err(IntoSeaError::Custom( 182 | "OpValsValue must have a #[modql(to_sea_value_fn=\"fn_name\"] or to_sea_condition_fn attribute" 183 | .to_string(), 184 | )); 185 | }; 186 | 187 | match for_sea_cond { 188 | ForSeaCondition::ToSeaValue(to_sea_value) => { 189 | OpValValue::into_sea_cond_expr_with_json_to_sea(ov, &col, node_options, to_sea_value)? 190 | } 191 | ForSeaCondition::ToSeaCondition(to_sea_condition) => to_sea_condition.call(&col, ov)?, 192 | } 193 | } 194 | }; 195 | 196 | node_sea_exprs.push(cond_expr); 197 | } 198 | 199 | Ok(node_sea_exprs) 200 | } 201 | } 202 | } 203 | // endregion: --- with-sea-query 204 | -------------------------------------------------------------------------------- /src/filter/ops/mod.rs: -------------------------------------------------------------------------------- 1 | use crate::filter::OpValsValue; 2 | use crate::filter::*; 3 | 4 | pub mod op_val_bool; 5 | pub mod op_val_nums; 6 | pub mod op_val_string; 7 | pub mod op_val_value; 8 | 9 | // region: --- OpVal 10 | #[derive(Debug, Clone)] 11 | pub enum OpVal { 12 | String(OpValString), 13 | 14 | Int64(OpValInt64), 15 | Int32(OpValInt32), 16 | 17 | Float64(OpValFloat64), 18 | 19 | Bool(OpValBool), 20 | Value(OpValValue), 21 | } 22 | 23 | // endregion: --- OpVal 24 | 25 | // region: --- From [Type]OpVal & Vec<[Type]OpVal> to [Type]OpVals 26 | 27 | // Convenient implementation when single constraints. 28 | // Common implementation 29 | macro_rules! impl_from_for_opvals { 30 | ($($ov:ident, $ovs:ident),*) => { 31 | $( 32 | impl From<$ov> for $ovs { 33 | fn from(val: $ov) -> Self { 34 | $ovs(vec![val]) 35 | } 36 | } 37 | 38 | impl From> for $ovs { 39 | fn from(val: Vec<$ov>) -> Self { 40 | $ovs(val) 41 | } 42 | } 43 | )* 44 | }; 45 | } 46 | 47 | // For all opvals (must specified the pair as macro rules are hygienic) 48 | impl_from_for_opvals!( 49 | // String 50 | OpValString, 51 | OpValsString, 52 | // Ints 53 | OpValInt64, 54 | OpValsInt64, 55 | OpValInt32, 56 | OpValsInt32, 57 | // Floats 58 | OpValFloat64, 59 | OpValsFloat64, 60 | // Bool 61 | OpValBool, 62 | OpValsBool, 63 | // OpValJson 64 | OpValValue, 65 | OpValsValue 66 | ); 67 | 68 | // endregion: --- From [Type]OpVal & Vec<[Type]OpVal> to [Type]OpVals 69 | 70 | #[cfg(feature = "with-sea-query")] 71 | pub use self::with_sea_query::*; 72 | 73 | #[cfg(feature = "with-sea-query")] 74 | mod with_sea_query { 75 | use sea_query::{ColumnRef, ConditionExpression, Expr}; 76 | 77 | pub fn sea_is_col_value_null(col: ColumnRef, null: bool) -> ConditionExpression { 78 | if null { 79 | Expr::col(col).is_null().into() 80 | } else { 81 | Expr::col(col).is_not_null().into() 82 | } 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /src/filter/ops/op_val_bool.rs: -------------------------------------------------------------------------------- 1 | use crate::filter::OpVal; 2 | 3 | #[derive(Debug, Clone)] 4 | pub struct OpValsBool(pub Vec); 5 | 6 | #[derive(Debug, Clone)] 7 | pub enum OpValBool { 8 | Eq(bool), 9 | Not(bool), 10 | Null(bool), 11 | } 12 | 13 | // region: --- Simple Value to Eq BoolOpVal 14 | impl From for OpValBool { 15 | fn from(val: bool) -> Self { 16 | OpValBool::Eq(val) 17 | } 18 | } 19 | 20 | impl From<&bool> for OpValBool { 21 | fn from(val: &bool) -> Self { 22 | OpValBool::Eq(*val) 23 | } 24 | } 25 | // endregion: --- Simple Value to Eq BoolOpVal 26 | 27 | // region: --- Simple Value to Eq BoolOpVals 28 | impl From for OpValsBool { 29 | fn from(val: bool) -> Self { 30 | OpValBool::from(val).into() 31 | } 32 | } 33 | 34 | impl From<&bool> for OpValsBool { 35 | fn from(val: &bool) -> Self { 36 | OpValBool::from(*val).into() 37 | } 38 | } 39 | // endregion: --- Simple Value to Eq BoolOpVals 40 | 41 | // region: --- BoolOpVal to OpVal 42 | impl From for OpVal { 43 | fn from(val: OpValBool) -> Self { 44 | OpVal::Bool(val) 45 | } 46 | } 47 | // endregion: --- BoolOpVal to OpVal 48 | 49 | // region: --- Simple Value to Eq OpVal::Bool(BoolOpVal::Eq) 50 | impl From for OpVal { 51 | fn from(val: bool) -> Self { 52 | OpValBool::Eq(val).into() 53 | } 54 | } 55 | 56 | impl From<&bool> for OpVal { 57 | fn from(val: &bool) -> Self { 58 | OpValBool::Eq(*val).into() 59 | } 60 | } 61 | // endregion: --- Simple Value to Eq OpVal::Bool(BoolOpVal::Eq) 62 | 63 | // region: --- json 64 | mod json { 65 | use super::*; 66 | use crate::filter::json::OpValueToOpValType; 67 | use crate::{Error, Result}; 68 | use serde_json::Value; 69 | 70 | impl OpValueToOpValType for OpValBool { 71 | fn op_value_to_op_val_type(op: &str, value: Value) -> Result 72 | where 73 | Self: Sized, 74 | { 75 | let ov = match (op, value) { 76 | ("$eq", Value::Bool(v)) => OpValBool::Eq(v), 77 | ("$not", Value::Bool(v)) => OpValBool::Not(v), 78 | ("$null", Value::Bool(v)) => OpValBool::Not(v), 79 | (_, value) => { 80 | return Err(Error::JsonOpValNotSupported { 81 | operator: op.to_string(), 82 | value, 83 | }) 84 | } 85 | }; 86 | 87 | Ok(ov) 88 | } 89 | } 90 | } 91 | // endregion: --- json 92 | 93 | // region: --- with-sea-query 94 | #[cfg(feature = "with-sea-query")] 95 | mod with_sea_query { 96 | use super::*; 97 | use crate::filter::{sea_is_col_value_null, FilterNodeOptions, SeaResult}; 98 | use crate::into_node_value_expr; 99 | use sea_query::{BinOper, ColumnRef, ConditionExpression, SimpleExpr}; 100 | 101 | impl OpValBool { 102 | pub fn into_sea_cond_expr( 103 | self, 104 | col: &ColumnRef, 105 | node_options: &FilterNodeOptions, 106 | ) -> SeaResult { 107 | let binary_fn = |op: BinOper, val: bool| { 108 | let vxpr = into_node_value_expr(val, node_options); 109 | ConditionExpression::SimpleExpr(SimpleExpr::binary(col.clone().into(), op, vxpr)) 110 | }; 111 | 112 | let cond = match self { 113 | OpValBool::Eq(b) => binary_fn(BinOper::Equal, b), 114 | OpValBool::Not(b) => binary_fn(BinOper::NotEqual, b), 115 | OpValBool::Null(null) => sea_is_col_value_null(col.clone(), null), 116 | }; 117 | 118 | Ok(cond) 119 | } 120 | } 121 | } 122 | // endregion: --- with-sea-query 123 | -------------------------------------------------------------------------------- /src/filter/ops/op_val_nums.rs: -------------------------------------------------------------------------------- 1 | use crate::filter::OpVal; 2 | 3 | /// - `ovs` OpValsType, e.g., `OpValsUint64` 4 | /// - `ov` OpValType, e.g., `OpValUint64` 5 | /// - `nt` Number type, e.g., `u64` 6 | /// - `vr` Opval Variant e.g., `OpVal::Uint64` 7 | macro_rules! impl_op_val { 8 | ($(($ovs:ident, $ov:ident,$nt:ty, $vr:expr)),+) => { 9 | $( 10 | 11 | #[derive(Debug, Clone)] 12 | pub struct $ovs(pub Vec<$ov>); 13 | 14 | #[derive(Debug, Clone)] 15 | pub enum $ov { 16 | Eq($nt), 17 | Not($nt), 18 | In(Vec<$nt>), 19 | NotIn(Vec<$nt>), 20 | Lt($nt), 21 | Lte($nt), 22 | Gt($nt), 23 | Gte($nt), 24 | Null(bool), 25 | } 26 | 27 | // region: --- Simple value to Eq e.g., OpValUint64 28 | impl From<$nt> for $ov { 29 | fn from(val: $nt) -> Self { 30 | $ov::Eq(val) 31 | } 32 | } 33 | 34 | impl From<&$nt> for $ov { 35 | fn from(val: &$nt) -> Self { 36 | $ov::Eq(*val) 37 | } 38 | } 39 | // endregion: --- Simple value to Eq e.g., OpValUint64 40 | 41 | // region: --- Simple value to Eq e.g., OpValsUint64 42 | impl From<$nt> for $ovs { 43 | fn from(val: $nt) -> Self { 44 | $ov::from(val).into() 45 | } 46 | } 47 | 48 | impl From<&$nt> for $ovs { 49 | fn from(val: &$nt) -> Self { 50 | $ov::from(*val).into() 51 | } 52 | } 53 | // endregion: --- Simple value to Eq e.g., OpValsUint64 54 | 55 | // region: --- e.g., OpValUint64 to OpVal 56 | impl From<$ov> for OpVal { 57 | fn from(val: $ov) -> Self { 58 | $vr(val) 59 | } 60 | } 61 | // endregion: --- e.g., OpValUint64 to OpVal 62 | 63 | // region: --- Primitive to OpVal::Int(IntOpVal::Eq) 64 | impl From<$nt> for OpVal { 65 | fn from(val: $nt) -> Self { 66 | $ov::Eq(val).into() 67 | } 68 | } 69 | 70 | impl From<&$nt> for OpVal { 71 | fn from(val: &$nt) -> Self { 72 | $ov::Eq(*val).into() 73 | } 74 | } 75 | // endregion: --- Primitive to OpVal::Int(IntOpVal::Eq) 76 | )+ 77 | }; 78 | } 79 | 80 | impl_op_val!( 81 | (OpValsInt64, OpValInt64, i64, OpVal::Int64), 82 | (OpValsInt32, OpValInt32, i32, OpVal::Int32), 83 | (OpValsFloat64, OpValFloat64, f64, OpVal::Float64) 84 | ); 85 | 86 | mod json { 87 | use super::*; 88 | use crate::filter::json::OpValueToOpValType; 89 | use crate::{Error, Result}; 90 | use serde_json::{Number, Value}; 91 | 92 | // - `ov` e.g., `OpValInt64` 93 | // - `asfn` e.g., `as_i64` 94 | macro_rules! from_json_to_opval_num{ 95 | ($(($ov:ident, $asfn:expr)),+) => { 96 | $( 97 | 98 | /// match a the op_value 99 | impl OpValueToOpValType for $ov { 100 | 101 | fn op_value_to_op_val_type(op: &str, value: Value) -> Result 102 | where 103 | Self: Sized, 104 | { 105 | 106 | // FIXME: Needs to do the In/Array patterns. 107 | let ov = match (op, value) { 108 | ("$eq", Value::Number(num)) => $ov::Eq($asfn(num)?), 109 | ("$in", value) => { 110 | let nums = into_numbers(value)?; 111 | let nums: Result> = nums.into_iter().map($asfn).collect(); 112 | let nums = nums?; 113 | $ov::In(nums) 114 | }, 115 | ("$not", Value::Number(num)) => $ov::Not($asfn(num)?), 116 | ("$notIn", value) => { 117 | let nums = into_numbers(value)?; 118 | let nums: Result> = nums.into_iter().map($asfn).collect(); 119 | let nums = nums?; 120 | $ov::NotIn(nums) 121 | }, 122 | 123 | ("$lt", Value::Number(num)) => $ov::Lt($asfn(num)?), 124 | ("$lte", Value::Number(num)) => $ov::Lte($asfn(num)?), 125 | 126 | ("$gt", Value::Number(num)) => $ov::Gt($asfn(num)?), 127 | ("$gte", Value::Number(num)) => $ov::Gte($asfn(num)?), 128 | 129 | ("$null", Value::Bool(v)) => $ov::Null(v), 130 | 131 | (_, value) => return Err(Error::JsonOpValNotSupported{ 132 | operator: op.to_string(), 133 | value, 134 | }), 135 | }; 136 | 137 | Ok(ov) 138 | } 139 | } 140 | )+ 141 | }; 142 | } 143 | 144 | from_json_to_opval_num!((OpValInt64, as_i64), (OpValInt32, as_i32), (OpValFloat64, as_f64)); 145 | 146 | fn as_i64(num: Number) -> Result { 147 | num.as_i64().ok_or(Error::JsonValNotOfType("i64")) 148 | } 149 | 150 | fn as_i32(num: Number) -> Result { 151 | num.as_i64().map(|n| n as i32).ok_or(Error::JsonValNotOfType("i32")) 152 | } 153 | 154 | fn as_f64(num: Number) -> Result { 155 | num.as_f64().ok_or(Error::JsonValNotOfType("f64")) 156 | } 157 | 158 | fn into_numbers(value: Value) -> Result> { 159 | let mut values = Vec::new(); 160 | 161 | let Value::Array(array) = value else { 162 | return Err(Error::JsonValArrayWrongType { actual_value: value }); 163 | }; 164 | 165 | for item in array.into_iter() { 166 | if let Value::Number(item) = item { 167 | values.push(item); 168 | } else { 169 | return Err(Error::JsonValArrayItemNotOfType { 170 | expected_type: "Number", 171 | actual_value: item, 172 | }); 173 | } 174 | } 175 | 176 | Ok(values) 177 | } 178 | } 179 | 180 | // region: --- with-sea-query 181 | #[cfg(feature = "with-sea-query")] 182 | mod with_sea_query { 183 | use super::*; 184 | use crate::filter::{sea_is_col_value_null, FilterNodeOptions, SeaResult}; 185 | use crate::into_node_value_expr; 186 | use sea_query::{BinOper, ColumnRef, ConditionExpression, SimpleExpr}; 187 | 188 | macro_rules! impl_into_sea_op_val { 189 | ($($ov:ident),+) => { 190 | $( 191 | impl $ov { 192 | pub fn into_sea_cond_expr(self, col: &ColumnRef, node_options: &FilterNodeOptions) -> SeaResult { 193 | let binary_fn = |op: BinOper, vxpr: SimpleExpr| { 194 | ConditionExpression::SimpleExpr(SimpleExpr::binary(col.clone().into(), op, vxpr)) 195 | }; 196 | let cond = match self { 197 | $ov::Eq(s) => binary_fn(BinOper::Equal, into_node_value_expr(s, node_options)), 198 | $ov::Not(s) => binary_fn(BinOper::NotEqual, into_node_value_expr(s, node_options)), 199 | $ov::In(s) => binary_fn( 200 | BinOper::In, 201 | SimpleExpr::Tuple(s.into_iter().map(|v| into_node_value_expr(v, node_options)).collect()), 202 | ), 203 | $ov::NotIn(s) => binary_fn( 204 | BinOper::NotIn, 205 | SimpleExpr::Tuple(s.into_iter().map(|v| into_node_value_expr(v, node_options)).collect()), 206 | ), 207 | $ov::Lt(s) => binary_fn(BinOper::SmallerThan, into_node_value_expr(s, node_options)), 208 | $ov::Lte(s) => binary_fn(BinOper::SmallerThanOrEqual, into_node_value_expr(s, node_options)), 209 | $ov::Gt(s) => binary_fn(BinOper::GreaterThan, into_node_value_expr(s, node_options)), 210 | $ov::Gte(s) => binary_fn(BinOper::GreaterThanOrEqual, into_node_value_expr(s, node_options)), 211 | 212 | $ov::Null(null) => sea_is_col_value_null(col.clone(), null), 213 | }; 214 | 215 | Ok(cond) 216 | } 217 | } 218 | )+ 219 | }; 220 | } 221 | 222 | impl_into_sea_op_val!(OpValInt64, OpValInt32, OpValFloat64); 223 | } 224 | // endregion: --- with-sea-query 225 | -------------------------------------------------------------------------------- /src/filter/ops/op_val_string.rs: -------------------------------------------------------------------------------- 1 | #![allow(deprecated)] // for now 2 | 3 | use crate::filter::OpVal; 4 | 5 | #[derive(Debug, Clone)] 6 | pub struct OpValsString(pub Vec); 7 | 8 | #[derive(Debug, Clone)] 9 | pub enum OpValString { 10 | Eq(String), 11 | Not(String), 12 | 13 | In(Vec), 14 | NotIn(Vec), 15 | 16 | Lt(String), 17 | Lte(String), 18 | 19 | Gt(String), 20 | Gte(String), 21 | 22 | Contains(String), 23 | NotContains(String), 24 | 25 | ContainsAny(Vec), 26 | NotContainsAny(Vec), 27 | 28 | ContainsAll(Vec), 29 | 30 | StartsWith(String), 31 | NotStartsWith(String), 32 | 33 | StartsWithAny(Vec), 34 | NotStartsWithAny(Vec), 35 | 36 | EndsWith(String), 37 | NotEndsWith(String), 38 | 39 | EndsWithAny(Vec), 40 | NotEndsWithAny(Vec), 41 | 42 | Empty(bool), 43 | Null(bool), 44 | 45 | ContainsCi(String), 46 | NotContainsCi(String), 47 | 48 | StartsWithCi(String), 49 | NotStartsWithCi(String), 50 | 51 | EndsWithCi(String), 52 | NotEndsWithCi(String), 53 | 54 | Ilike(String), 55 | } 56 | 57 | // region: --- Simple value to Eq OpValString 58 | impl From for OpValString { 59 | fn from(val: String) -> Self { 60 | OpValString::Eq(val) 61 | } 62 | } 63 | 64 | impl From<&str> for OpValString { 65 | fn from(val: &str) -> Self { 66 | OpValString::Eq(val.to_string()) 67 | } 68 | } 69 | // endregion: --- Simple value to Eq OpValString 70 | 71 | // region: --- Simple value to Eq OpValStrings 72 | impl From for OpValsString { 73 | fn from(val: String) -> Self { 74 | OpValString::from(val).into() 75 | } 76 | } 77 | 78 | impl From<&str> for OpValsString { 79 | fn from(val: &str) -> Self { 80 | OpValString::from(val).into() 81 | } 82 | } 83 | // endregion: --- Simple value to Eq OpValStrings 84 | 85 | // region: --- StringOpVal to OpVal 86 | impl From for OpVal { 87 | fn from(val: OpValString) -> Self { 88 | OpVal::String(val) 89 | } 90 | } 91 | // endregion: --- StringOpVal to OpVal 92 | 93 | // region: --- Primitive to OpVal::String(StringOpVal::Eq) 94 | impl From for OpVal { 95 | fn from(val: String) -> Self { 96 | OpValString::Eq(val).into() 97 | } 98 | } 99 | 100 | impl From<&str> for OpVal { 101 | fn from(val: &str) -> Self { 102 | OpValString::Eq(val.to_string()).into() 103 | } 104 | } 105 | // endregion: --- Primitive to OpVal::String(StringOpVal::Eq) 106 | 107 | mod json { 108 | use crate::filter::json::OpValueToOpValType; 109 | use crate::filter::OpValString; 110 | use crate::{Error, Result}; 111 | use serde_json::Value; 112 | 113 | impl OpValueToOpValType for OpValString { 114 | fn op_value_to_op_val_type(op: &str, value: Value) -> Result 115 | where 116 | Self: Sized, 117 | { 118 | fn into_strings(value: Value) -> Result> { 119 | let mut values = Vec::new(); 120 | 121 | let Value::Array(array) = value else { 122 | return Err(Error::JsonValArrayWrongType { actual_value: value }); 123 | }; 124 | 125 | for item in array.into_iter() { 126 | if let Value::String(item) = item { 127 | values.push(item); 128 | } else { 129 | return Err(Error::JsonValArrayItemNotOfType { 130 | expected_type: "String", 131 | actual_value: item, 132 | }); 133 | } 134 | } 135 | 136 | Ok(values) 137 | } 138 | 139 | let ov = match (op, value) { 140 | ("$eq", Value::String(string_v)) => OpValString::Eq(string_v), 141 | ("$in", value) => OpValString::In(into_strings(value)?), 142 | 143 | ("$not", Value::String(string_v)) => OpValString::Not(string_v), 144 | ("$notIn", value) => OpValString::NotIn(into_strings(value)?), 145 | 146 | ("$lt", Value::String(string_v)) => OpValString::Lt(string_v), 147 | ("$lte", Value::String(string_v)) => OpValString::Lte(string_v), 148 | 149 | ("$gt", Value::String(string_v)) => OpValString::Gt(string_v), 150 | ("$gte", Value::String(string_v)) => OpValString::Gte(string_v), 151 | 152 | ("$contains", Value::String(string_v)) => OpValString::Contains(string_v), 153 | ("$containsAny", value) => OpValString::ContainsAny(into_strings(value)?), 154 | 155 | ("$containsAll", value) => OpValString::ContainsAll(into_strings(value)?), 156 | 157 | ("$notContains", Value::String(string_v)) => OpValString::NotContains(string_v), 158 | ("$notContainsAny", value) => OpValString::NotContainsAny(into_strings(value)?), 159 | 160 | ("$startsWith", Value::String(string_v)) => OpValString::StartsWith(string_v), 161 | ("$startsWithAny", value) => OpValString::StartsWithAny(into_strings(value)?), 162 | 163 | ("$notStartsWith", Value::String(string_v)) => OpValString::NotStartsWith(string_v), 164 | ("$notStartsWithAny", value) => OpValString::NotStartsWithAny(into_strings(value)?), 165 | 166 | ("$endsWith", Value::String(string_v)) => OpValString::EndsWith(string_v), 167 | ("$endsWithAny", value) => OpValString::EndsWithAny(into_strings(value)?), 168 | 169 | ("$notEndsWith", Value::String(string_v)) => OpValString::NotEndsWith(string_v), 170 | ("$notEndsWithAny", value) => OpValString::NotEndsWithAny(into_strings(value)?), 171 | 172 | ("$empty", Value::Bool(v)) => OpValString::Empty(v), 173 | ("$null", Value::Bool(v)) => OpValString::Null(v), 174 | 175 | ("$containsCi", Value::String(string_v)) => OpValString::ContainsCi(string_v), 176 | ("$notContainsCi", Value::String(string_v)) => OpValString::NotContainsCi(string_v), 177 | 178 | ("$startsWithCi", Value::String(string_v)) => OpValString::StartsWithCi(string_v), 179 | ("$notStartsWithCi", Value::String(string_v)) => OpValString::NotStartsWithCi(string_v), 180 | 181 | ("$endsWithCi", Value::String(string_v)) => OpValString::EndsWithCi(string_v), 182 | ("$notEndsWithCi", Value::String(string_v)) => OpValString::NotEndsWithCi(string_v), 183 | 184 | // Postgres optimized case insensitive like 185 | ("$ilike", Value::String(string_v)) => OpValString::Ilike(string_v), 186 | 187 | (_, v) => { 188 | return Err(Error::JsonOpValNotSupported { 189 | operator: op.to_string(), 190 | value: v, 191 | }) 192 | } 193 | }; 194 | Ok(ov) 195 | } 196 | } 197 | } 198 | 199 | // region: --- with-sea-query 200 | #[cfg(feature = "with-sea-query")] 201 | mod with_sea_query { 202 | use super::*; 203 | use crate::filter::{sea_is_col_value_null, FilterNodeOptions, SeaResult}; 204 | use crate::{into_node_column_expr, into_node_value_expr}; 205 | use sea_query::{BinOper, ColumnRef, Condition, ConditionExpression, Expr, Func, SimpleExpr}; 206 | 207 | #[cfg(feature = "with-ilike")] 208 | use sea_query::extension::postgres::PgBinOper; 209 | 210 | impl OpValString { 211 | pub fn into_sea_cond_expr( 212 | self, 213 | col: &ColumnRef, 214 | node_options: &FilterNodeOptions, 215 | ) -> SeaResult { 216 | let binary_fn = |op: BinOper, v: String| { 217 | let vxpr = into_node_value_expr(v, node_options); 218 | let column = into_node_column_expr(col.clone(), node_options); 219 | ConditionExpression::SimpleExpr(SimpleExpr::binary(column, op, vxpr)) 220 | }; 221 | 222 | #[cfg(feature = "with-ilike")] 223 | let pg_binary_fn = |op: PgBinOper, v: String| { 224 | let vxpr = into_node_value_expr(v, node_options); 225 | let column = into_node_column_expr(col.clone(), node_options); 226 | ConditionExpression::SimpleExpr(SimpleExpr::binary(column.into(), BinOper::PgOperator(op), vxpr)) 227 | }; 228 | 229 | let binaries_fn = |op: BinOper, v: Vec| { 230 | let vxpr_list: Vec = v.into_iter().map(|v| into_node_value_expr(v, node_options)).collect(); 231 | let vxpr = SimpleExpr::Tuple(vxpr_list); 232 | let column = into_node_column_expr(col.clone(), node_options); 233 | ConditionExpression::SimpleExpr(SimpleExpr::binary(column, op, vxpr)) 234 | }; 235 | 236 | let cond_any_of_fn = |op: BinOper, values: Vec, val_prefix: &str, val_suffix: &str| { 237 | let mut cond = Condition::any(); 238 | 239 | for value in values { 240 | let expr = binary_fn(op, format!("{val_prefix}{value}{val_suffix}")); 241 | cond = cond.add(expr); 242 | } 243 | 244 | ConditionExpression::Condition(cond) 245 | }; 246 | 247 | let case_insensitive_fn = |op: BinOper, v: String| { 248 | let vxpr = SimpleExpr::Value(v.into()); 249 | let col_expr = SimpleExpr::FunctionCall(Func::lower(Expr::col(col.clone()))); 250 | let value_expr = SimpleExpr::FunctionCall(Func::lower(vxpr)); 251 | ConditionExpression::SimpleExpr(SimpleExpr::binary(col_expr, op, value_expr)) 252 | }; 253 | 254 | let cond = match self { 255 | OpValString::Eq(s) => binary_fn(BinOper::Equal, s), 256 | OpValString::Not(s) => binary_fn(BinOper::NotEqual, s), 257 | OpValString::In(s) => binaries_fn(BinOper::In, s), 258 | OpValString::NotIn(s) => binaries_fn(BinOper::NotIn, s), 259 | OpValString::Lt(s) => binary_fn(BinOper::SmallerThan, s), 260 | OpValString::Lte(s) => binary_fn(BinOper::SmallerThanOrEqual, s), 261 | OpValString::Gt(s) => binary_fn(BinOper::GreaterThan, s), 262 | OpValString::Gte(s) => binary_fn(BinOper::GreaterThanOrEqual, s), 263 | 264 | OpValString::Contains(s) => binary_fn(BinOper::Like, format!("%{s}%")), 265 | 266 | OpValString::NotContains(s) => binary_fn(BinOper::NotLike, format!("%{s}%")), 267 | 268 | OpValString::ContainsAll(values) => { 269 | let mut cond = Condition::all(); 270 | 271 | for value in values { 272 | let expr = binary_fn(BinOper::Like, format!("%{value}%")); 273 | cond = cond.add(expr); 274 | } 275 | 276 | ConditionExpression::Condition(cond) 277 | } 278 | 279 | OpValString::ContainsAny(values) => cond_any_of_fn(BinOper::Like, values, "%", "%"), 280 | OpValString::NotContainsAny(values) => cond_any_of_fn(BinOper::NotLike, values, "%", "%"), 281 | 282 | OpValString::StartsWith(s) => binary_fn(BinOper::Like, format!("{s}%")), 283 | OpValString::StartsWithAny(values) => cond_any_of_fn(BinOper::Like, values, "", "%"), 284 | 285 | OpValString::NotStartsWith(s) => binary_fn(BinOper::NotLike, format!("{s}%")), 286 | OpValString::NotStartsWithAny(values) => cond_any_of_fn(BinOper::NotLike, values, "", "%"), 287 | 288 | OpValString::EndsWith(s) => binary_fn(BinOper::Like, format!("%{s}")), 289 | OpValString::EndsWithAny(values) => cond_any_of_fn(BinOper::Like, values, "%", ""), 290 | 291 | OpValString::NotEndsWith(s) => binary_fn(BinOper::Like, format!("%{s}")), 292 | OpValString::NotEndsWithAny(values) => cond_any_of_fn(BinOper::NotLike, values, "%", ""), 293 | 294 | OpValString::Null(null) => sea_is_col_value_null(col.clone(), null), 295 | OpValString::Empty(empty) => { 296 | let op = if empty { BinOper::Equal } else { BinOper::NotEqual }; 297 | Condition::any() 298 | .add(sea_is_col_value_null(col.clone(), empty)) 299 | .add(binary_fn(op, "".to_string())) 300 | .into() 301 | } 302 | 303 | OpValString::ContainsCi(s) => case_insensitive_fn(BinOper::Like, format!("%{s}%")), 304 | OpValString::NotContainsCi(s) => case_insensitive_fn(BinOper::NotLike, format!("%{s}%")), 305 | 306 | OpValString::StartsWithCi(s) => case_insensitive_fn(BinOper::Like, format!("{s}%")), 307 | OpValString::NotStartsWithCi(s) => case_insensitive_fn(BinOper::NotLike, format!("{s}%")), 308 | 309 | OpValString::EndsWithCi(s) => case_insensitive_fn(BinOper::Like, format!("%{s}")), 310 | OpValString::NotEndsWithCi(s) => case_insensitive_fn(BinOper::NotLike, format!("%{s}")), 311 | 312 | OpValString::Ilike(s) => { 313 | #[cfg(feature = "with-ilike")] 314 | { 315 | pg_binary_fn(PgBinOper::ILike, format!("%{s}%")) 316 | } 317 | #[cfg(not(feature = "with-ilike"))] 318 | { 319 | case_insensitive_fn(BinOper::Like, format!("%{s}%")) 320 | } 321 | } 322 | }; 323 | 324 | Ok(cond) 325 | } 326 | } 327 | } 328 | // endregion: --- with-sea-query 329 | -------------------------------------------------------------------------------- /src/filter/ops/op_val_value.rs: -------------------------------------------------------------------------------- 1 | use crate::filter::OpVal; 2 | use serde_json::Value; 3 | 4 | #[derive(Debug, Clone)] 5 | pub struct OpValsValue(pub Vec); 6 | 7 | #[derive(Debug, Clone)] 8 | pub enum OpValValue { 9 | Eq(Value), 10 | Not(Value), 11 | 12 | In(Vec), 13 | NotIn(Vec), 14 | 15 | Lt(Value), 16 | Lte(Value), 17 | 18 | Gt(Value), 19 | Gte(Value), 20 | 21 | Null(bool), 22 | } 23 | 24 | // NOTE: We cannot implement the From for OpValValue, OpValsValue, .. 25 | // because it could fail if the json::Value is not a scalar type 26 | 27 | // region: --- OpValValue to OpVal::Value 28 | impl From for OpVal { 29 | fn from(val: OpValValue) -> Self { 30 | OpVal::Value(val) 31 | } 32 | } 33 | // endregion: --- OpValValue to OpVal::Value 34 | 35 | mod json { 36 | use crate::filter::json::OpValueToOpValType; 37 | use crate::filter::OpValValue; 38 | use crate::{Error, Result}; 39 | use serde_json::Value; 40 | 41 | impl OpValueToOpValType for OpValValue { 42 | fn op_value_to_op_val_type(op: &str, value: Value) -> Result 43 | where 44 | Self: Sized, 45 | { 46 | fn into_values(value: Value) -> Result> { 47 | let mut values = Vec::new(); 48 | 49 | let Value::Array(array) = value else { 50 | return Err(Error::JsonValArrayWrongType { actual_value: value }); 51 | }; 52 | 53 | for item in array.into_iter() { 54 | values.push(item) 55 | } 56 | 57 | Ok(values) 58 | } 59 | 60 | let ov = match (op, value) { 61 | ("$eq", v) => OpValValue::Eq(v), 62 | ("$in", value) => OpValValue::NotIn(into_values(value)?), 63 | 64 | ("$not", v) => OpValValue::Not(v), 65 | ("$notIn", value) => OpValValue::NotIn(into_values(value)?), 66 | 67 | ("$lt", v) => OpValValue::Lt(v), 68 | ("$lte", v) => OpValValue::Lte(v), 69 | 70 | ("$gt", v) => OpValValue::Gt(v), 71 | ("$gte", v) => OpValValue::Gte(v), 72 | 73 | ("$null", Value::Bool(v)) => OpValValue::Null(v), 74 | 75 | (_, v) => { 76 | return Err(Error::JsonOpValNotSupported { 77 | operator: op.to_string(), 78 | value: v, 79 | }) 80 | } 81 | }; 82 | Ok(ov) 83 | } 84 | } 85 | } 86 | 87 | // region: --- with-sea-query 88 | #[cfg(feature = "with-sea-query")] 89 | mod with_sea_query { 90 | use super::*; 91 | use crate::filter::{sea_is_col_value_null, FilterNodeOptions, SeaResult, ToSeaValueFnHolder}; 92 | use crate::{into_node_column_expr, into_node_value_expr}; 93 | use sea_query::{BinOper, ColumnRef, ConditionExpression, SimpleExpr}; 94 | 95 | impl OpValValue { 96 | pub fn into_sea_cond_expr_with_json_to_sea( 97 | self, 98 | col: &ColumnRef, 99 | node_options: &FilterNodeOptions, 100 | to_sea_value: &ToSeaValueFnHolder, 101 | ) -> SeaResult { 102 | // -- CondExpr builder for single value 103 | let binary_fn = |op: BinOper, json_value: serde_json::Value| -> SeaResult { 104 | let sea_value = to_sea_value.call(json_value)?; 105 | 106 | let vxpr = into_node_value_expr(sea_value, node_options); 107 | let column = into_node_column_expr(col.clone(), node_options); 108 | Ok(ConditionExpression::SimpleExpr(SimpleExpr::binary(column, op, vxpr))) 109 | }; 110 | 111 | // -- CondExpr builder for single value 112 | let binaries_fn = |op: BinOper, json_values: Vec| -> SeaResult { 113 | // -- Build the list of sea_query::Value 114 | let sea_values: Vec = json_values 115 | .into_iter() 116 | .map(|v| to_sea_value.call(v)) 117 | .collect::>()?; 118 | 119 | // -- Transform to the list of SimpleExpr 120 | let vxpr_list: Vec = 121 | sea_values.into_iter().map(|v| into_node_value_expr(v, node_options)).collect(); 122 | let vxpr = SimpleExpr::Tuple(vxpr_list); 123 | 124 | // -- Return the condition expression 125 | let column = into_node_column_expr(col.clone(), node_options); 126 | Ok(ConditionExpression::SimpleExpr(SimpleExpr::binary(column, op, vxpr))) 127 | }; 128 | 129 | let cond = match self { 130 | OpValValue::Eq(json_value) => binary_fn(BinOper::Equal, json_value)?, 131 | OpValValue::In(json_values) => binaries_fn(BinOper::In, json_values)?, 132 | 133 | OpValValue::Not(json_value) => binary_fn(BinOper::NotEqual, json_value)?, 134 | OpValValue::NotIn(json_value) => binaries_fn(BinOper::NotIn, json_value)?, 135 | 136 | OpValValue::Lt(json_value) => binary_fn(BinOper::SmallerThan, json_value)?, 137 | OpValValue::Lte(json_value) => binary_fn(BinOper::SmallerThanOrEqual, json_value)?, 138 | 139 | OpValValue::Gt(json_value) => binary_fn(BinOper::GreaterThan, json_value)?, 140 | OpValValue::Gte(json_value) => binary_fn(BinOper::GreaterThanOrEqual, json_value)?, 141 | 142 | OpValValue::Null(null) => sea_is_col_value_null(col.clone(), null), 143 | }; 144 | 145 | Ok(cond) 146 | } 147 | } 148 | } 149 | // endregion: --- with-sea-query 150 | -------------------------------------------------------------------------------- /src/includes.rs: -------------------------------------------------------------------------------- 1 | //! PLACEHOLDER for now. Not used yet. 2 | 3 | // region: --- Includes 4 | #[derive(Debug)] 5 | pub struct Includes { 6 | pub value: IncludeValue, 7 | } 8 | 9 | impl Includes { 10 | pub fn new(value: IncludeValue) -> Includes { 11 | Includes { value } 12 | } 13 | } 14 | // endregion: --- Includes 15 | 16 | // region: --- IncludeNode 17 | #[derive(Debug)] 18 | pub struct IncludeNode { 19 | pub name: String, 20 | pub value: IncludeValue, 21 | } 22 | 23 | impl From<(&str, bool)> for IncludeNode { 24 | fn from(val: (&str, bool)) -> Self { 25 | IncludeNode { 26 | name: val.0.to_owned(), 27 | value: IncludeValue::Value(val.1), 28 | } 29 | } 30 | } 31 | 32 | impl From<(String, bool)> for IncludeNode { 33 | fn from(val: (String, bool)) -> Self { 34 | IncludeNode { 35 | name: val.0, 36 | value: IncludeValue::Value(val.1), 37 | } 38 | } 39 | } 40 | // endregion: --- IncludeNode 41 | 42 | // region: --- IncludeValue 43 | #[derive(Debug)] 44 | pub enum IncludeValue { 45 | Value(bool), 46 | Nodes(Vec), 47 | } 48 | // endregion: --- IncludeValue 49 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | // #![allow(unused)] 2 | // --- Sub-Modules 3 | mod error; 4 | #[cfg(feature = "with-rusqlite")] 5 | mod sqlite; 6 | 7 | pub mod field; 8 | pub mod filter; 9 | pub mod includes; 10 | 11 | // --- Re-Exports 12 | pub use crate::error::{Error, Result}; 13 | 14 | #[cfg(feature = "with-sea-query")] 15 | mod sea_utils; 16 | 17 | #[cfg(feature = "with-sea-query")] 18 | pub use sea_utils::*; 19 | 20 | #[cfg(feature = "with-rusqlite")] 21 | pub use sqlite::*; 22 | -------------------------------------------------------------------------------- /src/sea_utils.rs: -------------------------------------------------------------------------------- 1 | use crate::filter::FilterNodeOptions; 2 | use sea_query::{ColumnRef, Iden, IdenStatic, SimpleExpr, Value}; 3 | 4 | /// String sea-query `Iden` wrapper 5 | #[derive(Debug)] 6 | pub struct StringIden(pub String); 7 | 8 | impl Iden for StringIden { 9 | fn unquoted(&self, s: &mut dyn std::fmt::Write) { 10 | // Should never fail, but just in case, we do not crash, just print. 11 | if let Err(err) = s.write_str(&self.0) { 12 | println!("modql StringIden fail write_str. Cause: {err}"); 13 | } 14 | } 15 | } 16 | 17 | /// Static str sea-query `Iden` wrapper 18 | #[derive(Debug, Clone, Copy)] 19 | pub struct SIden(pub &'static str); 20 | 21 | impl Iden for SIden { 22 | fn unquoted(&self, s: &mut dyn std::fmt::Write) { 23 | // Should never fail, but just in case, we do not crash, just print. 24 | if let Err(err) = s.write_str(self.0) { 25 | println!("modql SIden fail write_str. Cause: {err}"); 26 | } 27 | } 28 | } 29 | 30 | impl IdenStatic for SIden { 31 | fn as_str(&self) -> &'static str { 32 | self.0 33 | } 34 | } 35 | 36 | /// Convert a FilterNode value T into a sea-query SimpleExpr as long as T implements Into 37 | pub fn into_node_value_expr(val: T, node_options: &FilterNodeOptions) -> SimpleExpr 38 | where 39 | T: Into, 40 | { 41 | let mut vxpr = SimpleExpr::Value(val.into()); 42 | if let Some(cast_as) = node_options.cast_as.as_ref() { 43 | vxpr = vxpr.cast_as(StringIden(cast_as.to_string())); 44 | } 45 | vxpr 46 | } 47 | 48 | pub fn into_node_column_expr(col: ColumnRef, node_options: &FilterNodeOptions) -> SimpleExpr { 49 | let Some(cast_column_as) = &node_options.cast_column_as else { 50 | // If no cast is needed, wrap the ColumnRef as a SimpleExpr 51 | return SimpleExpr::Column(col); 52 | }; 53 | 54 | SimpleExpr::Column(col).cast_as(StringIden(cast_column_as.to_string())) 55 | } 56 | -------------------------------------------------------------------------------- /src/sqlite/mod.rs: -------------------------------------------------------------------------------- 1 | //! Requires the `with-rusqlite` and `with-sea-query` features 2 | //! and provides a very basic `sqlite::FromRow` based on the `Fields` derivation. 3 | //! 4 | 5 | pub use modql_macros::SqliteFromRow; 6 | pub use modql_macros::SqliteFromValue; 7 | pub use modql_macros::SqliteToValue; 8 | 9 | // -- deprecated 10 | pub use modql_macros::FromSqliteRow; 11 | pub use modql_macros::FromSqliteValue; 12 | pub use modql_macros::ToSqliteValue; 13 | 14 | #[deprecated(note = "use SqliteFromRow")] 15 | pub trait FromSqliteRow: SqliteFromRow 16 | where 17 | Self: Sized, 18 | { 19 | } 20 | 21 | pub trait SqliteFromRow 22 | where 23 | Self: Sized, 24 | { 25 | #[deprecated(note = "use sqlite_from_row")] 26 | fn from_sqlite_row(row: &rusqlite::Row<'_>) -> rusqlite::Result { 27 | Self::sqlite_from_row(row) 28 | } 29 | 30 | fn sqlite_from_row(row: &rusqlite::Row<'_>) -> rusqlite::Result; 31 | 32 | fn sqlite_from_row_partial(row: &rusqlite::Row<'_>, prop_names: &[&str]) -> rusqlite::Result; 33 | } 34 | -------------------------------------------------------------------------------- /tests/support/mod.rs: -------------------------------------------------------------------------------- 1 | pub type Result = core::result::Result; 2 | pub type Error = Box; // For early dev. 3 | 4 | #[cfg(feature = "with-rusqlite")] 5 | pub mod sqlite; 6 | -------------------------------------------------------------------------------- /tests/support/sqlite.rs: -------------------------------------------------------------------------------- 1 | use super::Result; 2 | use modql::SqliteFromRow; 3 | use rusqlite::{Connection, Params}; 4 | 5 | pub fn create_test_schema(conn: &Connection) -> Result<()> { 6 | conn.execute( 7 | "CREATE TABLE IF NOT EXISTS agent ( 8 | id INTEGER PRIMARY KEY AUTOINCREMENT, 9 | name TEXT, 10 | model TEXT, 11 | lvl INTEGER, 12 | module_id INTEGER, 13 | data_t TEXT, 14 | data_b BLOB 15 | ) STRICT", 16 | (), // empty list of parameters. 17 | )?; 18 | 19 | conn.execute( 20 | "CREATE TABLE IF NOT EXISTS module ( 21 | id INTEGER PRIMARY KEY AUTOINCREMENT, 22 | name TEXT 23 | ) STRICT", 24 | (), // empty list of parameters. 25 | )?; 26 | 27 | Ok(()) 28 | } 29 | 30 | // region: --- Seeders 31 | 32 | pub fn seed_agent(conn: &Connection, name: &str, model_id: Option) -> Result { 33 | let id = insert_with_returnning_id( 34 | conn, 35 | "INSERT INTO agent (name, lvl, module_id) VALUES (?, ?, ?) RETURNING id", 36 | (name, &123, &model_id), 37 | )?; 38 | 39 | Ok(id) 40 | } 41 | 42 | pub fn seed_module(conn: &Connection, name: &str) -> Result { 43 | let id = insert_with_returnning_id(conn, "INSERT INTO module (name) VALUES (?) RETURNING id", [name])?; 44 | 45 | Ok(id) 46 | } 47 | 48 | // endregion: --- Seeders 49 | 50 | // region: --- Query Helpers 51 | 52 | pub fn insert_with_returnning_id(conn: &Connection, insert_sql: &str, value_params: P) -> Result { 53 | let mut stmt = conn.prepare(insert_sql)?; 54 | let id = stmt.query_row(value_params, |r| r.get::<_, i64>(0))?; 55 | 56 | Ok(id) 57 | } 58 | 59 | #[allow(unused)] 60 | pub fn exec_select(conn: &Connection, sql: &str) -> Result> { 61 | let mut stmt = conn.prepare(sql)?; 62 | let iter = stmt.query_and_then([], |r| T::sqlite_from_row(r))?; 63 | let items = iter.collect::, _>>()?; 64 | 65 | Ok(items) 66 | } 67 | 68 | // endregion: --- Query Helpers 69 | -------------------------------------------------------------------------------- /tests/test_expand_fields.rs: -------------------------------------------------------------------------------- 1 | pub type Result = core::result::Result; 2 | pub type Error = Box; // For early dev. 3 | use modql::field::{Fields, HasFields}; 4 | 5 | #[derive(Debug, Default, Fields)] 6 | #[modql(rel = "todo_table")] 7 | pub struct Todo { 8 | pub id: i64, 9 | 10 | #[field(rel = "special_todo_table", name = "special_title_col")] 11 | pub title: String, 12 | 13 | #[field(name = "description")] 14 | pub desc: Option, 15 | 16 | #[field(skip)] 17 | pub other: Option, 18 | } 19 | 20 | #[test] 21 | fn test_struct_field_names() -> Result<()> { 22 | assert_eq!(Todo::field_names(), &["id", "special_title_col", "description"]); 23 | Ok(()) 24 | } 25 | 26 | #[test] 27 | fn test_struct_field_metas() -> Result<()> { 28 | // -- Exec 29 | let field_refs = Todo::field_metas(); 30 | 31 | // -- Check 32 | let names: Vec<&'static str> = field_refs.iter().map(|&meta| meta.name()).collect(); 33 | let rels: Vec> = field_refs.iter().map(|fr| fr.rel).collect(); 34 | assert_eq!(names, &["id", "special_title_col", "description"]); 35 | assert_eq!( 36 | rels, 37 | &[Some("todo_table"), Some("special_todo_table"), Some("todo_table")] 38 | ); 39 | 40 | Ok(()) 41 | } 42 | -------------------------------------------------------------------------------- /tests/test_expand_filter_nodes.rs: -------------------------------------------------------------------------------- 1 | #![cfg(feature = "with-sea-query")] 2 | //! TODO: Add more tests 3 | 4 | pub type Result = core::result::Result; 5 | pub type Error = Box; // For early dev. 6 | use modql::filter::{FilterNodes, OpValsInt64, OpValsString}; 7 | use modql::SIden; 8 | use sea_query::{Query, SqliteQueryBuilder}; 9 | 10 | #[derive(Clone, FilterNodes, Default)] 11 | pub struct ProjectFilter { 12 | id: Option, 13 | name: Option, 14 | #[modql(rel = "foo_rel")] 15 | label: Option, 16 | } 17 | 18 | #[derive(Clone, FilterNodes, Default)] 19 | #[modql(rel = "task_tbl")] 20 | pub struct TaskFilter { 21 | id: Option, 22 | #[modql(cast_column_as = "text")] 23 | title: Option, 24 | #[modql(rel = "foo_rel")] 25 | label: Option, 26 | } 27 | 28 | #[test] 29 | fn test_expand_filter_nodes_filter_rel() -> Result<()> { 30 | // -- Setup & Fixtures 31 | let filter = TaskFilter { 32 | id: Some(123.into()), 33 | title: Some("some title".into()), 34 | label: Some("Test".into()), 35 | }; 36 | 37 | // -- Exec 38 | let cond: Result = filter.try_into(); 39 | let cond = cond?; 40 | 41 | let mut query = Query::select(); 42 | query.from(SIden("task")).cond_where(cond); 43 | let (sql, _) = query.build(SqliteQueryBuilder); 44 | // Note: No columns, but that's ok for this test for now. 45 | 46 | // -- Check 47 | assert!( 48 | sql.contains(r#"WHERE "task_tbl"."id" = ? AND CAST("task_tbl"."title" AS text) = ? AND "foo_rel"."label" = ?"#), 49 | "Incorrect where statment" 50 | ); 51 | 52 | Ok(()) 53 | } 54 | 55 | #[test] 56 | fn test_expand_filter_nodes_simple() -> Result<()> { 57 | // -- Setup & Fixtures 58 | let filter = ProjectFilter { 59 | id: Some(123.into()), 60 | label: Some("Test".into()), 61 | ..Default::default() 62 | }; 63 | 64 | // -- Exec 65 | let cond: Result = filter.try_into(); 66 | let cond = cond?; 67 | 68 | let mut query = Query::select(); 69 | query.from(SIden("project")).cond_where(cond); 70 | let (sql, _) = query.build(SqliteQueryBuilder); 71 | // Note: No columns, but that's ok for this test for now. 72 | 73 | // -- Check 74 | assert!( 75 | sql.contains(r#"WHERE "id" = ? AND "foo_rel"."label" = ?"#), 76 | "Incorrect where statment" 77 | ); 78 | 79 | Ok(()) 80 | } 81 | -------------------------------------------------------------------------------- /tests/test_expand_filter_to_sea_condition.rs: -------------------------------------------------------------------------------- 1 | #![cfg(feature = "with-sea-query")] 2 | 3 | //! Should compile. No test functions yet. 4 | pub type Result = core::result::Result; 5 | pub type Error = Box; // For early dev. 6 | use modql::filter::{FilterNodes, OpValValue, OpValsInt64, OpValsString, OpValsValue, SeaResult}; 7 | use sea_query::{BinOper, ColumnRef, ConditionExpression, SimpleExpr, Value}; 8 | 9 | #[derive(FilterNodes, Default)] 10 | pub struct ProjectFilter { 11 | id: Option, 12 | name: Option, 13 | 14 | #[modql(to_sea_condition_fn = "my_to_sea_condition")] 15 | ctime: Option, 16 | } 17 | 18 | fn my_to_sea_condition(col: &ColumnRef, op_val_value: OpValValue) -> SeaResult { 19 | let binary_fn = |op: BinOper, v: serde_json::Value| { 20 | let v: i32 = serde_json::from_value(v).unwrap(); 21 | let vexpr: SimpleExpr = Value::from(v).into(); 22 | let expr = SimpleExpr::binary(col.clone().into(), op, vexpr); 23 | SeaResult::Ok(ConditionExpression::SimpleExpr(expr)) 24 | }; 25 | 26 | match op_val_value { 27 | OpValValue::Eq(v) => binary_fn(BinOper::Equal, v), 28 | OpValValue::Not(_) => todo!(), 29 | OpValValue::In(_) => todo!(), 30 | OpValValue::NotIn(_) => todo!(), 31 | OpValValue::Lt(_) => todo!(), 32 | OpValValue::Lte(_) => todo!(), 33 | OpValValue::Gt(_) => todo!(), 34 | OpValValue::Gte(_) => todo!(), 35 | OpValValue::Null(_) => todo!(), 36 | } 37 | } 38 | 39 | #[test] 40 | fn test_expand_filter_nodes() -> Result<()> { 41 | let _filter = ProjectFilter { 42 | id: Some(123.into()), 43 | ctime: Some(OpValValue::Eq(serde_json::Value::from("some-date")).into()), 44 | ..Default::default() 45 | }; 46 | 47 | Ok(()) 48 | } 49 | -------------------------------------------------------------------------------- /tests/test_expand_names_as_consts.rs: -------------------------------------------------------------------------------- 1 | pub type Result = core::result::Result; 2 | pub type Error = Box; // For early dev. 3 | use modql::field::Fields; 4 | 5 | #[derive(Debug, Default, Fields)] 6 | #[modql(rel = "todo_table", names_as_consts)] 7 | pub struct Todo { 8 | pub id: i64, 9 | 10 | #[field(rel = "special_todo_table", name = "special_title_col")] 11 | pub title: String, 12 | 13 | #[field(name = "description")] 14 | pub desc: Option, 15 | 16 | #[field(skip)] 17 | pub other: Option, 18 | } 19 | 20 | #[derive(Debug, Default, Fields)] 21 | #[modql(names_as_consts = "COL_")] 22 | pub struct Project { 23 | pub id: i64, 24 | 25 | #[field(name = "pname")] 26 | pub name: Option, 27 | } 28 | 29 | #[derive(Debug, Default, Fields)] 30 | #[modql(names_as_consts = "COL")] 31 | pub struct Label { 32 | pub id: i64, 33 | 34 | pub name: Option, 35 | } 36 | 37 | #[test] 38 | fn test_struct_const_names_no_prefix() -> Result<()> { 39 | assert_eq!(Todo::ID, "id"); 40 | assert_eq!(Todo::TITLE, "special_title_col"); 41 | 42 | Ok(()) 43 | } 44 | 45 | #[test] 46 | fn test_struct_const_names_full_prefix() -> Result<()> { 47 | assert_eq!(Project::COL_ID, "id"); 48 | assert_eq!(Project::COL_NAME, "pname"); 49 | 50 | Ok(()) 51 | } 52 | 53 | #[test] 54 | fn test_struct_const_names_simple_prefix() -> Result<()> { 55 | assert_eq!(Label::COL_ID, "id"); 56 | assert_eq!(Label::COL_NAME, "name"); 57 | 58 | Ok(()) 59 | } 60 | -------------------------------------------------------------------------------- /tests/test_expand_sea_fields.rs: -------------------------------------------------------------------------------- 1 | #![cfg(feature = "with-sea-query")] 2 | 3 | pub type Result = core::result::Result; 4 | pub type Error = Box; // For early dev. 5 | use modql::field::{Fields, HasSeaFields}; 6 | 7 | #[derive(Debug, Default, Fields)] 8 | pub struct Todo { 9 | pub id: i64, 10 | 11 | #[field(rel = "special_todo_table", name = "special_title_col")] 12 | pub title: String, 13 | 14 | #[field(name = "description")] 15 | pub desc: Option, 16 | 17 | #[field(skip)] 18 | pub other: Option, 19 | } 20 | 21 | #[test] 22 | fn test_struct_field_names() -> Result<()> { 23 | // -- Exec 24 | let sea_idens = Todo::sea_idens(); 25 | 26 | // -- Check 27 | let names = sea_idens.iter().map(|i| i.to_string()).collect::>(); 28 | let names = names.iter().map(|s| s.as_str()).collect::>(); 29 | assert_eq!(names, &["id", "special_title_col", "description"]); 30 | 31 | Ok(()) 32 | } 33 | -------------------------------------------------------------------------------- /tests/test_filter_node.rs: -------------------------------------------------------------------------------- 1 | #![allow(unused)] // For early development. 2 | #![cfg(feature = "with-sea-query")] 3 | 4 | use modql::filter::{ 5 | FilterNode, FilterNodeOptions, IntoSeaError, OpValInt32, OpValValue, SeaResult, ToSeaConditionFnHolder, 6 | }; 7 | use sea_query::{ColumnRef, ConditionExpression}; 8 | use std::sync::Arc; 9 | 10 | #[test] 11 | fn test_filter_node_with_sea_condition() { 12 | let special_to_sea_cond = ToSeaConditionFnHolder::new(special_to_sea_condition); // This should implement IntoSeaCondition 13 | 14 | let node = FilterNode { 15 | rel: None, 16 | name: "some_name".to_string(), 17 | opvals: vec![123.into()], 18 | options: FilterNodeOptions::default(), 19 | for_sea_condition: Some(special_to_sea_cond.into()), 20 | }; 21 | } 22 | 23 | pub fn special_to_sea_condition(col: &ColumnRef, op_val: OpValValue) -> SeaResult { 24 | todo!() 25 | } 26 | -------------------------------------------------------------------------------- /tests/test_impl_filter_nodes.rs: -------------------------------------------------------------------------------- 1 | //! Should compile. No test functions yet. 2 | 3 | use modql::filter::{FilterNode, IntoFilterNodes, OpVal, OpValInt64, OpValString, OpValsString}; 4 | 5 | pub struct ProjectFilter { 6 | id: Option>, 7 | name: Option>, 8 | } 9 | 10 | impl IntoFilterNodes for ProjectFilter { 11 | fn filter_nodes(self, rel: Option) -> Vec { 12 | let mut nodes = Vec::new(); 13 | 14 | if let Some(id) = self.id { 15 | let node = FilterNode::new_with_rel( 16 | rel.clone(), 17 | "id".to_string(), 18 | id.into_iter().map(|n| n.into()).collect::>(), 19 | ); 20 | nodes.push(node) 21 | } 22 | 23 | if let Some(name) = self.name { 24 | let node = FilterNode::new_with_rel( 25 | rel, 26 | "name".to_string(), 27 | name.into_iter().map(|n| n.into()).collect::>(), 28 | ); 29 | nodes.push(node) 30 | } 31 | 32 | nodes 33 | } 34 | } 35 | 36 | #[allow(unused)] 37 | pub struct TaskFilter { 38 | project: Option, 39 | title: Option, 40 | kind: Option, 41 | } 42 | 43 | impl IntoFilterNodes for TaskFilter { 44 | fn filter_nodes(self, rel: Option) -> Vec { 45 | let mut nodes = Vec::new(); 46 | 47 | if let Some(title) = self.title { 48 | let node = FilterNode::new_with_rel( 49 | rel, 50 | "title".to_string(), 51 | title.0.into_iter().map(|n| n.into()).collect::>(), 52 | ); 53 | nodes.push(node) 54 | } 55 | 56 | nodes 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /tests/test_json_filters.rs: -------------------------------------------------------------------------------- 1 | #![cfg(feature = "with-sea-query")] 2 | #![allow(unused)] // Ok for those tests. 3 | 4 | pub type Result = core::result::Result; 5 | pub type Error = Box; // For early dev. 6 | use modql::filter::{FilterGroups, FilterNode, IntoFilterNodes, OpValsBool, OpValsInt64, OpValsString}; 7 | use modql::SIden; 8 | use modql_macros::FilterNodes; 9 | use sea_query::{Condition, PostgresQueryBuilder, Query}; 10 | use serde::{Deserialize, Serialize}; 11 | use serde_json::{from_value, json}; 12 | use serde_with::{serde_as, OneOrMany}; 13 | 14 | #[derive(FilterNodes, Deserialize, Default, Debug)] 15 | pub struct TaskFilter { 16 | id: Option, 17 | title: Option, 18 | bool: Option, 19 | } 20 | 21 | #[serde_as] 22 | #[derive(Deserialize, Debug)] 23 | struct TaskListParams { 24 | #[serde_as(deserialize_as = "Option>")] 25 | filters: Option>, 26 | } 27 | 28 | #[test] 29 | fn test_json_filters_main() -> Result<()> { 30 | // let params = json!({ 31 | // "filters": [{ 32 | // "id": {"$gt": 123}, 33 | // "title": {"$contains": "World"} 34 | // }, 35 | // { 36 | // "title": {"$startsWith": "Hello"} 37 | // }] 38 | // }); 39 | 40 | let params = json!({ 41 | "filters": { 42 | // "title": {"$contains": "World"}, 43 | "title": {"$in": ["123", "124"]} 44 | } 45 | }); 46 | 47 | let params: TaskListParams = from_value(params)?; 48 | 49 | let filters = params.filters.unwrap(); 50 | 51 | let fg: FilterGroups = filters.into(); 52 | 53 | let cond: Condition = fg.into_sea_condition()?; 54 | 55 | let mut query = Query::select(); 56 | query.from(SIden("task")); 57 | query.cond_where(cond); 58 | 59 | let (sql, values) = query.build(PostgresQueryBuilder); 60 | // Note: for now, just check that all compiles and no runtime errors. 61 | 62 | Ok(()) 63 | } 64 | -------------------------------------------------------------------------------- /tests/test_readme.rs: -------------------------------------------------------------------------------- 1 | // #![allow(unused)] 2 | pub type Result = core::result::Result; 3 | pub type Error = Box; // For early dev. 4 | 5 | use modql::filter::{FilterGroups, FilterNode, FilterNodes, OpValBool, OpValString, OpValsBool, OpValsString}; 6 | 7 | #[test] 8 | fn test_readme_01() -> Result<()> { 9 | let filter_nodes: Vec = vec![ 10 | ( 11 | "title", 12 | OpValString::ContainsAny(vec!["Hello".to_string(), "welcome".to_string()]), 13 | ) 14 | .into(), 15 | ("done", true).into(), 16 | ]; 17 | let filter_groups: FilterGroups = filter_nodes.into(); 18 | 19 | println!("filter_groups:\n{filter_groups:?}"); 20 | 21 | Ok(()) 22 | } 23 | 24 | #[test] 25 | fn test_readme_02() -> Result<()> { 26 | #[derive(FilterNodes)] 27 | struct MyFilter { 28 | done: Option, 29 | name: Option, 30 | } 31 | 32 | let filter = MyFilter { 33 | done: Some(OpValBool::Eq(true).into()), 34 | name: Some( 35 | vec![ 36 | OpValString::Contains("Hello".to_string()), 37 | OpValString::Contains("welcome".to_string()), 38 | ] 39 | .into(), 40 | ), 41 | }; 42 | 43 | let filter_groups: FilterGroups = filter.into(); 44 | 45 | println!("filter_groups:\n{filter_groups:?}"); 46 | 47 | Ok(()) 48 | } 49 | -------------------------------------------------------------------------------- /tests/test_rusqlite_derives.rs: -------------------------------------------------------------------------------- 1 | #![cfg(feature = "with-rusqlite")] 2 | #![allow(unused)] 3 | 4 | mod support; 5 | 6 | pub type Result = core::result::Result; 7 | pub type Error = Box; // For early dev. 8 | 9 | use modql::field::Fields; 10 | use modql::{SqliteFromValue, SqliteToValue}; 11 | use rusqlite::Connection; 12 | 13 | /// Simple enum with From/To SqliteValue 14 | #[derive(SqliteFromValue, SqliteToValue)] 15 | pub enum DItemKind { 16 | Md, 17 | Pdf, 18 | Unknown, 19 | } 20 | 21 | /// Simple tuple struct with From/To SqliteValue 22 | #[derive(SqliteFromValue, SqliteToValue)] 23 | pub struct SimpleId(i64); 24 | 25 | #[derive(Debug, Clone, Fields)] 26 | pub struct Agent { 27 | pub id: i64, 28 | pub name: Option, 29 | pub level: Option, 30 | pub module_id: Option, 31 | } 32 | 33 | #[test] 34 | fn test_rust_sqlite_derives() -> Result<()> { 35 | // -- Setup & Fixtures 36 | let kind = DItemKind::Md; 37 | let sid = SimpleId(123); 38 | 39 | // For now just making sure it compiles above. 40 | // Later can do the exec in sqlite and check 41 | 42 | Ok(()) 43 | } 44 | -------------------------------------------------------------------------------- /tests/test_rusqlite_join.rs: -------------------------------------------------------------------------------- 1 | #![cfg(feature = "with-rusqlite")] 2 | #![allow(clippy::redundant_closure)] 3 | 4 | mod support; 5 | 6 | pub type Result = core::result::Result; 7 | pub type Error = Box; // For early dev. 8 | 9 | use crate::support::sqlite::{seed_agent, seed_module}; 10 | use modql::field::{Fields, HasFields}; 11 | use modql::SqliteFromRow; 12 | use rusqlite::Connection; 13 | use std::result; 14 | 15 | #[derive(Debug, Clone, Fields, SqliteFromRow)] 16 | #[modql(rel = "agent")] 17 | pub struct Agent { 18 | id: i64, 19 | name: Option, 20 | model: Option, 21 | #[field(name = "lvl")] 22 | level: Option, 23 | module_id: Option, 24 | #[field(rel = "module", name = "name")] 25 | module_name: Option, 26 | } 27 | 28 | #[test] 29 | fn test_sqlite_select_join_full() -> Result<()> { 30 | // -- Setup & Fixtures 31 | let conn = Connection::open_in_memory()?; 32 | support::sqlite::create_test_schema(&conn)?; 33 | let fx_module_name = "test-module-A"; 34 | let fx_agent_name = "test-agent-01"; 35 | let module_id = seed_module(&conn, fx_module_name)?; 36 | let _agent_id = seed_agent(&conn, fx_agent_name, Some(module_id))?; 37 | 38 | // -- Build the Sql 39 | let cols = Agent::field_metas().sql_col_refs(); 40 | let sql = format!( 41 | r#" 42 | SELECT {cols} FROM agent 43 | LEFT JOIN module ON agent.module_id = module.id; 44 | "# 45 | ); 46 | 47 | // -- Excute Query 48 | let mut stmt = conn.prepare(&sql)?; 49 | let iter = stmt.query_and_then([], |r| Agent::sqlite_from_row(r))?; 50 | let agents: Vec = iter.collect::>()?; 51 | let agent = agents.first().ok_or("Should have one agent")?; 52 | 53 | // -- Check result 54 | assert_eq!(agent.name.as_deref(), Some(fx_agent_name)); 55 | assert_eq!(agent.level, Some(123)); 56 | assert_eq!(agent.module_name.as_deref(), Some(fx_module_name)); 57 | 58 | Ok(()) 59 | } 60 | 61 | #[test] 62 | fn test_sqlite_select_join_partial() -> Result<()> { 63 | // -- Setup & Fixtures 64 | let conn = Connection::open_in_memory()?; 65 | support::sqlite::create_test_schema(&conn)?; 66 | let fx_module_name = "test-module-A"; 67 | let fx_agent_name = "test-agent-01"; 68 | let module_id = seed_module(&conn, fx_module_name)?; 69 | let _agent_id = seed_agent(&conn, fx_agent_name, Some(module_id))?; 70 | 71 | let prop_names = &["id", "name", "module_id", "module_name"]; 72 | 73 | // -- Build the Sql 74 | let cols = Agent::field_metas().sql_col_refs_for(prop_names); 75 | let sql = format!( 76 | r#" 77 | SELECT {cols} FROM agent 78 | LEFT JOIN module ON agent.module_id = module.id; 79 | "# 80 | ); 81 | 82 | // -- Excute Query 83 | let mut stmt = conn.prepare(&sql)?; 84 | let iter = stmt.query_and_then([], |r| Agent::sqlite_from_row_partial(r, prop_names))?; 85 | let agents: Vec = iter.collect::>()?; 86 | let agent = agents.first().ok_or("Should have one agent")?; 87 | 88 | // -- Check result 89 | assert_eq!(agent.name.as_deref(), Some(fx_agent_name)); 90 | assert_eq!(agent.level, None); // because not requested 91 | assert_eq!(agent.model, None); // because not requested 92 | assert_eq!(agent.module_name.as_deref(), Some(fx_module_name)); 93 | 94 | Ok(()) 95 | } 96 | -------------------------------------------------------------------------------- /tests/test_rusqlite_sea_query.rs: -------------------------------------------------------------------------------- 1 | #![cfg(all(feature = "with-rusqlite", feature = "with-sea-query"))] 2 | #![allow(clippy::redundant_closure)] 3 | 4 | mod support; 5 | 6 | pub type Result = core::result::Result; 7 | pub type Error = Box; // For early dev. 8 | 9 | use crate::support::sqlite::{exec_select, insert_with_returnning_id, seed_agent, seed_module}; 10 | use modql::field::{Fields, HasFields, HasSeaFields}; 11 | use modql::{SIden, SqliteFromRow}; 12 | use rusqlite::Connection; 13 | use sea_query::{Query, SqliteQueryBuilder}; 14 | use sea_query_rusqlite::RusqliteBinder; 15 | 16 | #[derive(Debug, Clone, Fields, SqliteFromRow)] 17 | pub struct Agent { 18 | id: i64, 19 | name: Option, 20 | model: Option, 21 | #[field(name = "lvl")] 22 | level: Option, 23 | module_id: Option, 24 | // #[field(rel = "module", name = "name")] 25 | // module_name: Option, 26 | } 27 | 28 | #[derive(Debug, Clone, Fields)] 29 | pub struct AgentForCreate { 30 | name: Option, 31 | model: Option, 32 | #[field(name = "lvl")] 33 | level: Option, 34 | module_id: Option, 35 | } 36 | 37 | #[test] 38 | fn test_sea_select() -> Result<()> { 39 | // -- Setup & Fixtures 40 | let conn = Connection::open_in_memory()?; 41 | support::sqlite::create_test_schema(&conn)?; 42 | let module_id = seed_module(&conn, "test-module-A")?; 43 | let _agent_id = seed_agent(&conn, "test-agent-01", Some(module_id))?; 44 | 45 | // -- Build sea select 46 | let mut query = Query::select(); 47 | query.from(SIden("agent")); 48 | let metas = Agent::field_metas(); 49 | for &meta in metas.iter() { 50 | meta.sea_apply_select_column(&mut query); 51 | } 52 | 53 | // -- Exec sea-query 54 | let (sql, values) = query.build_rusqlite(SqliteQueryBuilder); 55 | let mut stmt = conn.prepare(&sql)?; 56 | let iter = stmt.query_and_then(&*values.as_params(), |r| Agent::sqlite_from_row(r))?; 57 | let agents = iter.collect::, _>>()?; 58 | 59 | // -- Check result 60 | let agent = agents.first().ok_or("Should have one agent")?; 61 | assert_eq!(agent.name.as_deref(), Some("test-agent-01")); 62 | assert_eq!(agent.level, Some(123)); 63 | 64 | Ok(()) 65 | } 66 | 67 | #[test] 68 | fn test_sea_insert_and_raw_select() -> Result<()> { 69 | // -- Setup & Fixtures 70 | let conn = Connection::open_in_memory()?; 71 | support::sqlite::create_test_schema(&conn)?; 72 | let fx_agent_name = "test_insert_and_select AGENT"; 73 | let fx_level = 234; 74 | 75 | // -- Insert 76 | let agent_c = AgentForCreate { 77 | name: Some(fx_agent_name.to_string()), 78 | model: None, 79 | level: Some(fx_level), 80 | module_id: None, 81 | }; 82 | let fields = agent_c.not_none_sea_fields(); 83 | let (columns, sea_values) = fields.for_sea_insert(); 84 | let mut query = Query::insert(); 85 | query.into_table(SIden("agent")).columns(columns).values(sea_values)?; 86 | query.returning_col(SIden("id")); 87 | let (sql, values) = query.build_rusqlite(SqliteQueryBuilder); 88 | let _agent_id = insert_with_returnning_id(&conn, &sql, &*values.as_params())?; 89 | 90 | // -- Build & execute raw select 91 | let metas = Agent::field_metas(); 92 | let cols = metas.iter().map(|meta| meta.sql_col_ref()).collect::>(); 93 | let cols = cols.join(", "); 94 | let sql = format!("SELECT {cols} FROM agent"); 95 | 96 | // -- Excute Query (without sea-query) 97 | let agents: Vec = exec_select(&conn, &sql)?; 98 | let agent = agents.first().ok_or("Should have one agent")?; 99 | 100 | // -- Check result 101 | assert_eq!(agent.name.as_deref(), Some(fx_agent_name)); 102 | assert_eq!(agent.level, Some(fx_level)); 103 | 104 | Ok(()) 105 | } 106 | -------------------------------------------------------------------------------- /tests/test_rusqlite_simple.rs: -------------------------------------------------------------------------------- 1 | #![cfg(feature = "with-rusqlite")] 2 | #![allow(clippy::redundant_closure)] 3 | 4 | mod support; 5 | 6 | pub type Result = core::result::Result; 7 | pub type Error = Box; // For early dev. 8 | 9 | use crate::support::sqlite::{seed_agent, seed_module}; 10 | use modql::field::{Fields, HasFields}; 11 | use modql::{SqliteFromRow, SqliteFromValue, SqliteToValue}; 12 | use rusqlite::Connection; 13 | use std::result; 14 | 15 | #[derive(Debug, Clone, SqliteFromValue, SqliteToValue, modql::field::SeaFieldValue)] 16 | pub struct Id(i64); 17 | 18 | impl Id { 19 | pub fn as_i64(&self) -> i64 { 20 | self.0 21 | } 22 | } 23 | 24 | // from &i64 25 | impl From<&i64> for Id { 26 | fn from(val: &i64) -> Id { 27 | Id(*val) 28 | } 29 | } 30 | 31 | #[derive(Debug, Clone, Fields, SqliteFromRow)] 32 | pub struct Agent { 33 | id: Id, 34 | name: Option, 35 | model: Option, 36 | #[field(name = "lvl")] 37 | level: Option, 38 | module_id: Option, 39 | // #[field(rel = "module", name = "name")] 40 | // module_name: Option, 41 | } 42 | 43 | #[test] 44 | fn test_sqlite_select_simple() -> Result<()> { 45 | // -- Setup & Fixtures 46 | let conn = Connection::open_in_memory()?; 47 | support::sqlite::create_test_schema(&conn)?; 48 | let module_id = seed_module(&conn, "test-module-A")?; 49 | let _agent_id = seed_agent(&conn, "test-agent-01", Some(module_id))?; 50 | 51 | // -- Build the Sql 52 | let metas = Agent::field_metas(); 53 | let cols = metas.iter().map(|meta| meta.sql_col_ref()).collect::>(); 54 | let cols = cols.join(", "); 55 | let sql = format!("SELECT {cols} FROM agent"); 56 | 57 | // -- Excute Query 58 | let mut stmt = conn.prepare(&sql)?; 59 | let iter = stmt.query_and_then([], |r| Agent::sqlite_from_row(r))?; 60 | let agents: Vec = iter.collect::>()?; 61 | let agent = agents.first().ok_or("Should have one agent")?; 62 | 63 | // -- Check result 64 | assert_eq!(agent.name.as_deref(), Some("test-agent-01")); 65 | assert_eq!(agent.level, Some(123)); 66 | 67 | Ok(()) 68 | } 69 | 70 | #[test] 71 | fn test_sqlite_select_partial() -> Result<()> { 72 | // -- Setup & Fixtures 73 | let conn = Connection::open_in_memory()?; 74 | support::sqlite::create_test_schema(&conn)?; 75 | let module_id = seed_module(&conn, "test-module-A")?; 76 | let _agent_id = seed_agent(&conn, "test-agent-01", Some(module_id))?; 77 | 78 | let only_props = &["id", "name"]; 79 | 80 | // -- Build the Sql 81 | // note: Here we could use filed_metas().sql_col_refs_for(prop_names) 82 | let metas = Agent::field_metas(); 83 | let cols = metas 84 | .iter() 85 | .filter(|m| only_props.contains(&m.prop_name)) 86 | .map(|meta| meta.sql_col_ref()) 87 | .collect::>(); 88 | let cols = cols.join(", "); 89 | let sql = format!("SELECT {cols} FROM agent"); 90 | 91 | // -- Excute Query 92 | let mut stmt = conn.prepare(&sql)?; 93 | let iter = stmt.query_and_then([], |r| Agent::sqlite_from_row_partial(r, &["id", "name"]))?; 94 | let agents: Vec = iter.collect::>()?; 95 | let agent = agents.first().ok_or("Should have one agent")?; 96 | 97 | // -- Check result 98 | assert_eq!(agent.name.as_deref(), Some("test-agent-01")); 99 | assert_eq!(agent.level, None); // because we did not get it 100 | 101 | Ok(()) 102 | } 103 | -------------------------------------------------------------------------------- /tests/test_serde_des.rs: -------------------------------------------------------------------------------- 1 | pub type Result = core::result::Result; 2 | pub type Error = Box; // For early dev. 3 | use modql::filter::{FilterNodes, IntoFilterNodes, OpValsInt64, OpValsString}; 4 | use serde::Deserialize; 5 | use serde_json::Value; 6 | 7 | #[derive(Deserialize, Debug, FilterNodes)] 8 | struct MyFilter { 9 | id: Option, 10 | name: Option, 11 | } 12 | 13 | #[test] 14 | fn test_des_string_simple() -> Result<()> { 15 | let json = r#" 16 | { 17 | "name": "Hello" 18 | } 19 | "# 20 | .to_string(); 21 | 22 | let json: Value = serde_json::from_str(&json)?; 23 | let my_filter: MyFilter = serde_json::from_value(json)?; 24 | 25 | assert!(format!("{my_filter:?}").contains("id: None, name: Some(OpValsString([Eq(\"Hello\")]))")); 26 | 27 | Ok(()) 28 | } 29 | 30 | #[test] 31 | fn test_des_string_map() -> Result<()> { 32 | let json = r#" 33 | {"name": { 34 | "$contains": "World", 35 | "$startsWith": "Hello" 36 | } 37 | }"#; 38 | 39 | let my_filter: MyFilter = serde_json::from_str(json)?; 40 | 41 | let mut nodes = my_filter.filter_nodes(None); 42 | 43 | assert_eq!(nodes.len(), 1, "number of filter node should be 1"); 44 | let node = nodes.pop().unwrap(); 45 | assert_eq!(format!("{:?}", node.opvals[0]), "String(Contains(\"World\"))"); 46 | assert_eq!(format!("{:?}", node.opvals[1]), "String(StartsWith(\"Hello\"))"); 47 | // assert_eq!(node.opvals[0]) 48 | 49 | Ok(()) 50 | } 51 | 52 | #[test] 53 | fn test_des_number_simple() -> Result<()> { 54 | let json = r#" 55 | { 56 | "id": 123 57 | } 58 | "#; 59 | 60 | let my_filter: MyFilter = serde_json::from_str(json)?; 61 | let filter_str = format!("{my_filter:?}"); 62 | assert!( 63 | filter_str.contains("{ id: Some(OpValsInt64([Eq(123)])), name: None }"), 64 | "{filter_str}" 65 | ); 66 | 67 | Ok(()) 68 | } 69 | 70 | #[test] 71 | fn test_des_number_map() -> Result<()> { 72 | let json = r#" 73 | { 74 | "id": {"$gt": 100} 75 | } 76 | "#; 77 | 78 | let my_filter: MyFilter = serde_json::from_str(json)?; 79 | assert!(format!("{my_filter:?}").contains("{ id: Some(OpValsInt64([Gt(100)])), name: None }")); 80 | 81 | Ok(()) 82 | } 83 | --------------------------------------------------------------------------------