restq_http/
lib.rs

1#![deny(warnings)]
2
3use http::{Method, Request};
4use percent_encoding::percent_decode_str;
5pub use restq::{
6    ast::{
7        ddl::{alter_table, drop_table, table_def, ColumnDef},
8        dml::{delete, insert, update},
9        AlterTable, Delete, DropTable, Foreign, Insert, Select, Statement,
10        TableDef, Update, Value,
11    },
12    parser::select,
13    pom::parser::{sym, tag, Parser},
14    space, to_chars, CsvRows, DataValue, Error, StmtData,
15};
16use std::io::Cursor;
17
18/// Parse into SQL Statement AST from http::Request
19pub fn parse_statement(
20    request: &Request<String>,
21) -> Result<(Statement, Vec<Vec<Value>>), Error> {
22    let method = request.method();
23    let url = extract_path_and_query(request);
24    let body = request.body().as_bytes().to_vec();
25    parse_statement_from_parts(method, &url, Some(body))
26}
27
28fn parse_statement_from_parts(
29    method: &Method,
30    url: &str,
31    body: Option<Vec<u8>>,
32) -> Result<(Statement, Vec<Vec<Value>>), Error> {
33    let csv_data = csv_data_from_parts(&method, url, body)?;
34    let statement = csv_data.statement();
35    let csv_rows = csv_data.rows_iter();
36
37    let data_values: Vec<Vec<Value>> = if let Some(csv_rows) = csv_rows {
38        csv_rows.into_iter().collect()
39    } else {
40        vec![]
41    };
42
43    Ok((statement, data_values))
44}
45
46fn extract_path_and_query<T>(request: &Request<T>) -> String {
47    let pnq = request
48        .uri()
49        .path_and_query()
50        .map(|pnq| pnq.as_str())
51        .unwrap_or("/");
52    percent_decode_str(pnq).decode_utf8_lossy().to_string()
53}
54
55pub fn extract_restq_from_request<T>(request: &Request<T>) -> String {
56    let method = request.method();
57    let url = extract_path_and_query(request);
58    let prefix = method_to_prefix(method);
59    format!("{} {}\n", prefix, url)
60}
61
62fn method_to_prefix(method: &Method) -> &'static str {
63    match *method {
64        Method::GET => "GET",
65        Method::PUT => "PUT",
66        Method::POST => "POST",
67        Method::PATCH => "PATCH",
68        Method::DELETE => "DELETE",
69        Method::HEAD => "HEAD",
70        Method::OPTIONS => todo!(),
71        Method::TRACE => todo!("use this for database connection checking"),
72        Method::CONNECT => {
73            todo!("maybe used this for precaching/db_url connect")
74        }
75        _ => {
76            let _ext = method.as_str();
77            todo!("Support for DROP, PURGE, ALTER, CREATE here")
78        }
79    }
80}
81
82/// Parse into SQL Statement AST from separate parts
83/// this is useful when using a different crate for the http request
84pub fn csv_data_from_parts(
85    method: &Method,
86    url: &str,
87    body: Option<Vec<u8>>,
88) -> Result<StmtData<Cursor<Vec<u8>>>, Error> {
89    let prefix = method_to_prefix(method);
90    let mut prefixed_url_and_body =
91        format!("{} {}\n", prefix, url).into_bytes();
92    println!(
93        "url_with_body: {}",
94        String::from_utf8_lossy(&prefixed_url_and_body)
95    );
96    body.map(|body| prefixed_url_and_body.extend(body));
97    Ok(StmtData::from_reader(Cursor::new(prefixed_url_and_body))?)
98}
99
100#[cfg(test)]
101mod tests {
102    use super::*;
103    use http::Request;
104    use percent_encoding::{utf8_percent_encode, NON_ALPHANUMERIC};
105    use restq::{
106        ast::{
107            ddl::{ColumnAttribute, ColumnDef, DataTypeDef},
108            ColumnName, TableDef, TableLookup, TableName,
109        },
110        DataType,
111    };
112
113    #[test]
114    fn test_parse_create_statement() {
115        let url = "product{*product_id:s32,@name:text,description:text,updated:utc,created_by(users):u32,@is_active:bool}";
116        let url = utf8_percent_encode(url, NON_ALPHANUMERIC).to_string();
117        let url = format!("http://localhost:8000/{}", url);
118        println!("url: {}", url);
119        let req = Request::builder()
120            .method("PUT")
121            .uri(&url)
122            .body(
123                "1,go pro,a slightly used go pro, 2019-10-31 10:10:10.1\n\
124                   2,shovel,a slightly used shovel, 2019-11-11 11:11:11.2\n\
125                "
126                .to_string(),
127            )
128            .unwrap();
129
130        let (statement, _rows) = parse_statement(&req).expect("must not fail");
131
132        println!("statement: {:#?}", statement);
133
134        let users_table = TableDef {
135            table: TableName {
136                name: "users".into(),
137            },
138            columns: vec![ColumnDef {
139                column: ColumnName {
140                    name: "user_id".into(),
141                },
142                attributes: Some(vec![ColumnAttribute::Primary]),
143                data_type_def: DataTypeDef {
144                    data_type: DataType::U64,
145                    is_optional: false,
146                    default: None,
147                },
148                foreign: None,
149            }],
150        };
151        let mut table_lookup = TableLookup::new();
152        table_lookup.add_table(users_table);
153        assert_eq!(
154            statement
155                .into_sql_statement(Some(&table_lookup))
156                .expect("must not fail")
157                .to_string(),
158            "CREATE TABLE IF NOT EXISTS product (product_id INT PRIMARY KEY NOT NULL, name TEXT NOT NULL, description TEXT NOT NULL, updated TIMESTAMP NOT NULL, created_by INT NOT NULL REFERENCES users (user_id), is_active BOOLEAN NOT NULL)"
159        );
160    }
161
162    #[test]
163    fn test_parse_select_statement() {
164        let url = "person-><-users{name,age,class}?(age=gt.42&student=eq.true)|(gender=eq.`M`&is_active=true)&group_by=sum(age),grade,gender&having=min(age)=gte.42&order_by=age.desc,height.asc&page=2&page_size=10";
165        let url = utf8_percent_encode(url, NON_ALPHANUMERIC).to_string();
166        let url = format!("http://localhost:8000/{}", url);
167        println!("url: {}", url);
168        let req = Request::builder()
169            .method("GET")
170            .uri(&url)
171            .body("".to_string())
172            .unwrap();
173        let (statement, _rows) = parse_statement(&req).expect("must not fail");
174        println!("statement: {:#?}", statement);
175
176        let person_table = TableDef {
177            table: TableName {
178                name: "person".into(),
179            },
180            columns: vec![ColumnDef {
181                column: ColumnName { name: "id".into() },
182                attributes: Some(vec![ColumnAttribute::Primary]),
183                data_type_def: DataTypeDef {
184                    data_type: DataType::S64,
185                    is_optional: false,
186                    default: None,
187                },
188                foreign: None,
189            }],
190        };
191        let users_table = TableDef {
192            table: TableName {
193                name: "users".into(),
194            },
195            columns: vec![ColumnDef {
196                column: ColumnName {
197                    name: "person_id".into(),
198                },
199                attributes: None,
200                data_type_def: DataTypeDef {
201                    data_type: DataType::U64,
202                    is_optional: false,
203                    default: None,
204                },
205                foreign: Some(Foreign {
206                    table: TableName {
207                        name: "person".into(),
208                    },
209                    column: Some(ColumnName { name: "id".into() }),
210                }),
211            }],
212        };
213        let mut table_lookup = TableLookup::new();
214        table_lookup.add_table(person_table);
215        table_lookup.add_table(users_table);
216        assert_eq!(
217            statement
218                .into_sql_statement(Some(&table_lookup))
219                .unwrap()
220                .to_string(),
221            "SELECT name, age, class FROM person JOIN users ON users.person_id = person.id WHERE (age > 42 AND student = true) OR (gender = 'M' AND is_active = true) GROUP BY sum(age), grade, gender HAVING min(age) >= 42 ORDER BY age DESC, height ASC LIMIT 10 OFFSET 10"
222        );
223    }
224}