Skip to content

Commit 49ee945

Browse files
committed
new sqlpage.link function
1 parent 9c47ec9 commit 49ee945

File tree

4 files changed

+120
-1
lines changed

4 files changed

+120
-1
lines changed

CHANGELOG.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,14 @@
2727
- Updated apexcharts.js to [v3.50.0](https://github.com/apexcharts/apexcharts.js/releases/tag/v3.50.0)
2828
- Improve truncation of long page titles
2929
- ![screenshot long title](https://github.com/lovasoa/SQLpage/assets/552629/9859023e-c706-47b3-aa9e-1c613046fdfa)
30+
- new function: [`sqlpage.link`](https://sql.ophir.dev/functions.sql?function=link#function) to easily create links with parameters between pages. For instance, you can now use
31+
```sql
32+
select 'list' as component;
33+
select
34+
product_name as title,
35+
sqlpage.link('product.sql', json_object('product_id', product_id)) as link
36+
from products;
37+
```
3038

3139
## 0.24.0 (2024-06-23)
3240
- in the form component, searchable `select` fields now support more than 50 options. They used to display only the first 50 options.

src/webserver/database/sqlpage_functions/functions.rs

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,10 @@
11
use super::RequestInfo;
22
use crate::webserver::{
3-
database::execute_queries::DbConn, http::SingleOrVec, request_variables::ParamMap,
3+
database::{
4+
execute_queries::DbConn, sqlpage_functions::url_parameter_deserializer::URLParameters,
5+
},
6+
http::SingleOrVec,
7+
request_variables::ParamMap,
48
ErrorWithStatus,
59
};
610
use anyhow::{anyhow, Context};
@@ -23,6 +27,8 @@ super::function_definition_macro::sqlpage_functions! {
2327
hash_password(password: Option<String>);
2428
header((&RequestInfo), name: Cow<str>);
2529

30+
link(file: Cow<str>, parameters: Cow<str>);
31+
2632
path((&RequestInfo));
2733
persist_uploaded_file((&RequestInfo), field_name: Cow<str>, folder: Option<Cow<str>>, allowed_extensions: Option<Cow<str>>);
2834
protocol((&RequestInfo));
@@ -188,6 +194,19 @@ async fn header<'a>(request: &'a RequestInfo, name: Cow<'a, str>) -> Option<Cow<
188194
request.headers.get(&*name).map(SingleOrVec::as_json_str)
189195
}
190196

197+
/// Builds a URL from a file name and a JSON object conatining URL parameters.
198+
/// For instance, if the file is "index.sql" and the parameters are {"x": "hello world"},
199+
/// the result will be "index.sql?x=hello%20world".
200+
async fn link<'a>(file: Cow<'a, str>, parameters: Option<Cow<'a, str>>) -> anyhow::Result<String> {
201+
let mut url = file.into_owned();
202+
if let Some(parameters) = parameters {
203+
url.push('?');
204+
let encoded = serde_json::from_str::<URLParameters>(&parameters)?;
205+
url.push_str(encoded.get());
206+
}
207+
Ok(url)
208+
}
209+
191210
/// Returns the path component of the URL of the current request.
192211
async fn path(request: &RequestInfo) -> &str {
193212
&request.path

src/webserver/database/sqlpage_functions/mod.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ mod function_definition_macro;
22
mod function_traits;
33
pub(super) mod functions;
44
mod http_fetch_request;
5+
mod url_parameter_deserializer;
56

67
use sqlparser::ast::FunctionArg;
78

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
use percent_encoding::{percent_encode, NON_ALPHANUMERIC};
2+
use serde::{Deserialize, Deserializer};
3+
use std::borrow::Cow;
4+
use std::fmt;
5+
6+
pub struct URLParameters(String);
7+
8+
impl URLParameters {
9+
fn encode_and_push(&mut self, v: &str) {
10+
let val: Cow<str> = percent_encode(v.as_bytes(), NON_ALPHANUMERIC).into();
11+
self.0.push_str(&val);
12+
}
13+
fn push_kv(&mut self, key: &str, value: &str) {
14+
if !self.0.is_empty() {
15+
self.0.push('&');
16+
}
17+
self.encode_and_push(key);
18+
self.0.push('=');
19+
self.encode_and_push(value);
20+
}
21+
pub fn get(&self) -> &str {
22+
&self.0
23+
}
24+
}
25+
26+
impl<'de> Deserialize<'de> for URLParameters {
27+
fn deserialize<D>(deserializer: D) -> Result<URLParameters, D::Error>
28+
where
29+
D: Deserializer<'de>,
30+
{
31+
// Visit an object and append keys and values to the string
32+
struct URLParametersVisitor;
33+
34+
impl<'de> serde::de::Visitor<'de> for URLParametersVisitor {
35+
type Value = URLParameters;
36+
37+
fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
38+
formatter.write_str("a sequence")
39+
}
40+
41+
fn visit_map<A>(self, mut map: A) -> Result<URLParameters, A::Error>
42+
where
43+
A: serde::de::MapAccess<'de>,
44+
{
45+
let mut out = URLParameters(String::new());
46+
while let Some((key, value)) =
47+
map.next_entry::<Cow<str>, Cow<serde_json::value::RawValue>>()?
48+
{
49+
let value = value.get();
50+
if let Ok(str_val) = serde_json::from_str::<Cow<str>>(value) {
51+
out.push_kv(&key, &str_val);
52+
} else if let Ok(vec_val) =
53+
serde_json::from_str::<Vec<serde_json::Value>>(value)
54+
{
55+
for val in vec_val {
56+
if !out.0.is_empty() {
57+
out.0.push('&');
58+
}
59+
out.encode_and_push(&key);
60+
out.0.push_str("[]");
61+
out.0.push('=');
62+
out.encode_and_push(&val.to_string());
63+
}
64+
} else {
65+
out.push_kv(&key, value);
66+
}
67+
}
68+
69+
Ok(out)
70+
}
71+
}
72+
73+
deserializer.deserialize_map(URLParametersVisitor)
74+
}
75+
}
76+
77+
#[test]
78+
fn test_url_parameters_deserializer() {
79+
use serde_json::json;
80+
let json = json!({
81+
"x": "hello world",
82+
"num": 123,
83+
"arr": [1, 2, 3],
84+
});
85+
86+
let url_parameters: URLParameters = serde_json::from_value(json).unwrap();
87+
assert_eq!(
88+
url_parameters.0,
89+
"x=hello%20world&num=123&arr[]=1&arr[]=2&arr[]=3"
90+
);
91+
}

0 commit comments

Comments
 (0)