Skip to content

Commit d2dd70f

Browse files
authored
Support validate path params in path block (#23)
1 parent 5e28302 commit d2dd70f

File tree

5 files changed

+140
-19
lines changed

5 files changed

+140
-19
lines changed

aiscript-runtime/src/ast/mod.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,7 @@ pub struct Endpoint {
8181
pub path_specs: Vec<PathSpec>,
8282
#[allow(unused)]
8383
pub return_type: Option<String>,
84+
pub path: Vec<Field>,
8485
pub query: Vec<Field>,
8586
pub body: RequestBody,
8687
pub statements: String,

aiscript-runtime/src/endpoint.rs

Lines changed: 101 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ use aiscript_vm::{ReturnValue, Vm, VmError};
33
use axum::{
44
Form, Json, RequestExt,
55
body::Body,
6-
extract::{self, FromRequest, Request},
6+
extract::{self, FromRequest, RawPathParams, Request},
77
http::{HeaderName, HeaderValue},
88
response::{IntoResponse, Response},
99
};
@@ -57,6 +57,7 @@ pub struct Field {
5757
#[derive(Clone)]
5858
pub struct Endpoint {
5959
pub annotation: RouteAnnotation,
60+
pub path_params: Vec<Field>,
6061
pub query_params: Vec<Field>,
6162
pub body_type: BodyKind,
6263
pub body_fields: Vec<Field>,
@@ -70,6 +71,7 @@ pub struct Endpoint {
7071

7172
enum ProcessingState {
7273
ValidatingAuth,
74+
ValidatingPath,
7375
ValidatingQuery,
7476
ValidatingBody,
7577
Executing(JoinHandle<Result<ReturnValue, VmError>>),
@@ -79,6 +81,7 @@ pub struct RequestProcessor {
7981
endpoint: Endpoint,
8082
request: Request<Body>,
8183
jwt_claim: Option<Value>,
84+
path_data: HashMap<String, Value>,
8285
query_data: HashMap<String, Value>,
8386
body_data: HashMap<String, Value>,
8487
state: ProcessingState,
@@ -89,12 +92,13 @@ impl RequestProcessor {
8992
let state = if endpoint.annotation.is_auth_required() {
9093
ProcessingState::ValidatingAuth
9194
} else {
92-
ProcessingState::ValidatingQuery
95+
ProcessingState::ValidatingPath
9396
};
9497
Self {
9598
endpoint,
9699
request,
97100
jwt_claim: None,
101+
path_data: HashMap::new(),
98102
query_data: HashMap::new(),
99103
body_data: HashMap::new(),
100104
state,
@@ -307,6 +311,99 @@ impl Future for RequestProcessor {
307311
}
308312
}
309313
}
314+
self.state = ProcessingState::ValidatingPath;
315+
}
316+
ProcessingState::ValidatingPath => {
317+
let raw_path_params = {
318+
// Extract path parameters using Axum's RawPathParams extractor
319+
let future = self.request.extract_parts::<RawPathParams>();
320+
321+
tokio::pin!(future);
322+
match future.poll(cx) {
323+
Poll::Pending => return Poll::Pending,
324+
Poll::Ready(Ok(params)) => params,
325+
Poll::Ready(Err(e)) => {
326+
return Poll::Ready(Ok(format!(
327+
"Failed to extract path parameters: {}",
328+
e
329+
)
330+
.into_response()));
331+
}
332+
}
333+
};
334+
335+
// Process and validate each path parameter
336+
for (param_name, param_value) in &raw_path_params {
337+
// Find the corresponding path parameter field
338+
if let Some(field) = self
339+
.endpoint
340+
.path_params
341+
.iter()
342+
.find(|f| f.name == param_name)
343+
{
344+
// Convert the value to the appropriate type based on the field definition
345+
let value = match field.field_type {
346+
FieldType::Str => Value::String(param_value.to_string()),
347+
FieldType::Number => {
348+
if let Ok(num) = param_value.parse::<i64>() {
349+
Value::Number(num.into())
350+
} else if let Ok(float) = param_value.parse::<f64>() {
351+
match serde_json::Number::from_f64(float) {
352+
Some(n) => Value::Number(n),
353+
None => {
354+
return Poll::Ready(Ok(
355+
format!("Invalid path parameter type for {}: could not convert to number", param_name)
356+
.into_response()
357+
));
358+
}
359+
}
360+
} else {
361+
return Poll::Ready(Ok(format!(
362+
"Invalid path parameter type for {}: expected a number",
363+
param_name
364+
)
365+
.into_response()));
366+
}
367+
}
368+
FieldType::Bool => match param_value.to_lowercase().as_str() {
369+
"true" => Value::Bool(true),
370+
"false" => Value::Bool(false),
371+
_ => {
372+
return Poll::Ready(Ok(
373+
format!("Invalid path parameter type for {}: expected a boolean", param_name)
374+
.into_response()
375+
));
376+
}
377+
},
378+
_ => {
379+
return Poll::Ready(Ok(format!(
380+
"Unsupported path parameter type for {}",
381+
param_name
382+
)
383+
.into_response()));
384+
}
385+
};
386+
387+
// Validate the value using our existing validation infrastructure
388+
if let Err(e) = Self::validate_field(field, &value) {
389+
return Poll::Ready(Ok(e.into_response()));
390+
}
391+
392+
// Store the validated parameter
393+
self.path_data.insert(param_name.to_string(), value);
394+
}
395+
}
396+
397+
// Check for missing required parameters
398+
for field in &self.endpoint.path_params {
399+
if !self.path_data.contains_key(&field.name) && field.required {
400+
return Poll::Ready(Ok(
401+
ServerError::MissingField(field.name.clone()).into_response()
402+
));
403+
}
404+
}
405+
406+
// Move to the next state
310407
self.state = ProcessingState::ValidatingQuery;
311408
}
312409
ProcessingState::ValidatingQuery => {
@@ -400,6 +497,7 @@ impl Future for RequestProcessor {
400497
} else {
401498
None
402499
};
500+
let path_data = mem::take(&mut self.path_data);
403501
let query_data = mem::take(&mut self.query_data);
404502
let body_data = mem::take(&mut self.body_data);
405503
let pg_connection = self.endpoint.pg_connection.clone();
@@ -417,6 +515,7 @@ impl Future for RequestProcessor {
417515
vm.eval_function(
418516
0,
419517
&[
518+
Value::Object(path_data.into_iter().collect()),
420519
Value::Object(query_data.into_iter().collect()),
421520
Value::Object(body_data.into_iter().collect()),
422521
Value::Object(

aiscript-runtime/src/lib.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -210,6 +210,7 @@ async fn run_server(
210210
for endpoint_spec in route.endpoints {
211211
let endpoint = Endpoint {
212212
annotation: endpoint_spec.annotation.or(&route.annotation),
213+
path_params: endpoint_spec.path.into_iter().map(convert_field).collect(),
213214
query_params: endpoint_spec.query.into_iter().map(convert_field).collect(),
214215
body_type: endpoint_spec.body.kind,
215216
body_fields: endpoint_spec

aiscript-runtime/src/parser.rs

Lines changed: 21 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,7 @@ impl<'a> Parser<'a> {
8787
let docs = self.parse_docs();
8888

8989
// Parse structured parts (query and body)
90+
let mut path = Vec::new();
9091
let mut query = Vec::new();
9192
let mut body = RequestBody::default();
9293

@@ -98,6 +99,9 @@ impl<'a> Parser<'a> {
9899
} else if self.scanner.check_identifier("body") {
99100
self.advance();
100101
body.fields = self.parse_fields()?;
102+
} else if self.scanner.check_identifier("path") {
103+
self.advance();
104+
path = self.parse_fields()?;
101105
} else if self.scanner.check(TokenType::At) {
102106
let directives = DirectiveParser::new(&mut self.scanner).parse_directives();
103107
for directive in directives {
@@ -126,12 +130,16 @@ impl<'a> Parser<'a> {
126130
}
127131
// Parse the handler function body
128132
let script = self.read_raw_script()?;
129-
let statements = format!("ai fn handler(query, body, request, header){{{}}}", script);
133+
let statements = format!(
134+
"ai fn handler(path, query, body, request, header){{{}}}",
135+
script
136+
);
130137
self.consume(TokenType::CloseBrace, "Expect '}' after endpoint")?;
131138
Ok(Endpoint {
132139
annotation,
133140
path_specs,
134141
return_type: None,
142+
path,
135143
query,
136144
body,
137145
statements,
@@ -304,30 +312,26 @@ impl<'a> Parser<'a> {
304312
path.push('/');
305313
self.advance();
306314
}
307-
TokenType::Less => {
308-
self.advance(); // Consume <
315+
TokenType::Colon => {
316+
self.advance(); // Consume :
309317

310318
// Parse parameter name
311319
if !self.check(TokenType::Identifier) {
312-
return Err("Expected parameter name".to_string());
320+
return Err("Expected parameter name after ':'".to_string());
313321
}
314322
let name = self.current.lexeme.to_string();
315323
self.advance();
316324

317-
self.consume(TokenType::Colon, "Expected ':' after parameter name")?;
318-
319-
// Parse parameter type
320-
if !self.check(TokenType::Identifier) {
321-
return Err("Expected parameter type".to_string());
322-
}
323-
let param_type = self.current.lexeme.to_string();
324-
self.advance();
325-
326-
self.consume(TokenType::Greater, "Expected '>' after parameter type")?;
327-
328-
path.push(':');
325+
// Add parameter to path in the format Axum expects: {id}
326+
path.push('{');
329327
path.push_str(&name);
330-
params.push(PathParameter { name, param_type });
328+
path.push('}');
329+
330+
// Add parameter to our list
331+
params.push(PathParameter {
332+
name,
333+
param_type: "str".to_string(), // Default type, will be overridden by path block
334+
});
331335
}
332336
TokenType::Identifier => {
333337
path.push_str(self.current.lexeme);

examples/routes/path.ai

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
get /users/:id/posts/:postId {
2+
path {
3+
@string(min_len=3)
4+
id: str,
5+
postId: int,
6+
}
7+
8+
query {
9+
refresh: bool = true
10+
}
11+
12+
print(path);
13+
let userId = path.id;
14+
let postId = path.postId;
15+
return f"Accessing post {postId} for user {userId}";
16+
}

0 commit comments

Comments
 (0)