diff --git a/examples/official-site/sqlpage/migrations/08_functions.sql b/examples/official-site/sqlpage/migrations/08_functions.sql index 3a5fd4a4..0a8b4442 100644 --- a/examples/official-site/sqlpage/migrations/08_functions.sql +++ b/examples/official-site/sqlpage/migrations/08_functions.sql @@ -478,3 +478,114 @@ VALUES ( 'The string to encode.', 'TEXT' ); +INSERT INTO sqlpage_functions ( + "name", + "introduced_in_version", + "icon", + "description_md" + ) +VALUES ( + 'get_path_segment', + '0.44', + 'cut', + 'Returns the Nth segment of a path. + +### Example + +#### Get the user id from the path ''/api/v1/user/42'' + +```sql +select ''text'' AS component; +select sqlpage.get_path_segment(''/api/v1/user/42'',4) AS contents; +``` + +#### Result + +`42` + +#### Notes + +- Segments are separated by ''/''. +- If the path is NULL, or the index is out of bounds, it will return an empty string. +- The index is 1-based, so the first segment is at index 1. +' + ); +INSERT INTO sqlpage_function_parameters ( + "function", + "index", + "name", + "description_md", + "type" + ) +VALUES ( + 'get_path_segment', + 1, + 'path', + 'The path to extract the segment from.', + 'TEXT' + ), + ( + 'get_path_segment', + 2, + 'index', + 'The index of the segment to extract. 1-based.', + 'INTEGER' + ); +INSERT INTO sqlpage_functions ( + "name", + "introduced_in_version", + "icon", + "description_md" + ) +VALUES ( + 'is_path_matching', + '0.44', + 'flip-horizontal', + 'Returns the path if it matches the pattern, otherwise returns an empty string. + +### Example + +#### Check if the current path matches a pattern + +```sql +select ''text'' AS component; +select sqlpage.is_path_matching(sqlpage.path(),''/api/v1/user/%d/%s'') AS contents; +``` + +#### Result + +`/api/v1/user/42/name` + +#### Notes + +- The pattern is a list of segments separated by ''/''. +- If the path is NULL, or the pattern is NULL, it will return an empty string. +- If the path and pattern have different numbers of segments, it will return an empty string. +- If the path and pattern have the same number of segments, it will compare them segment by segment. +- If a segment in the pattern is ''%d'', it will match any non-empty segment that is an integer. +- If a segment in the pattern is ''%s'', it will match any non-empty segment in the path. +- If all segments match, it will return the path. +- Otherwise, it will return an empty string. +' + ); +INSERT INTO sqlpage_function_parameters ( + "function", + "index", + "name", + "description_md", + "type" + ) +VALUES ( + 'is_path_matching', + 1, + 'path', + 'The path to match against.', + 'TEXT' + ), + ( + 'is_path_matching', + 2, + 'pattern', + 'The template pattern.', + 'TEXT' + ); \ No newline at end of file diff --git a/src/webserver/database/sqlpage_functions/functions.rs b/src/webserver/database/sqlpage_functions/functions.rs index b78f64e0..66d08f04 100644 --- a/src/webserver/database/sqlpage_functions/functions.rs +++ b/src/webserver/database/sqlpage_functions/functions.rs @@ -30,6 +30,9 @@ super::function_definition_macro::sqlpage_functions! { environment_variable(name: Cow); exec((&RequestInfo), program_name: Cow, args: Vec>); + get_path_segment(path: Option>, index: SqlPageFunctionParam); + is_path_matching(path: Option>, pattern: Option>); + fetch((&RequestInfo), http_request: Option>>); fetch_with_meta((&RequestInfo), http_request: Option>>); @@ -809,6 +812,62 @@ async fn url_encode(raw_text: Option>) -> Option> { }) } +/// Returns the path if it matches the pattern, otherwise returns an empty string. +async fn is_path_matching<'a>( + path: Option>, + pattern: Option>, +) -> Option> { + let (Some(p_val), Some(pattern)) = (path.as_ref(), pattern) else { + return Some(Cow::Borrowed("")); + }; + + let path_segments: Vec<&str> = p_val.split('/').collect(); + let pattern_segments: Vec<&str> = pattern.split('/').collect(); + + if path_segments.len() != pattern_segments.len() { + return Some(Cow::Borrowed("")); + } + + for (ps, pat_s) in path_segments.iter().zip(pattern_segments.iter()) { + if *pat_s == "%s" { + if ps.is_empty() { + return Some(Cow::Borrowed("")); + } + } else if *pat_s == "%d" { + let ps_decoded = percent_encoding::percent_decode_str(ps).decode_utf8_lossy(); + if ps_decoded.is_empty() || ps_decoded.parse::().is_err() { + return Some(Cow::Borrowed("")); + } + } else { + let ps_decoded = percent_encoding::percent_decode_str(ps).decode_utf8_lossy(); + let pat_s_decoded = percent_encoding::percent_decode_str(pat_s).decode_utf8_lossy(); + if ps_decoded != pat_s_decoded { + return Some(Cow::Borrowed("")); + } + } + } + + path +} + +/// Returns the Nth segment of a path. Segments are separated by '/'. +async fn get_path_segment(path: Option>, index: usize) -> String { + let Some(path) = path else { + return String::new(); + }; + if index == 0 { + return String::new(); + } + let segment = path + .trim_start_matches('/') + .split('/') + .nth(index - 1) + .unwrap_or_default(); + percent_encoding::percent_decode_str(segment) + .decode_utf8_lossy() + .into_owned() +} + /// Returns all variables in the request as a JSON object. async fn variables<'a>( request: &'a ExecutionContext,