Skip to content

Commit d6519d9

Browse files
committed
Merge branch 'main' of https://github.com/code0-tech/draco
2 parents c5fe8d8 + d99513e commit d6519d9

File tree

6 files changed

+279
-60
lines changed

6 files changed

+279
-60
lines changed

.github/workflows/build-and-test.yml

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
name: Build & Test Draco
2+
3+
on:
4+
push:
5+
6+
jobs:
7+
draco:
8+
runs-on: ubuntu-latest
9+
10+
defaults:
11+
run:
12+
shell: bash
13+
14+
steps:
15+
- uses: actions/checkout@v4
16+
- name: Setup rust
17+
run: rustup update --no-self-update stable
18+
- name: Build crate
19+
run: PATH=${{ runner.temp }}/proto/bin:$PATH cargo build
20+
env:
21+
RUST_BACKTRACE: 'full'
22+
- name: Run Tests
23+
run: PATH=${{ runner.temp }}/proto/bin:$PATH cargo test
24+
env:
25+
RUST_BACKTRACE: 'full'

draco_rest/src/http/request.rs

Lines changed: 170 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,9 @@ use std::{
55
io::{BufRead, BufReader, Read},
66
net::TcpStream,
77
str::FromStr,
8+
usize,
89
};
9-
use tucana::shared::Value;
10+
use tucana::shared::{value::Kind, Struct, Value};
1011

1112
#[derive(Debug)]
1213
pub enum HttpOption {
@@ -42,30 +43,104 @@ impl ToString for HttpOption {
4243
}
4344

4445
#[derive(Debug)]
45-
pub struct HttpRequest {
46-
pub method: HttpOption,
47-
pub path: String,
48-
pub version: String,
49-
pub headers: Vec<String>,
50-
pub body: Option<Value>,
46+
pub struct HeaderMap {
47+
pub fields: HashMap<String, String>,
5148
}
5249

53-
#[derive(Debug)]
54-
pub enum PrimitiveValue {
55-
String(String),
56-
Number(f64),
57-
Boolean(bool),
58-
}
50+
impl HeaderMap {
51+
pub fn new() -> Self {
52+
HeaderMap {
53+
fields: HashMap::new(),
54+
}
55+
}
5956

60-
#[derive(Debug)]
61-
pub struct PrimitiveStruct {
62-
pub fields: HashMap<String, PrimitiveValue>,
57+
/// Create a new HeaderMap from a vector of strings.
58+
///
59+
/// Each string should be in the format "key: value".
60+
///
61+
/// # Examples
62+
///
63+
/// ```
64+
/// let header = vec![
65+
/// "Content-Type: application/json".to_string(),
66+
/// "User-Agent: Mozilla/5.0".to_string(),
67+
/// ];
68+
/// let header_map = HeaderMap::from_vec(header);
69+
/// assert_eq!(header_map.get("content-type"), Some(&"application/json".to_string()));
70+
/// assert_eq!(header_map.get("user-agent"), Some(&"Mozilla/5.0".to_string()));
71+
/// ```
72+
pub fn from_vec(header: Vec<String>) -> Self {
73+
let mut header_map = HeaderMap::new();
74+
75+
for param in header {
76+
let mut parts = param.split(": ");
77+
let key = match parts.next() {
78+
Some(key) => key.to_lowercase(),
79+
None => continue,
80+
};
81+
let value = match parts.next() {
82+
Some(value) => value.to_lowercase(),
83+
None => continue,
84+
};
85+
86+
header_map.add(key, value);
87+
}
88+
89+
header_map
90+
}
91+
92+
#[inline]
93+
pub fn add(&mut self, key: String, value: String) {
94+
self.fields.insert(key, value);
95+
}
96+
97+
#[inline]
98+
pub fn get(&self, key: &str) -> Option<&String> {
99+
self.fields.get(key)
100+
}
63101
}
64102

65103
#[derive(Debug)]
66-
pub struct HttpParameter {
67-
pub url_query: Option<PrimitiveStruct>,
68-
pub url_parameters: Option<PrimitiveStruct>,
104+
pub struct HttpRequest {
105+
pub method: HttpOption,
106+
pub path: String,
107+
pub version: String,
108+
pub headers: HeaderMap,
109+
110+
/// The body of the request.
111+
///
112+
/// # Example
113+
/// If the url was called:
114+
///
115+
/// url: .../api/users/123/posts/456?filter=recent&sort=asc
116+
///
117+
/// from the regex: "^/api/users/(?P<user_id>\d+)/posts/(?P<post_id>\d+)(\?.*)?$"
118+
///
119+
/// With the request body:
120+
///
121+
/// ```json
122+
/// {
123+
/// "first": 2,
124+
/// "second": 300
125+
/// }
126+
/// ```
127+
/// The equivalent HTTP request body will look like:
128+
/// ```json
129+
/// {
130+
/// "url": {
131+
/// "user_id": "123",
132+
/// "post_id": "456",
133+
/// },
134+
/// "query": {
135+
/// "filter": "recent",
136+
/// "sort": "asc"
137+
/// },
138+
/// "body": {
139+
/// "first": "1",
140+
/// "second": "2"
141+
/// }
142+
/// }
143+
/// ```
69144
pub body: Option<Value>,
70145
}
71146

@@ -85,29 +160,7 @@ pub fn convert_to_http_request(stream: &TcpStream) -> Result<HttpRequest, HttpRe
85160
}
86161

87162
// Parse headers
88-
let mut http_request = parse_request(raw_http_request)?;
89-
90-
// Read body if Content-Length is specified
91-
for header in &http_request.headers {
92-
if header.to_lowercase().starts_with("content-length:") {
93-
let content_length: usize = header
94-
.split(':')
95-
.nth(1)
96-
.and_then(|s| s.trim().parse().ok())
97-
.unwrap_or(0);
98-
99-
if content_length > 0 {
100-
let mut body = vec![0; content_length];
101-
if let Ok(_) = buf_reader.read_exact(&mut body) {
102-
// Parse JSON body
103-
if let Ok(json_value) = serde_json::from_slice::<serde_json::Value>(&body) {
104-
http_request.body = Some(to_tucana_value(json_value));
105-
}
106-
}
107-
}
108-
break;
109-
}
110-
}
163+
let http_request = parse_request(raw_http_request, buf_reader)?;
111164

112165
log::debug!("Received HTTP Request: {:?}", &http_request);
113166

@@ -121,8 +174,14 @@ pub fn convert_to_http_request(stream: &TcpStream) -> Result<HttpRequest, HttpRe
121174
Ok(http_request)
122175
}
123176

124-
fn parse_request(raw_http_request: Vec<String>) -> Result<HttpRequest, HttpResponse> {
177+
#[inline]
178+
fn parse_request(
179+
raw_http_request: Vec<String>,
180+
mut buf_reader: BufReader<&TcpStream>,
181+
) -> Result<HttpRequest, HttpResponse> {
125182
let params = &raw_http_request[0];
183+
let headers = raw_http_request[1..raw_http_request.len()].to_vec();
184+
let header_map = HeaderMap::from_vec(headers);
126185

127186
if params.is_empty() {
128187
return Err(HttpResponse::bad_request(
@@ -152,11 +211,77 @@ fn parse_request(raw_http_request: Vec<String>) -> Result<HttpRequest, HttpRespo
152211
}
153212
};
154213

214+
let mut body_values: HashMap<String, Value> = HashMap::new();
215+
216+
if let Some(content_length) = header_map.get("content-length") {
217+
let size: usize = match content_length.parse() {
218+
Ok(len) => len,
219+
Err(_) => {
220+
return Err(HttpResponse::bad_request(
221+
"Invalid content-length header".to_string(),
222+
HashMap::new(),
223+
))
224+
}
225+
};
226+
227+
let mut body = vec![0; size];
228+
if let Ok(_) = buf_reader.read_exact(&mut body) {
229+
if let Ok(json_value) = serde_json::from_slice::<serde_json::Value>(&body) {
230+
body_values.insert("body".to_string(), to_tucana_value(json_value));
231+
}
232+
}
233+
};
234+
235+
if path.contains("?") {
236+
let mut fields: HashMap<String, Value> = HashMap::new();
237+
if let Some((_, query)) = path.split_once("?") {
238+
let values = query.split("&");
239+
240+
for value in values {
241+
let mut parts = value.split("=");
242+
let key = match parts.next() {
243+
Some(key) => key.to_string(),
244+
None => continue,
245+
};
246+
247+
let value = match parts.next() {
248+
Some(value) => value.to_string(),
249+
None => continue,
250+
};
251+
252+
fields.insert(
253+
key,
254+
Value {
255+
kind: Some(Kind::StringValue(value)),
256+
},
257+
);
258+
}
259+
};
260+
261+
if !fields.is_empty() {
262+
let value = Value {
263+
kind: Some(Kind::StructValue(Struct { fields })),
264+
};
265+
266+
body_values.insert("query".to_string(), value);
267+
}
268+
};
269+
270+
let body = if !body_values.is_empty() {
271+
Some(Value {
272+
kind: Some(Kind::StructValue(Struct {
273+
fields: body_values,
274+
})),
275+
})
276+
} else {
277+
None
278+
};
279+
155280
Ok(HttpRequest {
156281
method,
157282
path: path.to_string(),
158283
version: version.to_string(),
159-
headers: raw_http_request.clone(),
160-
body: None,
284+
headers: header_map,
285+
body,
161286
})
162287
}

draco_rest/src/queue/mod.rs

Lines changed: 50 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ pub mod queue {
99
};
1010
use draco_validator::{resolver::flow_resolver::resolve_flow, verify_flow};
1111
use std::{collections::HashMap, sync::Arc, time::Duration};
12+
use tucana::shared::{Struct, Value};
1213

1314
fn create_rest_message(message_content: String) -> Message {
1415
Message {
@@ -28,14 +29,14 @@ pub mod queue {
2829
}
2930

3031
pub async fn handle_connection(
31-
request: HttpRequest,
32+
mut request: HttpRequest,
3233
flow_store: FlowStore,
3334
rabbitmq_client: Arc<RabbitmqClient>,
3435
) -> HttpResponse {
3536
// Check if a flow exists for the given settings
3637
let flow_exists = check_flow_exists(&flow_store, &request).await;
3738

38-
let flow = match flow_exists {
39+
let flow_result = match flow_exists {
3940
Some(flow) => flow,
4041
None => {
4142
return HttpResponse::not_found(
@@ -45,6 +46,53 @@ pub mod queue {
4546
}
4647
};
4748

49+
let flow = flow_result.flow;
50+
let regex_pattern = flow_result.regex_pattern;
51+
let mut url_params: HashMap<String, Value> = HashMap::new();
52+
53+
//Resolve url params
54+
let capture_keys = regex_pattern.capture_names();
55+
if let Some(captures) = regex_pattern.captures(&request.path) {
56+
for key_option in capture_keys {
57+
let key = match key_option {
58+
Some(key) => key,
59+
None => continue,
60+
};
61+
62+
let value = match captures.name(key) {
63+
Some(value) => value.as_str().to_string(),
64+
None => continue,
65+
};
66+
67+
let string_value = Value {
68+
kind: Some(tucana::shared::value::Kind::StringValue(value)),
69+
};
70+
71+
url_params.insert(key.to_string(), string_value);
72+
}
73+
}
74+
75+
//Will add the url params to the request body
76+
if !url_params.is_empty() {
77+
if let Some(body) = &mut request.body {
78+
if let Some(kind) = &mut body.kind {
79+
match kind {
80+
tucana::shared::value::Kind::StructValue(struct_value) => {
81+
struct_value.fields.insert(
82+
"url".to_string(),
83+
Value {
84+
kind: Some(tucana::shared::value::Kind::StructValue(Struct {
85+
fields: url_params,
86+
})),
87+
},
88+
);
89+
}
90+
_ => {}
91+
}
92+
}
93+
}
94+
}
95+
4896
// Determine which flow to use based on request body
4997
let flow_to_use = if let Some(body) = &request.body {
5098
// Verify flow

0 commit comments

Comments
 (0)