├── .gitignore ├── .travis.yml ├── Cargo.toml ├── LICENSE ├── README.md ├── appveyor.yml ├── examples └── simple.rs ├── rustfmt.toml ├── src └── lib.rs ├── tests └── test_conversion.rs └── update_readme.sh /.gitignore: -------------------------------------------------------------------------------- 1 | # Compiled files 2 | *.o 3 | *.so 4 | *.rlib 5 | *.dll 6 | #Generated by rustorm 7 | #/examples/gen/** 8 | /gen/** 9 | # Executables 10 | *.exe 11 | 12 | # Generated by Cargo 13 | /target*/ 14 | 15 | #generated by eclipse 16 | /.project 17 | /.DS_Store 18 | 19 | #swap files 20 | *~ 21 | *.swp 22 | *.swo 23 | *.bk 24 | Cargo.lock 25 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: rust 2 | rust: 3 | - nightly 4 | 5 | script: 6 | - cargo build 7 | - cargo test 8 | 9 | after_success: | 10 | sudo apt-get install libcurl4-openssl-dev libelf-dev libdw-dev && 11 | wget https://github.com/SimonKagstrom/kcov/archive/master.tar.gz && 12 | tar xzf master.tar.gz && 13 | mkdir kcov-master/build && 14 | cd kcov-master/build && 15 | cmake .. && 16 | make && 17 | sudo make install && 18 | cd ../.. && 19 | kcov --coveralls-id=$TRAVIS_JOB_ID --exclude-pattern=/.cargo target/kcov target/debug/inquerest-*; 20 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "inquerest" 3 | version = "0.3.1" 4 | authors = [ "Jovansonlee Cesar" ] 5 | license = "MIT" 6 | description = "A complex url parameter parser for rest filter queries" 7 | readme = "README.md" 8 | repository = "https://github.com/ivanceras/inquerest" 9 | documentation = "https://docs.rs/inquerest" 10 | edition = "2018" 11 | keywords = ["url", "param", "parser", "rest", "query", ] 12 | 13 | [dependencies] 14 | restq = { version = "0.3" } 15 | thiserror = "1.0" 16 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Jovansonlee Cesar 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. 22 | 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # inquerest 2 | 3 | Inquerest can parse complex url query into a SQL abstract syntax tree. 4 | 5 | Example this url: 6 | ```rust 7 | /person?age=lt.42&(student=eq.true|gender=eq.'M')&group_by=sum(age),grade,gender&having=min(age)=gt.42&order_by=age.desc,height.asc&page=20&page_size=100 8 | ``` 9 | will be parsed into: 10 | 11 | ```rust 12 | Select { 13 | from_table: FromTable { 14 | from: Table { 15 | name: "person", 16 | }, 17 | join: None, 18 | }, 19 | filter: Some( 20 | BinaryOperation( 21 | BinaryOperation { 22 | left: BinaryOperation( 23 | BinaryOperation { 24 | left: Column( 25 | Column { 26 | name: "age", 27 | }, 28 | ), 29 | operator: Lt, 30 | right: Value( 31 | Number( 32 | 42.0, 33 | ), 34 | ), 35 | }, 36 | ), 37 | operator: And, 38 | right: Nested( 39 | BinaryOperation( 40 | BinaryOperation { 41 | left: BinaryOperation( 42 | BinaryOperation { 43 | left: Column( 44 | Column { 45 | name: "student", 46 | }, 47 | ), 48 | operator: Eq, 49 | right: Value( 50 | Bool( 51 | true, 52 | ), 53 | ), 54 | }, 55 | ), 56 | operator: Or, 57 | right: BinaryOperation( 58 | BinaryOperation { 59 | left: Column( 60 | Column { 61 | name: "gender", 62 | }, 63 | ), 64 | operator: Eq, 65 | right: Value( 66 | String( 67 | "M", 68 | ), 69 | ), 70 | }, 71 | ), 72 | }, 73 | ), 74 | ), 75 | }, 76 | ), 77 | ), 78 | group_by: Some( 79 | [ 80 | Function( 81 | Function { 82 | name: "sum", 83 | params: [ 84 | Column( 85 | Column { 86 | name: "age", 87 | }, 88 | ), 89 | ], 90 | }, 91 | ), 92 | Column( 93 | Column { 94 | name: "grade", 95 | }, 96 | ), 97 | Column( 98 | Column { 99 | name: "gender", 100 | }, 101 | ), 102 | ], 103 | ), 104 | having: Some( 105 | BinaryOperation( 106 | BinaryOperation { 107 | left: Function( 108 | Function { 109 | name: "min", 110 | params: [ 111 | Column( 112 | Column { 113 | name: "age", 114 | }, 115 | ), 116 | ], 117 | }, 118 | ), 119 | operator: Gt, 120 | right: Value( 121 | Number( 122 | 42.0, 123 | ), 124 | ), 125 | }, 126 | ), 127 | ), 128 | projection: None, 129 | order_by: Some( 130 | [ 131 | Order { 132 | expr: Column( 133 | Column { 134 | name: "age", 135 | }, 136 | ), 137 | direction: Some( 138 | Desc, 139 | ), 140 | }, 141 | Order { 142 | expr: Column( 143 | Column { 144 | name: "height", 145 | }, 146 | ), 147 | direction: Some( 148 | Asc, 149 | ), 150 | }, 151 | ], 152 | ), 153 | range: Some( 154 | Page( 155 | Page { 156 | page: 20, 157 | page_size: 100, 158 | }, 159 | ), 160 | ), 161 | } 162 | ``` 163 | Which translate to the sql statement: 164 | ```sql 165 | SELECT * FROM person WHERE age < 42 AND (student = true OR gender = 'M') GROUP BY sum(age), grade, gender HAVING min(age) > 42 ORDER BY age DESC, height ASC LIMIT 100 OFFSET 1900 ROWS 166 | ``` 167 | Note: However, you don't want to convert to the sql statement directly to avoid sql injection 168 | attack. You need to validate the tables and columns if it is allowed to be accessed by the 169 | user. You also need to extract the values yourself and supply it as a parameterized value into 170 | your ORM. 171 | 172 | ##### Please support this project: 173 | [![Become a patron](https://c5.patreon.com/external/logo/become_a_patron_button.png)](https://www.patreon.com/ivanceras) 174 | 175 | License: MIT 176 | -------------------------------------------------------------------------------- /appveyor.yml: -------------------------------------------------------------------------------- 1 | install: 2 | - ps: Start-FileDownload 'https://static.rust-lang.org/dist/rust-nightly-i686-pc-windows-gnu.exe' 3 | - rust-nightly-i686-pc-windows-gnu.exe /VERYSILENT /NORESTART /DIR="C:\Program Files (x86)\Rust" 4 | - SET PATH=%PATH%;C:\Program Files (x86)\Rust\bin 5 | - SET PATH=%PATH%;C:\MinGW\bin 6 | - rustc -V 7 | - cargo -V 8 | - git submodule update --init --recursive 9 | 10 | build: false 11 | 12 | test_script: 13 | - cargo test --verbose 14 | -------------------------------------------------------------------------------- /examples/simple.rs: -------------------------------------------------------------------------------- 1 | fn main() { 2 | let url = "/person?age=lt.42&(student=eq.true|gender=eq.'M')&group_by=sum(age),grade,gender&having=min(age)=gt.42&order_by=age.desc,height.asc&page=20&page_size=100"; 3 | let query = inquerest::parse_query(url); 4 | println!("query: {:#?}", query); 5 | println!( 6 | "sql query: {}", 7 | query.unwrap().into_sql_statement(None).unwrap().to_string() 8 | ); 9 | 10 | let filter = "age=lt.42&(student=eq.true|gender=eq.'M')&group_by=sum(age),grade,gender&having=min(age)=gt.42&order_by=age.desc,height.asc&page=20&page_size=100"; 11 | let result = inquerest::parse_filter(filter); 12 | println!("filter_only: {:#?}", result); 13 | } 14 | -------------------------------------------------------------------------------- /rustfmt.toml: -------------------------------------------------------------------------------- 1 | # Use unstable features 2 | unstable_features = true 3 | 4 | max_width = 80 5 | 6 | ## Visually align, useful in writing the view 7 | indent_style = "Block" 8 | imports_indent = "Block" 9 | reorder_imports = true 10 | reorder_impl_items = true 11 | merge_imports = true 12 | ## I want to be able to delete unused imports easily 13 | imports_layout = "Vertical" 14 | ## Default value is false, yet clipy keeps nagging on this 15 | use_field_init_shorthand = true 16 | 17 | ## also format macro 18 | format_macro_matchers = true 19 | force_multiline_blocks = true 20 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | #![deny(warnings)] 2 | //! Inquerest can parse complex url query into a SQL abstract syntax tree. 3 | //! 4 | //! Example this url: 5 | //! ```no_run,ignore 6 | //! /person?age=lt.42&(student=eq.true|gender=eq.'M')&group_by=sum(age),grade,gender&having=min(age)=gt.42&order_by=age.desc,height.asc&page=20&page_size=100 7 | //! ``` 8 | //! will be parsed into: 9 | //! 10 | //! ```rust,ignore 11 | //! Select { 12 | //! from_table: FromTable { 13 | //! from: Table { 14 | //! name: "person", 15 | //! }, 16 | //! join: None, 17 | //! }, 18 | //! filter: Some( 19 | //! BinaryOperation( 20 | //! BinaryOperation { 21 | //! left: BinaryOperation( 22 | //! BinaryOperation { 23 | //! left: Column( 24 | //! Column { 25 | //! name: "age", 26 | //! }, 27 | //! ), 28 | //! operator: Lt, 29 | //! right: Value( 30 | //! Number( 31 | //! 42.0, 32 | //! ), 33 | //! ), 34 | //! }, 35 | //! ), 36 | //! operator: And, 37 | //! right: Nested( 38 | //! BinaryOperation( 39 | //! BinaryOperation { 40 | //! left: BinaryOperation( 41 | //! BinaryOperation { 42 | //! left: Column( 43 | //! Column { 44 | //! name: "student", 45 | //! }, 46 | //! ), 47 | //! operator: Eq, 48 | //! right: Value( 49 | //! Bool( 50 | //! true, 51 | //! ), 52 | //! ), 53 | //! }, 54 | //! ), 55 | //! operator: Or, 56 | //! right: BinaryOperation( 57 | //! BinaryOperation { 58 | //! left: Column( 59 | //! Column { 60 | //! name: "gender", 61 | //! }, 62 | //! ), 63 | //! operator: Eq, 64 | //! right: Value( 65 | //! String( 66 | //! "M", 67 | //! ), 68 | //! ), 69 | //! }, 70 | //! ), 71 | //! }, 72 | //! ), 73 | //! ), 74 | //! }, 75 | //! ), 76 | //! ), 77 | //! group_by: Some( 78 | //! [ 79 | //! Function( 80 | //! Function { 81 | //! name: "sum", 82 | //! params: [ 83 | //! Column( 84 | //! Column { 85 | //! name: "age", 86 | //! }, 87 | //! ), 88 | //! ], 89 | //! }, 90 | //! ), 91 | //! Column( 92 | //! Column { 93 | //! name: "grade", 94 | //! }, 95 | //! ), 96 | //! Column( 97 | //! Column { 98 | //! name: "gender", 99 | //! }, 100 | //! ), 101 | //! ], 102 | //! ), 103 | //! having: Some( 104 | //! BinaryOperation( 105 | //! BinaryOperation { 106 | //! left: Function( 107 | //! Function { 108 | //! name: "min", 109 | //! params: [ 110 | //! Column( 111 | //! Column { 112 | //! name: "age", 113 | //! }, 114 | //! ), 115 | //! ], 116 | //! }, 117 | //! ), 118 | //! operator: Gt, 119 | //! right: Value( 120 | //! Number( 121 | //! 42.0, 122 | //! ), 123 | //! ), 124 | //! }, 125 | //! ), 126 | //! ), 127 | //! projection: None, 128 | //! order_by: Some( 129 | //! [ 130 | //! Order { 131 | //! expr: Column( 132 | //! Column { 133 | //! name: "age", 134 | //! }, 135 | //! ), 136 | //! direction: Some( 137 | //! Desc, 138 | //! ), 139 | //! }, 140 | //! Order { 141 | //! expr: Column( 142 | //! Column { 143 | //! name: "height", 144 | //! }, 145 | //! ), 146 | //! direction: Some( 147 | //! Asc, 148 | //! ), 149 | //! }, 150 | //! ], 151 | //! ), 152 | //! range: Some( 153 | //! Page( 154 | //! Page { 155 | //! page: 20, 156 | //! page_size: 100, 157 | //! }, 158 | //! ), 159 | //! ), 160 | //! } 161 | //! ``` 162 | //! Which translate to the sql statement: 163 | //! ```sql 164 | //! SELECT * FROM person WHERE age < 42 AND (student = true OR gender = 'M') GROUP BY sum(age), grade, gender HAVING min(age) > 42 ORDER BY age DESC, height ASC LIMIT 100 OFFSET 1900 ROWS 165 | //! ``` 166 | //! Note: However, you don't want to convert to the sql statement directly to avoid sql injection 167 | //! attack. You need to validate the tables and columns if it is allowed to be accessed by the 168 | //! user. You also need to extract the values yourself and supply it as a parameterized value into 169 | //! your ORM. 170 | //! 171 | //! #### Please support this project: 172 | //! [![Become a patron](https://c5.patreon.com/external/logo/become_a_patron_button.png)](https://www.patreon.com/ivanceras) 173 | pub use restq; 174 | 175 | pub use restq::{ 176 | ast::{Expr, Select}, 177 | parser::filter_expr, 178 | to_chars, Error, 179 | }; 180 | 181 | /// Parse a path and query in a url to a Select AST 182 | /// Example: 183 | /// ```rust 184 | /// use inquerest::*; 185 | /// 186 | /// let url = "/person?age=lt.42&(student=eq.true|gender=eq.'M')&group_by=sum(age),grade,gender&having=min(age)=gt.42&order_by=age.desc,height.asc&page=20&page_size=100"; 187 | /// let query = inquerest::parse_query(url); 188 | /// println!("query: {:#?}", query); 189 | /// println!( 190 | /// "sql query: {}", 191 | /// query.unwrap().into_sql_statement(None).unwrap().to_string() 192 | /// ); 193 | /// ``` 194 | pub fn parse_query(input: &str) -> Result { 195 | let input_chars = to_chars(input); 196 | restq::parse_select_chars(&input_chars) 197 | } 198 | 199 | /// Parse the query in a url to an Expression 200 | /// 201 | /// Example: 202 | /// ```rust 203 | /// use inquerest::*; 204 | /// 205 | /// let filter = "age=lt.42&(student=eq.true|gender=eq.'M')&group_by=sum(age),grade,gender&having=min(age)=gt.42&order_by=age.desc,height.asc&page=20&page_size=100"; 206 | /// let result = parse_filter(filter); 207 | /// println!("filter_only: {:#?}", result); 208 | /// ``` 209 | pub fn parse_filter(input: &str) -> Result { 210 | let input_chars = to_chars(input); 211 | parse_filter_chars(&input_chars) 212 | } 213 | 214 | fn parse_filter_chars(input: &[char]) -> Result { 215 | Ok(filter_expr().parse(input)?) 216 | } 217 | -------------------------------------------------------------------------------- /tests/test_conversion.rs: -------------------------------------------------------------------------------- 1 | #[test] 2 | fn test1() { 3 | let url = "/person?age=lt.42&(student=eq.true|gender=eq.'M')&group_by=sum(age),grade,gender&having=min(age)=gt.42&order_by=age.desc,height.asc&page=20&page_size=100"; 4 | let query = inquerest::parse_query(url); 5 | println!("query: {:#?}", query); 6 | assert_eq!( 7 | "SELECT * FROM person WHERE age < 42 AND (student = true OR gender = 'M') GROUP BY sum(age), grade, gender HAVING min(age) > 42 ORDER BY age DESC, height ASC LIMIT 100 OFFSET 1900 ROWS", 8 | query.unwrap().into_sql_statement(None).unwrap().to_string() 9 | ); 10 | } 11 | -------------------------------------------------------------------------------- /update_readme.sh: -------------------------------------------------------------------------------- 1 | cargo readme > README.md 2 | --------------------------------------------------------------------------------