diff --git a/.github/workflows/build-and-test.yml b/.github/workflows/build-and-test.yml new file mode 100644 index 0000000..7d8fa64 --- /dev/null +++ b/.github/workflows/build-and-test.yml @@ -0,0 +1,25 @@ +name: Build & Test Draco + +on: + push: + +jobs: + draco: + runs-on: ubuntu-latest + + defaults: + run: + shell: bash + + steps: + - uses: actions/checkout@v4 + - name: Setup rust + run: rustup update --no-self-update stable + - name: Build crate + run: PATH=${{ runner.temp }}/proto/bin:$PATH cargo build + env: + RUST_BACKTRACE: 'full' + - name: Run Tests + run: PATH=${{ runner.temp }}/proto/bin:$PATH cargo test + env: + RUST_BACKTRACE: 'full' diff --git a/draco_rest/src/http/request.rs b/draco_rest/src/http/request.rs index f751c25..1958e8c 100644 --- a/draco_rest/src/http/request.rs +++ b/draco_rest/src/http/request.rs @@ -5,8 +5,9 @@ use std::{ io::{BufRead, BufReader, Read}, net::TcpStream, str::FromStr, + usize, }; -use tucana::shared::Value; +use tucana::shared::{value::Kind, Struct, Value}; #[derive(Debug)] pub enum HttpOption { @@ -42,30 +43,104 @@ impl ToString for HttpOption { } #[derive(Debug)] -pub struct HttpRequest { - pub method: HttpOption, - pub path: String, - pub version: String, - pub headers: Vec, - pub body: Option, +pub struct HeaderMap { + pub fields: HashMap, } -#[derive(Debug)] -pub enum PrimitiveValue { - String(String), - Number(f64), - Boolean(bool), -} +impl HeaderMap { + pub fn new() -> Self { + HeaderMap { + fields: HashMap::new(), + } + } -#[derive(Debug)] -pub struct PrimitiveStruct { - pub fields: HashMap, + /// Create a new HeaderMap from a vector of strings. + /// + /// Each string should be in the format "key: value". + /// + /// # Examples + /// + /// ``` + /// let header = vec![ + /// "Content-Type: application/json".to_string(), + /// "User-Agent: Mozilla/5.0".to_string(), + /// ]; + /// let header_map = HeaderMap::from_vec(header); + /// assert_eq!(header_map.get("content-type"), Some(&"application/json".to_string())); + /// assert_eq!(header_map.get("user-agent"), Some(&"Mozilla/5.0".to_string())); + /// ``` + pub fn from_vec(header: Vec) -> Self { + let mut header_map = HeaderMap::new(); + + for param in header { + let mut parts = param.split(": "); + let key = match parts.next() { + Some(key) => key.to_lowercase(), + None => continue, + }; + let value = match parts.next() { + Some(value) => value.to_lowercase(), + None => continue, + }; + + header_map.add(key, value); + } + + header_map + } + + #[inline] + pub fn add(&mut self, key: String, value: String) { + self.fields.insert(key, value); + } + + #[inline] + pub fn get(&self, key: &str) -> Option<&String> { + self.fields.get(key) + } } #[derive(Debug)] -pub struct HttpParameter { - pub url_query: Option, - pub url_parameters: Option, +pub struct HttpRequest { + pub method: HttpOption, + pub path: String, + pub version: String, + pub headers: HeaderMap, + + /// The body of the request. + /// + /// # Example + /// If the url was called: + /// + /// url: .../api/users/123/posts/456?filter=recent&sort=asc + /// + /// from the regex: "^/api/users/(?P\d+)/posts/(?P\d+)(\?.*)?$" + /// + /// With the request body: + /// + /// ```json + /// { + /// "first": 2, + /// "second": 300 + /// } + /// ``` + /// The equivalent HTTP request body will look like: + /// ```json + /// { + /// "url": { + /// "user_id": "123", + /// "post_id": "456", + /// }, + /// "query": { + /// "filter": "recent", + /// "sort": "asc" + /// }, + /// "body": { + /// "first": "1", + /// "second": "2" + /// } + /// } + /// ``` pub body: Option, } @@ -85,29 +160,7 @@ pub fn convert_to_http_request(stream: &TcpStream) -> Result 0 { - let mut body = vec![0; content_length]; - if let Ok(_) = buf_reader.read_exact(&mut body) { - // Parse JSON body - if let Ok(json_value) = serde_json::from_slice::(&body) { - http_request.body = Some(to_tucana_value(json_value)); - } - } - } - break; - } - } + let http_request = parse_request(raw_http_request, buf_reader)?; log::debug!("Received HTTP Request: {:?}", &http_request); @@ -121,8 +174,14 @@ pub fn convert_to_http_request(stream: &TcpStream) -> Result) -> Result { +#[inline] +fn parse_request( + raw_http_request: Vec, + mut buf_reader: BufReader<&TcpStream>, +) -> Result { let params = &raw_http_request[0]; + let headers = raw_http_request[1..raw_http_request.len()].to_vec(); + let header_map = HeaderMap::from_vec(headers); if params.is_empty() { return Err(HttpResponse::bad_request( @@ -152,11 +211,77 @@ fn parse_request(raw_http_request: Vec) -> Result = HashMap::new(); + + if let Some(content_length) = header_map.get("content-length") { + let size: usize = match content_length.parse() { + Ok(len) => len, + Err(_) => { + return Err(HttpResponse::bad_request( + "Invalid content-length header".to_string(), + HashMap::new(), + )) + } + }; + + let mut body = vec![0; size]; + if let Ok(_) = buf_reader.read_exact(&mut body) { + if let Ok(json_value) = serde_json::from_slice::(&body) { + body_values.insert("body".to_string(), to_tucana_value(json_value)); + } + } + }; + + if path.contains("?") { + let mut fields: HashMap = HashMap::new(); + if let Some((_, query)) = path.split_once("?") { + let values = query.split("&"); + + for value in values { + let mut parts = value.split("="); + let key = match parts.next() { + Some(key) => key.to_string(), + None => continue, + }; + + let value = match parts.next() { + Some(value) => value.to_string(), + None => continue, + }; + + fields.insert( + key, + Value { + kind: Some(Kind::StringValue(value)), + }, + ); + } + }; + + if !fields.is_empty() { + let value = Value { + kind: Some(Kind::StructValue(Struct { fields })), + }; + + body_values.insert("query".to_string(), value); + } + }; + + let body = if !body_values.is_empty() { + Some(Value { + kind: Some(Kind::StructValue(Struct { + fields: body_values, + })), + }) + } else { + None + }; + Ok(HttpRequest { method, path: path.to_string(), version: version.to_string(), - headers: raw_http_request.clone(), - body: None, + headers: header_map, + body, }) } diff --git a/draco_rest/src/queue/mod.rs b/draco_rest/src/queue/mod.rs index 084dd45..1c98e47 100644 --- a/draco_rest/src/queue/mod.rs +++ b/draco_rest/src/queue/mod.rs @@ -9,6 +9,7 @@ pub mod queue { }; use draco_validator::{resolver::flow_resolver::resolve_flow, verify_flow}; use std::{collections::HashMap, sync::Arc, time::Duration}; + use tucana::shared::{Struct, Value}; fn create_rest_message(message_content: String) -> Message { Message { @@ -28,14 +29,14 @@ pub mod queue { } pub async fn handle_connection( - request: HttpRequest, + mut request: HttpRequest, flow_store: FlowStore, rabbitmq_client: Arc, ) -> HttpResponse { // Check if a flow exists for the given settings let flow_exists = check_flow_exists(&flow_store, &request).await; - let flow = match flow_exists { + let flow_result = match flow_exists { Some(flow) => flow, None => { return HttpResponse::not_found( @@ -45,6 +46,53 @@ pub mod queue { } }; + let flow = flow_result.flow; + let regex_pattern = flow_result.regex_pattern; + let mut url_params: HashMap = HashMap::new(); + + //Resolve url params + let capture_keys = regex_pattern.capture_names(); + if let Some(captures) = regex_pattern.captures(&request.path) { + for key_option in capture_keys { + let key = match key_option { + Some(key) => key, + None => continue, + }; + + let value = match captures.name(key) { + Some(value) => value.as_str().to_string(), + None => continue, + }; + + let string_value = Value { + kind: Some(tucana::shared::value::Kind::StringValue(value)), + }; + + url_params.insert(key.to_string(), string_value); + } + } + + //Will add the url params to the request body + if !url_params.is_empty() { + if let Some(body) = &mut request.body { + if let Some(kind) = &mut body.kind { + match kind { + tucana::shared::value::Kind::StructValue(struct_value) => { + struct_value.fields.insert( + "url".to_string(), + Value { + kind: Some(tucana::shared::value::Kind::StructValue(Struct { + fields: url_params, + })), + }, + ); + } + _ => {} + } + } + } + } + // Determine which flow to use based on request body let flow_to_use = if let Some(body) = &request.body { // Verify flow diff --git a/draco_rest/src/store/mod.rs b/draco_rest/src/store/mod.rs index 774d1ba..44b5ff9 100644 --- a/draco_rest/src/store/mod.rs +++ b/draco_rest/src/store/mod.rs @@ -2,9 +2,19 @@ pub mod store { use crate::http::request::HttpRequest; use code0_flow::flow_store::connection::FlowStore; use redis::{AsyncCommands, JsonAsyncCommands}; + use regex::Regex; use tucana::shared::{value::Kind, Flow, Struct}; - pub async fn check_flow_exists(flow_store: &FlowStore, request: &HttpRequest) -> Option { + //The regex is required for later purposes --> resolve the parameter of the url + pub struct FlowExistResult { + pub flow: Flow, + pub regex_pattern: Regex, + } + + pub async fn check_flow_exists( + flow_store: &FlowStore, + request: &HttpRequest, + ) -> Option { let mut store = flow_store.lock().await; // Get all keys from Redis @@ -26,6 +36,7 @@ pub mod store { for flow in result { let mut correct_url = false; let mut correct_method = false; + let mut flow_regex: Option = None; for setting in flow.settings.clone() { let definition = match setting.definition { @@ -71,6 +82,7 @@ pub mod store { if regex.is_match(&request.path) { correct_url = true; + flow_regex = Some(regex); } } } @@ -81,7 +93,15 @@ pub mod store { } if correct_method && correct_url { - return Some(flow); + let regex_pattern = match flow_regex { + Some(regex) => regex.clone(), + None => continue, + }; + + return Some(FlowExistResult { + flow, + regex_pattern, + }); } } diff --git a/draco_validator/src/resolver/flow_resolver.rs b/draco_validator/src/resolver/flow_resolver.rs index b190d9d..8a2bc9b 100644 --- a/draco_validator/src/resolver/flow_resolver.rs +++ b/draco_validator/src/resolver/flow_resolver.rs @@ -1,5 +1,7 @@ use tucana::shared::{value::Kind, Flow, Value}; +use crate::path::path::expect_kind; + pub fn resolve_flow(flow: &mut Flow, body: Value) -> Result { let node = match &mut flow.starting_node { Some(node) => node, @@ -15,18 +17,16 @@ pub fn resolve_flow(flow: &mut Flow, body: Value) -> Result { match value { tucana::shared::node_parameter::Value::LiteralValue(param_value) => { if let Some(Kind::StringValue(key)) = &mut param_value.kind { - if let Some(Kind::StructValue(ref struct_value)) = body.kind { - let body_value = struct_value.fields.get(key); + if let Some(kind) = expect_kind(key, &body) { + let body_value = Value { kind: Some(kind) }; println!( "Field: {}, will be replaced from body with the value: {:?}", key, body_value ); - if let Some(body_val) = body_value { - *param_value = body_val.clone(); - } - } + *param_value = body_value + }; } /* if let Some(Kind::StructValue(struct_value)) = &mut param_value.kind { diff --git a/draco_validator/src/rules/contains_key.rs b/draco_validator/src/rules/contains_key.rs index 34a4b7c..089ead2 100644 --- a/draco_validator/src/rules/contains_key.rs +++ b/draco_validator/src/rules/contains_key.rs @@ -2,6 +2,7 @@ use super::violation::ContainsKeyRuleViolation; use super::violation::DataTypeRuleError; use super::violation::DataTypeRuleViolation; use super::violation::MissingDataTypeRuleDefinition; +use crate::path::path::expect_kind; use crate::{verify_body, ContainsRule}; use tucana::shared::value::Kind; use tucana::shared::DataType; @@ -29,11 +30,11 @@ pub fn apply_contains_key( ) -> Result<(), DataTypeRuleError> { println!("{:?} on body {:?}", rule, body); - if let Some(Kind::StructValue(struct_value)) = &body.kind { - let fields = struct_value.fields.to_owned(); - - let value = match fields.get(&rule.key) { - Some(value) => value.to_owned(), + if let Some(Kind::StructValue(_)) = &body.kind { + let value = match expect_kind(&rule.key, &body) { + Some(value) => Value { + kind: Some(value.to_owned()), + }, None => { let error = ContainsKeyRuleViolation { missing_key: rule.key,