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
18pub 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
82pub 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}