Skip to content

Commit f36cc2d

Browse files
author
Olivier Auverlot
committed
get_path_segment and is_path_matching sqlpage functions
1 parent 298d86c commit f36cc2d

2 files changed

Lines changed: 165 additions & 0 deletions

File tree

examples/official-site/sqlpage/migrations/08_functions.sql

Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -478,3 +478,114 @@ VALUES (
478478
'The string to encode.',
479479
'TEXT'
480480
);
481+
INSERT INTO sqlpage_functions (
482+
"name",
483+
"introduced_in_version",
484+
"icon",
485+
"description_md"
486+
)
487+
VALUES (
488+
'get_path_segment',
489+
'0.44',
490+
'cut',
491+
'Returns the Nth segment of a path.
492+
493+
### Example
494+
495+
#### Get the user id from the path ''/api/v1/user/42''
496+
497+
```sql
498+
select ''text'' AS component;
499+
select sqlpage.get_path_segment(''/api/v1/user/42'',4) AS contents;
500+
```
501+
502+
#### Result
503+
504+
`42`
505+
506+
#### Notes
507+
508+
- Segments are separated by ''/''.
509+
- If the path is NULL, or the index is out of bounds, it will return an empty string.
510+
- The index is 1-based, so the first segment is at index 1.
511+
'
512+
);
513+
INSERT INTO sqlpage_function_parameters (
514+
"function",
515+
"index",
516+
"name",
517+
"description_md",
518+
"type"
519+
)
520+
VALUES (
521+
'get_path_segment',
522+
1,
523+
'path',
524+
'The path to extract the segment from.',
525+
'TEXT'
526+
),
527+
(
528+
'get_path_segment',
529+
2,
530+
'index',
531+
'The index of the segment to extract. 1-based.',
532+
'INTEGER'
533+
);
534+
INSERT INTO sqlpage_functions (
535+
"name",
536+
"introduced_in_version",
537+
"icon",
538+
"description_md"
539+
)
540+
VALUES (
541+
'is_path_matching',
542+
'0.44',
543+
'flip-horizontal',
544+
'Returns the path if it matches the pattern, otherwise returns an empty string.
545+
546+
### Example
547+
548+
#### Check if the current path matches a pattern
549+
550+
```sql
551+
select ''text'' AS component;
552+
select sqlpage.is_path_matching(sqlpage.path(),''/api/%/%'') AS contents;
553+
```
554+
555+
#### Result
556+
557+
`/api/v1/user/42`
558+
559+
#### Notes
560+
561+
- The pattern is a list of segments separated by ''/''.
562+
- If the path is NULL, or the pattern is NULL, it will return an empty string.
563+
- If the path and pattern have different numbers of segments, it will return an empty string.
564+
- If the path and pattern have the same number of segments, it will compare them segment by segment.
565+
- If a segment in the pattern is ''%'', it will match any non-empty segment in the path.
566+
- If a segment in the pattern is a string, it will match the corresponding segment in the path if they are equal.
567+
- If all segments match, it will return the path.
568+
- Otherwise, it will return an empty string.
569+
'
570+
);
571+
INSERT INTO sqlpage_function_parameters (
572+
"function",
573+
"index",
574+
"name",
575+
"description_md",
576+
"type"
577+
)
578+
VALUES (
579+
'is_path_matching',
580+
1,
581+
'path',
582+
'The path to match against.',
583+
'TEXT'
584+
),
585+
(
586+
'is_path_matching',
587+
2,
588+
'pattern',
589+
'The template pattern.',
590+
'TEXT'
591+
);

src/webserver/database/sqlpage_functions/functions.rs

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,9 @@ super::function_definition_macro::sqlpage_functions! {
3030
environment_variable(name: Cow<str>);
3131
exec((&RequestInfo), program_name: Cow<str>, args: Vec<Cow<str>>);
3232

33+
get_path_segment(path: Option<Cow<str>>, index: SqlPageFunctionParam<usize>);
34+
is_path_matching(path: Option<Cow<str>>, pattern: Option<Cow<str>>);
35+
3336
fetch((&RequestInfo), http_request: Option<SqlPageFunctionParam<HttpFetchRequest<'_>>>);
3437
fetch_with_meta((&RequestInfo), http_request: Option<SqlPageFunctionParam<HttpFetchRequest<'_>>>);
3538

@@ -809,6 +812,57 @@ async fn url_encode(raw_text: Option<Cow<'_, str>>) -> Option<Cow<'_, str>> {
809812
})
810813
}
811814

815+
/// Returns the path if it matches the pattern, otherwise returns an empty string.
816+
async fn is_path_matching<'a>(
817+
path: Option<Cow<'a, str>>,
818+
pattern: Option<Cow<'_, str>>,
819+
) -> Option<Cow<'a, str>> {
820+
let (Some(p_val), Some(pattern)) = (path.as_ref(), pattern) else {
821+
return Some(Cow::Borrowed(""));
822+
};
823+
824+
let path_segments: Vec<&str> = p_val.split('/').collect();
825+
let pattern_segments: Vec<&str> = pattern.split('/').collect();
826+
827+
if path_segments.len() != pattern_segments.len() {
828+
return Some(Cow::Borrowed(""));
829+
}
830+
831+
for (ps, pat_s) in path_segments.iter().zip(pattern_segments.iter()) {
832+
if *pat_s == "%" {
833+
if ps.is_empty() {
834+
return Some(Cow::Borrowed(""));
835+
}
836+
} else {
837+
let ps_decoded = percent_encoding::percent_decode_str(ps).decode_utf8_lossy();
838+
let pat_s_decoded = percent_encoding::percent_decode_str(pat_s).decode_utf8_lossy();
839+
if ps_decoded != pat_s_decoded {
840+
return Some(Cow::Borrowed(""));
841+
}
842+
}
843+
}
844+
845+
path
846+
}
847+
848+
/// Returns the Nth segment of a path. Segments are separated by '/'.
849+
async fn get_path_segment(path: Option<Cow<'_, str>>, index: usize) -> String {
850+
let Some(path) = path else {
851+
return String::new();
852+
};
853+
if index == 0 {
854+
return String::new();
855+
}
856+
let segment = path
857+
.trim_start_matches('/')
858+
.split('/')
859+
.nth(index - 1)
860+
.unwrap_or_default();
861+
percent_encoding::percent_decode_str(segment)
862+
.decode_utf8_lossy()
863+
.into_owned()
864+
}
865+
812866
/// Returns all variables in the request as a JSON object.
813867
async fn variables<'a>(
814868
request: &'a ExecutionContext,

0 commit comments

Comments
 (0)