Skip to content

Commit f1f3271

Browse files
committed
ref: major refactoring
1 parent 1264ce3 commit f1f3271

7 files changed

Lines changed: 570 additions & 144 deletions

File tree

Cargo.lock

Lines changed: 3 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

adapter/rest/Cargo.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@ base = { workspace = true }
1515
anyhow = { workspace = true }
1616
base64 = "0.22.1"
1717
ring = "0.17.14"
18+
form_urlencoded = "1.2.1"
19+
percent-encoding = "2.3.1"
1820
hyper-util = "0.1.19"
1921
hyper = "1.8.1"
2022
http-body-util = "0.1.3"

adapter/rest/src/main.rs

Lines changed: 3 additions & 138 deletions
Original file line numberDiff line numberDiff line change
@@ -1,32 +1,20 @@
11
use base::{
22
runner::{ServerContext, ServerRunner},
3-
store::{FlowExecutionResult, FlowIdentifyResult},
43
traits::Server as ServerTrait,
54
};
65
use code0_flow::flow_service::ModuleDefinitionAppendix;
7-
use http_body_util::{BodyExt, Full};
86
use hyper::server::conn::http1;
9-
use hyper::{Request, Response};
10-
use hyper::{
11-
StatusCode,
12-
body::{Bytes, Incoming},
13-
};
147
use hyper_util::rt::TokioIo;
15-
use std::convert::Infallible;
168
use std::net::SocketAddr;
179
use std::sync::Arc;
1810
use tokio::net::TcpListener;
1911
use tonic::async_trait;
20-
use tucana::shared::{
21-
Endpoint, ModuleDefinition, Struct, ValidationFlow, Value, helper::value::ToValue, value::Kind,
22-
};
23-
24-
use crate::auth::{authenticate_header_name, validate_flow_auth};
25-
use crate::response::{error_to_http_response, value_to_http_response};
12+
use tucana::shared::{Endpoint, ModuleDefinition};
2613

2714
mod auth;
2815
mod config;
2916
mod content_type;
17+
mod request;
3018
mod response;
3119
mod route;
3220

@@ -69,129 +57,6 @@ struct HttpServer {
6957
addr: Option<SocketAddr>,
7058
}
7159

72-
async fn execute_flow_to_hyper_response(
73-
flow: ValidationFlow,
74-
body: Value,
75-
store: Arc<base::store::AdapterStore>,
76-
) -> Response<Full<Bytes>> {
77-
match store.execute_flow_with_emitter(flow, Some(body)).await {
78-
FlowExecutionResult::Ongoing(result) => {
79-
log::debug!("Received first ongoing response from emitter");
80-
value_to_http_response(result)
81-
}
82-
FlowExecutionResult::Failed => {
83-
log::error!("Flow execution failed event received from emitter");
84-
error_to_http_response(StatusCode::INTERNAL_SERVER_ERROR, "Internal server error")
85-
}
86-
FlowExecutionResult::FinishedWithoutOngoing => Response::builder()
87-
.status(StatusCode::NO_CONTENT)
88-
.body(Full::new(Bytes::new()))
89-
.unwrap(),
90-
FlowExecutionResult::TransportError => {
91-
log::error!("Flow execution transport error");
92-
error_to_http_response(StatusCode::INTERNAL_SERVER_ERROR, "Internal server error")
93-
}
94-
}
95-
}
96-
97-
pub async fn handle_request(
98-
req: Request<Incoming>,
99-
store: Arc<base::store::AdapterStore>,
100-
) -> Result<Response<Full<Bytes>>, Infallible> {
101-
let method = req.method().clone();
102-
let path = req.uri().path().to_string();
103-
let headers = req.headers().clone();
104-
105-
// Read full body
106-
let body_bytes = match BodyExt::collect(req.into_body()).await {
107-
Ok(collected) => collected.to_bytes().to_vec(),
108-
Err(err) => {
109-
log::error!("Failed to read request body: {}", err);
110-
return Ok(error_to_http_response(
111-
StatusCode::BAD_REQUEST,
112-
"Failed to read request body",
113-
));
114-
}
115-
};
116-
117-
// slug matching
118-
let Some(slug) = route::extract_slug_from_path(&path) else {
119-
return Ok(error_to_http_response(
120-
StatusCode::BAD_REQUEST,
121-
"Missing slug in path",
122-
));
123-
};
124-
125-
let pattern = format!("REST.{}.*", slug);
126-
let route = route::RequestRoute {
127-
url: path.clone(),
128-
method,
129-
};
130-
131-
let resp = match store.get_possible_flow_match(pattern, route).await {
132-
FlowIdentifyResult::Single(flow) => {
133-
if let Err(err) = validate_flow_auth(&flow, &headers) {
134-
let mut response = error_to_http_response(err.status_code(), err.message());
135-
response
136-
.headers_mut()
137-
.insert(authenticate_header_name(), err.challenge());
138-
return Ok(response);
139-
}
140-
141-
let request_body_value =
142-
match content_type::parse_body_from_headers(&headers, &body_bytes) {
143-
Ok(value) => value,
144-
Err(err) => {
145-
log::warn!("Failed to parse request body: {}", err);
146-
let status_code = match err {
147-
content_type::BodyParseError::UnsupportedContentType { .. } => {
148-
StatusCode::UNSUPPORTED_MEDIA_TYPE
149-
}
150-
_ => StatusCode::BAD_REQUEST,
151-
};
152-
153-
return Ok(error_to_http_response(status_code, &err.to_string()));
154-
}
155-
};
156-
157-
let mut header_fields = std::collections::HashMap::new();
158-
let mut fields = std::collections::HashMap::new();
159-
160-
for (name, value) in headers.iter() {
161-
let key = name.as_str().to_owned();
162-
let value_str = value
163-
.to_str()
164-
.map(str::to_owned)
165-
.unwrap_or_else(|_| String::from_utf8_lossy(value.as_bytes()).into_owned());
166-
167-
header_fields.insert(key, value_str.to_value());
168-
}
169-
170-
if let Some(v) = request_body_value {
171-
fields.insert(String::from("payload"), v);
172-
};
173-
174-
fields.insert(
175-
String::from("headers"),
176-
Value {
177-
kind: Some(Kind::StructValue(Struct {
178-
fields: header_fields,
179-
})),
180-
},
181-
);
182-
183-
let input = Value {
184-
kind: Some(Kind::StructValue(Struct { fields })),
185-
};
186-
187-
execute_flow_to_hyper_response(flow, input, store).await
188-
}
189-
_ => error_to_http_response(StatusCode::NOT_FOUND, "No flow found for path"),
190-
};
191-
192-
Ok(resp)
193-
}
194-
19560
#[async_trait]
19661
impl ServerTrait<config::HttpServerConfig> for HttpServer {
19762
async fn init(&mut self, ctx: &ServerContext<config::HttpServerConfig>) -> anyhow::Result<()> {
@@ -247,7 +112,7 @@ impl ServerTrait<config::HttpServerConfig> for HttpServer {
247112
tokio::spawn(async move {
248113
let svc = hyper::service::service_fn(move |req| {
249114
let store = Arc::clone(&store);
250-
async move { handle_request(req, store).await }
115+
async move { request::handle(req, store).await }
251116
});
252117

253118
let conn = http1::Builder::new().serve_connection(io, svc);

adapter/rest/src/request/input.rs

Lines changed: 167 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,167 @@
1+
use hyper::{HeaderMap, header::HeaderValue};
2+
use std::collections::HashMap;
3+
use tucana::shared::{Struct, ValidationFlow, Value, helper::value::ToValue, value::Kind};
4+
5+
use crate::route;
6+
7+
pub(super) fn build_flow_input(
8+
flow: &ValidationFlow,
9+
path: &str,
10+
query: Option<&str>,
11+
headers: &HeaderMap<HeaderValue>,
12+
payload: Option<Value>,
13+
) -> Value {
14+
let mut fields = HashMap::new();
15+
16+
if let Some(payload) = payload {
17+
fields.insert(String::from("payload"), payload);
18+
}
19+
20+
fields.insert(
21+
String::from("headers"),
22+
string_map_to_value(header_map(headers)),
23+
);
24+
fields.insert(
25+
String::from("query_params"),
26+
string_map_to_value(query_params(query)),
27+
);
28+
fields.insert(
29+
String::from("path_params"),
30+
string_map_to_value(route::extract_path_params(flow, path)),
31+
);
32+
33+
Value {
34+
kind: Some(Kind::StructValue(Struct { fields })),
35+
}
36+
}
37+
38+
fn header_map(headers: &HeaderMap<HeaderValue>) -> HashMap<String, String> {
39+
headers
40+
.iter()
41+
.map(|(name, value)| {
42+
let value = value
43+
.to_str()
44+
.map(str::to_owned)
45+
.unwrap_or_else(|_| String::from_utf8_lossy(value.as_bytes()).into_owned());
46+
47+
(name.as_str().to_owned(), value)
48+
})
49+
.collect()
50+
}
51+
52+
fn query_params(query: Option<&str>) -> HashMap<String, String> {
53+
let Some(query) = query else {
54+
return HashMap::new();
55+
};
56+
57+
// Repeated query keys currently use last-write-wins because Taurus receives
58+
// a simple object here, not a multi-map.
59+
form_urlencoded::parse(query.as_bytes())
60+
.map(|(key, value)| (key.into_owned(), value.into_owned()))
61+
.collect()
62+
}
63+
64+
fn string_map_to_value(map: HashMap<String, String>) -> Value {
65+
Value {
66+
kind: Some(Kind::StructValue(Struct {
67+
fields: map
68+
.into_iter()
69+
.map(|(key, value)| (key, value.to_value()))
70+
.collect(),
71+
})),
72+
}
73+
}
74+
75+
#[cfg(test)]
76+
mod tests {
77+
use super::{build_flow_input, query_params, string_map_to_value};
78+
use hyper::HeaderMap;
79+
use tucana::shared::{FlowSetting, Struct, ValidationFlow, Value, value::Kind};
80+
81+
#[test]
82+
fn query_params_are_percent_decoded() {
83+
let params = query_params(Some("search=hello+world&tag=a%2Fb&empty="));
84+
85+
assert_eq!(
86+
params.get("search").map(String::as_str),
87+
Some("hello world")
88+
);
89+
assert_eq!(params.get("tag").map(String::as_str), Some("a/b"));
90+
assert_eq!(params.get("empty").map(String::as_str), Some(""));
91+
}
92+
93+
#[test]
94+
fn string_map_is_converted_to_struct_value() {
95+
let value =
96+
string_map_to_value([("id".to_string(), "42".to_string())].into_iter().collect());
97+
98+
let Value {
99+
kind: Some(Kind::StructValue(Struct { fields })),
100+
} = value
101+
else {
102+
panic!("expected struct value");
103+
};
104+
105+
assert_eq!(
106+
fields.get("id").and_then(|value| value.kind.as_ref()),
107+
Some(&Kind::StringValue("42".to_string()))
108+
);
109+
}
110+
111+
#[test]
112+
fn flow_input_contains_query_and_path_params() {
113+
let flow = ValidationFlow {
114+
flow_id: 1,
115+
project_slug: "project".to_string(),
116+
settings: vec![FlowSetting {
117+
database_id: None,
118+
flow_setting_id: "httpURL".to_string(),
119+
value: Some(Value {
120+
kind: Some(Kind::StringValue("/users/:user_id".to_string())),
121+
}),
122+
cast: None,
123+
}],
124+
..ValidationFlow::default()
125+
};
126+
127+
let input = build_flow_input(
128+
&flow,
129+
"/project/users/42",
130+
Some("search=hello+world"),
131+
&HeaderMap::new(),
132+
None,
133+
);
134+
135+
assert_eq!(
136+
nested_string_field(&input, "query_params", "search"),
137+
Some("hello world")
138+
);
139+
assert_eq!(
140+
nested_string_field(&input, "path_params", "user_id"),
141+
Some("42")
142+
);
143+
}
144+
145+
fn nested_string_field<'a>(value: &'a Value, field: &str, nested: &str) -> Option<&'a str> {
146+
let Some(Kind::StructValue(Struct { fields })) = value.kind.as_ref() else {
147+
return None;
148+
};
149+
let Some(Value {
150+
kind:
151+
Some(Kind::StructValue(Struct {
152+
fields: nested_fields,
153+
})),
154+
}) = fields.get(field)
155+
else {
156+
return None;
157+
};
158+
let Some(Value {
159+
kind: Some(Kind::StringValue(value)),
160+
}) = nested_fields.get(nested)
161+
else {
162+
return None;
163+
};
164+
165+
Some(value.as_str())
166+
}
167+
}

0 commit comments

Comments
 (0)