Skip to content

Commit 7df0d5a

Browse files
committed
Resolve merge-queue conflict with variable lookup refactor
Merge the queued squash commit for PR #1535. Preserve its array and null-value assertions alongside PR #1529 production-app request helpers.
2 parents a59f23e + 359f224 commit 7df0d5a

8 files changed

Lines changed: 195 additions & 78 deletions

File tree

‎src/webserver/database/sqlpage_expr.rs‎

Lines changed: 14 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,6 @@ use serde_json::Value;
1212
use super::execute_queries::DbConn;
1313
use super::sqlpage_functions::functions::SqlPageFunctionName;
1414
use crate::webserver::http_request_info::ExecutionContext;
15-
use crate::webserver::single_or_vec::SingleOrVec;
1615

1716
/// An expression evaluated by `SQLPage`.
1817
///
@@ -163,50 +162,21 @@ fn value_to_sqlpage_value(value: Value) -> SqlPageValue<'static> {
163162

164163
impl VariableRef {
165164
fn evaluate<'a>(&self, request: &'a ExecutionContext) -> SqlPageValue<'a> {
166-
let value = match self.source {
167-
VariableSource::Url => request
168-
.url_params
169-
.get(&self.name)
170-
.map(SingleOrVec::as_json_str),
171-
VariableSource::SetOrForm => {
172-
if let Some(value) = request.set_variables.borrow().get(&self.name) {
173-
return value.as_ref().map_or(SqlPageValue::Null, |value| {
174-
SqlPageValue::Text(Cow::Owned(value.as_json_str().into_owned()))
175-
});
176-
}
177-
request
178-
.post_variables
179-
.get(&self.name)
180-
.map(SingleOrVec::as_json_str)
181-
}
182-
VariableSource::SetOrUrl => {
183-
if let Some(value) = request.set_variables.borrow().get(&self.name) {
184-
return value.as_ref().map_or(SqlPageValue::Null, |value| {
185-
SqlPageValue::Text(Cow::Owned(value.as_json_str().into_owned()))
186-
});
187-
}
188-
let url_value = request.url_params.get(&self.name);
189-
if request.post_variables.contains_key(&self.name) {
190-
if url_value.is_some() {
191-
log::warn!(
192-
"Deprecation warning! There is both a URL parameter named '{}' and a form field named '{}'. SQLPage is using the URL parameter for ${}. Please use :{} to reference the form field explicitly.",
193-
self.name,
194-
self.name,
195-
self.name,
196-
self.name,
197-
);
198-
} else {
199-
log::warn!(
200-
"Deprecation warning! ${} was used to reference a form field value (a POST variable). This now uses only URL parameters. Please use :{} instead.",
201-
self.name,
202-
self.name,
203-
);
204-
}
205-
}
206-
url_value.map(SingleOrVec::as_json_str)
207-
}
165+
use crate::webserver::request_variables::{LookupPolicy, VariableAccess, VariableValue};
166+
let policy = match self.source {
167+
VariableSource::Url => LookupPolicy::GetOnly,
168+
VariableSource::SetOrForm => LookupPolicy::SetThenPost,
169+
VariableSource::SetOrUrl => LookupPolicy::SetThenGet,
208170
};
209-
value.map_or(SqlPageValue::Null, SqlPageValue::Text)
171+
let variables = VariableAccess::new(
172+
&request.url_params,
173+
&request.post_variables,
174+
&request.set_variables,
175+
);
176+
match variables.lookup(&self.name, policy) {
177+
VariableValue::Missing | VariableValue::Null => SqlPageValue::Null,
178+
VariableValue::Text(value) => SqlPageValue::Text(value),
179+
}
210180
}
211181
}
212182

‎src/webserver/database/sqlpage_functions/functions/variables.rs‎

Lines changed: 6 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -22,28 +22,11 @@ pub(super) async fn variables<'a>(
2222
));
2323
}
2424
} else {
25-
use serde::{Serializer, ser::SerializeMap};
26-
let mut res = Vec::new();
27-
let mut serializer = serde_json::Serializer::new(&mut res);
28-
let set_vars = request.set_variables.borrow();
29-
let len = request.url_params.len() + request.post_variables.len() + set_vars.len();
30-
let mut ser = serializer.serialize_map(Some(len))?;
31-
let mut seen_keys = std::collections::HashSet::new();
32-
for (k, v) in &*set_vars {
33-
seen_keys.insert(k);
34-
ser.serialize_entry(k, v)?;
35-
}
36-
for (k, v) in &request.post_variables {
37-
if seen_keys.insert(k) {
38-
ser.serialize_entry(k, v)?;
39-
}
40-
}
41-
for (k, v) in &request.url_params {
42-
if seen_keys.insert(k) {
43-
ser.serialize_entry(k, v)?;
44-
}
45-
}
46-
ser.end()?;
47-
String::from_utf8(res)?
25+
let variables = crate::webserver::request_variables::VariableAccess::new(
26+
&request.url_params,
27+
&request.post_variables,
28+
&request.set_variables,
29+
);
30+
serde_json::to_string(&variables)?
4831
})
4932
}

‎src/webserver/request_variables.rs‎

Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,95 @@
1+
use std::borrow::Cow;
2+
use std::cell::RefCell;
13
use std::collections::{HashMap, hash_map::Entry};
24

5+
use serde::{Serialize, Serializer, ser::SerializeMap};
6+
37
use crate::webserver::single_or_vec::SingleOrVec;
48

59
pub type ParamMap = HashMap<String, SingleOrVec>;
610
pub type SetVariablesMap = HashMap<String, Option<SingleOrVec>>;
711

12+
/// Request values stay borrowed; mutable SET values are copied before releasing
13+
/// their `RefCell` borrow. A present SET NULL must suppress request fallbacks.
14+
pub(crate) struct VariableAccess<'a> {
15+
get: &'a ParamMap,
16+
post: &'a ParamMap,
17+
set: &'a RefCell<SetVariablesMap>,
18+
}
19+
20+
#[derive(Clone, Copy)]
21+
pub(crate) enum LookupPolicy {
22+
GetOnly,
23+
SetThenGet,
24+
SetThenPost,
25+
}
26+
27+
pub(crate) enum VariableValue<'a> {
28+
Missing,
29+
Null,
30+
Text(Cow<'a, str>),
31+
}
32+
33+
impl<'a> VariableAccess<'a> {
34+
pub(crate) fn new(
35+
get: &'a ParamMap,
36+
post: &'a ParamMap,
37+
set: &'a RefCell<SetVariablesMap>,
38+
) -> Self {
39+
Self { get, post, set }
40+
}
41+
42+
pub(crate) fn lookup(&self, name: &str, policy: LookupPolicy) -> VariableValue<'a> {
43+
if !matches!(policy, LookupPolicy::GetOnly)
44+
&& let Some(value) = self.set.borrow().get(name)
45+
{
46+
return value.as_ref().map_or(VariableValue::Null, |value| {
47+
VariableValue::Text(Cow::Owned(value.as_json_str().into_owned()))
48+
});
49+
}
50+
let values = match policy {
51+
LookupPolicy::SetThenPost => self.post,
52+
LookupPolicy::GetOnly | LookupPolicy::SetThenGet => self.get,
53+
};
54+
if matches!(policy, LookupPolicy::SetThenGet) && self.post.contains_key(name) {
55+
if values.contains_key(name) {
56+
log::warn!(
57+
"Deprecation warning! There is both a URL parameter named '{name}' and a form field named '{name}'. SQLPage is using the URL parameter for ${name}. Please use :{name} to reference the form field explicitly."
58+
);
59+
} else {
60+
log::warn!(
61+
"Deprecation warning! ${name} was used to reference a form field value (a POST variable). This now uses only URL parameters. Please use :{name} instead."
62+
);
63+
}
64+
}
65+
values.get(name).map_or(VariableValue::Missing, |value| {
66+
VariableValue::Text(value.as_json_str())
67+
})
68+
}
69+
}
70+
71+
/// Serialize the merged SET > POST > GET view without cloning its values.
72+
impl Serialize for VariableAccess<'_> {
73+
fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
74+
let set = self.set.borrow();
75+
let mut map = serializer.serialize_map(None)?;
76+
for (key, value) in &*set {
77+
map.serialize_entry(key, value)?;
78+
}
79+
for (key, value) in self.post {
80+
if !set.contains_key(key) {
81+
map.serialize_entry(key, value)?;
82+
}
83+
}
84+
for (key, value) in self.get {
85+
if !set.contains_key(key) && !self.post.contains_key(key) {
86+
map.serialize_entry(key, value)?;
87+
}
88+
}
89+
map.end()
90+
}
91+
}
92+
893
pub fn param_map<PAIRS: IntoIterator<Item = (String, String)>>(values: PAIRS) -> ParamMap {
994
values
1095
.into_iter()
@@ -26,3 +111,47 @@ pub fn param_map<PAIRS: IntoIterator<Item = (String, String)>>(values: PAIRS) ->
26111
map
27112
})
28113
}
114+
115+
#[cfg(test)]
116+
mod tests {
117+
use super::*;
118+
use LookupPolicy::{GetOnly, SetThenGet, SetThenPost};
119+
use VariableValue::{Missing, Null, Text};
120+
121+
#[test]
122+
fn lookup_distinguishes_null_missing_and_releases_set_borrows() {
123+
let get = param_map([("value".into(), "get".into())]);
124+
let post = param_map([("value".into(), "post".into())]);
125+
for (policy, fallback) in [(GetOnly, "get"), (SetThenGet, "get"), (SetThenPost, "post")] {
126+
let set = RefCell::new(SetVariablesMap::from([("value".into(), None)]));
127+
let view = VariableAccess::new(&get, &post, &set);
128+
assert!(matches!(view.lookup("absent", policy), Missing));
129+
let get_only = matches!(policy, GetOnly);
130+
assert_eq!(matches!(view.lookup("value", policy), Null), !get_only);
131+
for (value, expected) in [
132+
(SingleOrVec::Single("set".into()), "set"),
133+
(
134+
SingleOrVec::Vec(vec!["a".into(), "b".into()]),
135+
r#"["a","b"]"#,
136+
),
137+
] {
138+
set.borrow_mut().insert("value".into(), Some(value));
139+
let Text(actual) = view.lookup("value", policy) else {
140+
panic!("missing SET")
141+
};
142+
if expected.starts_with('[') {
143+
assert_eq!(
144+
serde_json::to_value(&view).unwrap()["value"],
145+
serde_json::json!(["a", "b"])
146+
);
147+
}
148+
set.borrow_mut().clear();
149+
assert_eq!(matches!(actual, Cow::Borrowed(_)), get_only);
150+
assert_eq!(actual, if get_only { "get" } else { expected });
151+
assert!(
152+
matches!(view.lookup("value", policy), Text(Cow::Borrowed(value)) if value == fallback)
153+
);
154+
}
155+
}
156+
}
157+
}

‎tests/requests/mod.rs‎

Lines changed: 17 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -127,8 +127,8 @@ async fn test_large_form_field_roundtrip() -> actix_web::Result<()> {
127127

128128
#[actix_web::test]
129129
async fn test_variables_function() -> actix_web::Result<()> {
130-
let url = "/tests/requests/variables.sql?common=get_value&get_only=get_val";
131-
let req_body = "common=post_value&post_only=post_val";
130+
let url = "/tests/requests/variables.sql?common=get_value&get_only=get_val&array[]=get_a&array[]=get_b";
131+
let req_body = "common=post_value&post_only=post_val&array[]=post_a&array[]=post_b";
132132
let req = request_for(url)
133133
.insert_header(("content-type", "application/x-www-form-urlencoded"))
134134
.insert_header(("accept", "application/json"))
@@ -142,30 +142,30 @@ async fn test_variables_function() -> actix_web::Result<()> {
142142
[
143143
(
144144
"all_vars",
145-
json!({"get_only": "get_val", "common": "get_value", "post_only": "post_val", "common": "post_value"}),
145+
json!({"get_only": "get_val", "common": "post_value", "post_only": "post_val", "array": ["post_a", "post_b"]}),
146146
),
147147
(
148148
"get_vars",
149-
json!({"get_only": "get_val", "common": "get_value"}),
149+
json!({"get_only": "get_val", "common": "get_value", "array": ["get_a", "get_b"]}),
150150
),
151151
(
152152
"post_vars",
153-
json!({"post_only": "post_val", "common": "post_value"}),
153+
json!({"post_only": "post_val", "common": "post_value", "array": ["post_a", "post_b"]}),
154154
),
155155
("set_vars", json!({})),
156156
],
157157
[
158158
(
159159
"all_vars",
160-
json!({"get_only": "get_val", "common": "set_common_value", "post_only": "post_val", "my_set_var": "set_value"}),
160+
json!({"get_only": "get_val", "common": "set_common_value", "post_only": "post_val", "my_set_var": "set_value", "array": ["post_a", "post_b"]}),
161161
),
162162
(
163163
"get_vars",
164-
json!({"get_only": "get_val", "common": "get_value"}),
164+
json!({"get_only": "get_val", "common": "get_value", "array": ["get_a", "get_b"]}),
165165
),
166166
(
167167
"post_vars",
168-
json!({"post_only": "post_val", "common": "post_value"}),
168+
json!({"post_only": "post_val", "common": "post_value", "array": ["post_a", "post_b"]}),
169169
),
170170
(
171171
"set_vars",
@@ -174,9 +174,17 @@ async fn test_variables_function() -> actix_web::Result<()> {
174174
],
175175
];
176176

177+
let mut null_step = expected[1].clone();
178+
null_step[0].1["common"] = json!(null);
179+
null_step[3].1["common"] = json!(null);
177180
let actual_array = body_json.as_array().expect("response is nota json array");
178-
for (i, expected_step) in expected.into_iter().enumerate() {
181+
assert_eq!(actual_array.len(), expected.len() + 1);
182+
for (i, expected_step) in expected.into_iter().chain([null_step]).enumerate() {
179183
let actual = &actual_array[i];
184+
assert!(actual["missing"].is_null());
185+
assert!(actual["post_only_compat"].is_null());
186+
assert_eq!(actual["get_array_lookup"], r#"["get_a","get_b"]"#);
187+
assert_eq!(actual["post_array_lookup"], r#"["post_a","post_b"]"#);
180188
for (key, expected_value) in expected_step {
181189
let actual_decoded: serde_json::Value =
182190
serde_json::from_str(actual[key].as_str().unwrap()).unwrap();

‎tests/requests/variables.sql‎

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,9 @@ select
22
sqlpage.variables() as all_vars,
33
sqlpage.variables('get') as get_vars,
44
sqlpage.variables('post') as post_vars,
5-
sqlpage.variables('set') as set_vars;
5+
sqlpage.variables('set') as set_vars,
6+
$absent as missing, $post_only as post_only_compat,
7+
$array as get_array_lookup, :array as post_array_lookup;
68

79
set my_set_var = 'set_value';
810
set common = 'set_common_value';
@@ -11,4 +13,12 @@ select
1113
sqlpage.variables() as all_vars,
1214
sqlpage.variables('get') as get_vars,
1315
sqlpage.variables('post') as post_vars,
14-
sqlpage.variables('set') as set_vars;
16+
sqlpage.variables('set') as set_vars,
17+
$absent as missing, $post_only as post_only_compat,
18+
$array as get_array_lookup, :array as post_array_lookup;
19+
20+
set common = NULL;
21+
select sqlpage.variables() as all_vars, sqlpage.variables('get') as get_vars,
22+
sqlpage.variables('post') as post_vars, sqlpage.variables('set') as set_vars,
23+
$absent as missing, $post_only as post_only_compat,
24+
$array as get_array_lookup, :array as post_array_lookup;

‎tests/sql_test_files/data/immutable_variables.sql‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,3 +8,9 @@ select '"x":"set_value"' as expected_contains, sqlpage.variables('set') as actua
88
select '"set_only":"only_in_set"' as expected_contains, sqlpage.variables('set') as actual;
99
select '"x":"set_value"' as expected_contains, sqlpage.variables() as actual;
1010
select '"set_only":"only_in_set"' as expected_contains, sqlpage.variables() as actual;
11+
12+
-- The fixture runner provides ?x=1. SET NULL suppresses $x but leaves GET enumeration immutable.
13+
set x = NULL;
14+
select NULL as expected, $x as actual;
15+
select '"x":null' as expected_contains, sqlpage.variables() as actual;
16+
select '"x":"1"' as expected_contains, sqlpage.variables('get') as actual;
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
set scope = 'parent';
2+
set child_rows = sqlpage.run_sql('tests/sql_test_files/variable_scope_child.sql');
3+
select '"inherited":"parent"' as expected_contains, $child_rows as actual;
4+
select '"changed":"child"' as expected_contains, $child_rows as actual;
5+
select 'parent' as expected, $scope as actual;
6+
set child_rows = sqlpage.run_sql('tests/sql_test_files/variable_scope_child.sql', '{"scope":null}');
7+
select '"inherited":null' as expected_contains, $child_rows as actual;
8+
select 'parent' as expected, $scope as actual;
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
select $scope as inherited;
2+
set scope = 'child';
3+
select $scope as changed;

0 commit comments

Comments
 (0)