#![deny(warnings)]
use http::{
Method,
Request,
};
pub use restq::{
ast::{
ddl::{
alter_table,
drop_table,
table_def,
ColumnDef,
},
dml::{
delete,
insert,
update,
},
AlterTable,
Delete,
DropTable,
Insert,
Select,
Statement,
TableDef,
Update,
},
parser::select,
pom::parser::{
sym,
tag,
Parser,
},
space,
to_chars,
CsvRows,
DataValue,
Error,
StmtData,
};
use std::io::Cursor;
pub fn parse_statement(
request: &Request<String>,
) -> Result<(Statement, Vec<Vec<DataValue>>), Error> {
let method = request.method();
let url = extract_path_and_query(request);
let body = request.body().as_bytes().to_vec();
parse_statement_from_parts(method, url, Some(body))
}
fn parse_statement_from_parts(
method: &Method,
url: &str,
body: Option<Vec<u8>>,
) -> Result<(Statement, Vec<Vec<DataValue>>), Error> {
let csv_data = csv_data_from_parts(&method, url, body)?;
let statement = csv_data.statement();
let csv_rows = csv_data.rows_iter(None);
let data_values: Vec<Vec<DataValue>> = if let Some(csv_rows) = csv_rows {
csv_rows.into_iter().collect()
} else {
vec![]
};
Ok((statement, data_values))
}
fn extract_path_and_query<T>(request: &Request<T>) -> &str {
request
.uri()
.path_and_query()
.map(|pnq| pnq.as_str())
.unwrap_or("/")
}
pub fn extract_restq_from_request<T>(request: &Request<T>) -> String {
let method = request.method();
let url = extract_path_and_query(request);
let prefix = method_to_prefix(method);
format!("{} {}\n", prefix, url)
}
fn method_to_prefix(method: &Method) -> &'static str {
match *method {
Method::GET => "GET",
Method::PUT => "PUT",
Method::POST => "POST",
Method::PATCH => "PATCH",
Method::DELETE => "DELETE",
Method::HEAD => "HEAD",
Method::OPTIONS => todo!(),
Method::TRACE => todo!("use this for database connection checking"),
Method::CONNECT => {
todo!("maybe used this for precaching/db_url connect")
}
_ => {
let _ext = method.as_str();
todo!("Support for DROP, PURGE, ALTER, CREATE here")
}
}
}
pub fn csv_data_from_parts(
method: &Method,
url: &str,
body: Option<Vec<u8>>,
) -> Result<StmtData<Cursor<Vec<u8>>>, Error> {
let prefix = method_to_prefix(method);
let mut prefixed_url_and_body =
format!("{} {}\n", prefix, url).into_bytes();
println!(
"url_with_body: {}",
String::from_utf8_lossy(&prefixed_url_and_body)
);
body.map(|body| prefixed_url_and_body.extend(body));
Ok(StmtData::from_reader(Cursor::new(prefixed_url_and_body))?)
}
#[cfg(test)]
mod tests {
use super::parse_statement;
use http::Request;
use restq::{
ast::{
ddl::{
ColumnAttribute,
ColumnDef,
DataTypeDef,
},
Column,
Table,
TableDef,
TableLookup,
},
DataType,
};
#[test]
fn test_parse_create_statement() {
let req = Request::builder()
.method("PUT")
.uri("https://localhost:8000/product(*product_id:s32,@name:text,description:text,updated:utc,created_by(users):u32,@is_active:bool)")
.body("1,go pro,a slightly used go pro, 2019-10-31 10:10:10.1\n\
2,shovel,a slightly used shovel, 2019-11-11 11:11:11.2\n\
".to_string())
.unwrap();
let (statement, _rows) = parse_statement(&req).expect("must not fail");
println!("statement: {:#?}", statement);
let users_table = TableDef {
table: Table {
name: "users".into(),
},
columns: vec![ColumnDef {
column: Column {
name: "user_id".into(),
},
attributes: Some(vec![ColumnAttribute::Primary]),
data_type_def: DataTypeDef {
data_type: DataType::U64,
is_optional: false,
default: None,
},
foreign: None,
}],
};
let mut table_lookup = TableLookup::new();
table_lookup.add_table(users_table);
assert_eq!(
statement
.into_sql_statement(Some(&table_lookup))
.expect("must not fail")
.to_string(),
"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)"
);
}
#[test]
fn test_parse_select_statement() {
let req = Request::builder()
.method("GET")
.uri("https://localhost:8000/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")
.body("".to_string())
.unwrap();
let (statement, _rows) = parse_statement(&req).expect("must not fail");
println!("statement: {:#?}", statement);
let person_table = TableDef {
table: Table {
name: "person".into(),
},
columns: vec![ColumnDef {
column: Column { name: "id".into() },
attributes: Some(vec![ColumnAttribute::Primary]),
data_type_def: DataTypeDef {
data_type: DataType::S64,
is_optional: false,
default: None,
},
foreign: None,
}],
};
let users_table = TableDef {
table: Table {
name: "users".into(),
},
columns: vec![ColumnDef {
column: Column {
name: "person_id".into(),
},
attributes: None,
data_type_def: DataTypeDef {
data_type: DataType::U64,
is_optional: false,
default: None,
},
foreign: Some(Table {
name: "person".into(),
}),
}],
};
let mut table_lookup = TableLookup::new();
table_lookup.add_table(person_table);
table_lookup.add_table(users_table);
assert_eq!(
statement
.into_sql_statement(Some(&table_lookup))
.unwrap()
.to_string(),
"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 ROWS"
);
}
}