Skip to content

Commit bd71557

Browse files
committed
less magic
1 parent d3673c5 commit bd71557

40 files changed

Lines changed: 238 additions & 151 deletions

build.rs

Lines changed: 0 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,6 @@ async fn main() {
1717
.unwrap();
1818

1919
println!("cargo:rerun-if-changed=build.rs");
20-
generate_sqlpage_functions();
2120
let c = Rc::new(make_client());
2221

2322
for h in [
@@ -36,35 +35,6 @@ async fn main() {
3635
set_odbc_rpath();
3736
}
3837

39-
/// Lists the modules in `sqlpage_functions/functions/` into a `sqlpage_functions! { ... }` call, so
40-
/// built-in SQL functions register themselves just by having a file, with no hand-maintained list.
41-
fn generate_sqlpage_functions() {
42-
const DIR: &str = "src/webserver/database/sqlpage_functions/functions";
43-
println!("cargo:rerun-if-changed={DIR}");
44-
let out = PathBuf::from(std::env::var("OUT_DIR").unwrap()).join("sqlpage_functions.rs");
45-
let Ok(dir) = std::fs::read_dir(DIR) else {
46-
// The source tree isn't present yet (e.g. the dependency-only Docker build stage, which
47-
// compiles before `src/` is copied in). Emit an empty registry; once the directory exists
48-
// the `rerun-if-changed` above makes the real build run this again.
49-
std::fs::write(&out, "sqlpage_functions! {}\n").unwrap();
50-
return;
51-
};
52-
let mut files: Vec<PathBuf> = dir
53-
.map(|entry| entry.unwrap().path())
54-
.filter(|path| path.extension().is_some_and(|ext| ext == "rs"))
55-
.collect();
56-
files.sort();
57-
let entries: String = files
58-
.iter()
59-
.map(|path| {
60-
let name = path.file_stem().unwrap().to_str().unwrap();
61-
let abs = path.canonicalize().unwrap();
62-
format!(" {name} = {abs:?},\n")
63-
})
64-
.collect();
65-
std::fs::write(out, format!("sqlpage_functions! {{\n{entries}}}\n")).unwrap();
66-
}
67-
6838
fn make_client() -> awc::Client {
6939
awc::ClientBuilder::new()
7040
.timeout(Duration::from_secs(10))
Lines changed: 15 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,34 +1,35 @@
11
# Built-in SQL functions
22

3-
Each built-in `sqlpage.*` function is a plain `async fn` in its own file in [`functions/`](functions),
4-
with an ordinary Rust signature:
3+
Each built-in `sqlpage.*` function is a plain `async fn` in its own file in
4+
[`functions/`](functions). The file stem is the SQL function name and must match the Rust function it
5+
exports:
56

67
```rust
7-
#[allow(clippy::wildcard_imports)]
8-
use super::*;
8+
use std::borrow::Cow;
9+
10+
use crate::webserver::http_request_info::RequestInfo;
911

1012
pub(super) async fn example(request: &RequestInfo, value: Option<Cow<'_, str>>) -> Option<String> {
1113
// ...
1214
}
1315
```
1416

15-
To register it, add one line to the list in [`functions.rs`](functions.rs), giving the SQL names of
16-
its arguments (used only in error messages):
17+
To add `sqlpage.example`, create `functions/example.rs` and add it to the
18+
[`sqlpage_functions!`](function_traits.rs) call in [`functions.rs`](functions.rs):
1719

1820
```rust
1921
sqlpage_functions! {
2022
// ...
21-
example("value");
23+
example,
2224
}
2325
```
2426

2527
The [`sqlpage_functions!`](function_traits.rs) macro declares the modules and generates the
26-
`SqlPageFunctionName` enum the SQL engine dispatches on. That is the only compile-time code: there is
27-
no build script involvement and no per-function generated code. Argument extraction, dispatch, and
28-
return-value conversion are handled generically in [`function_traits.rs`](function_traits.rs) by the
29-
`Extract`, `Handler`, and `IntoCowResult` traits. A function's argument and return types are read
30-
straight from its signature, so the supported argument types are exactly those that implement
31-
`Extract` (add an `impl` there to support a new one).
28+
`SqlPageFunctionName` enum the SQL engine dispatches on. Per-function argument extraction, dispatch,
29+
and return-value conversion are handled generically in [`function_traits.rs`](function_traits.rs) by
30+
the `Extract`, `Handler`, and `IntoCowResult` traits. A function's argument and return types are read
31+
straight from its signature, so supported argument types are the types that implement `Extract` there.
32+
Functions can take up to five arguments.
3233

3334
Keep helpers and unit tests that are specific to a function in that function's file. Shared helpers can
34-
be made `pub(super)` and used by sibling function modules through `use super::*`.
35+
be made `pub(super)` and imported by name from sibling function modules.

src/webserver/database/sqlpage_functions/function_traits.rs

Lines changed: 17 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
//! Dispatch machinery for the built-in `sqlpage.*` SQL functions.
22
//!
33
//! Each function is a plain `async fn` in its own module under [`functions/`](super::functions).
4-
//! `build.rs` lists those modules and the [`sqlpage_functions!`] macro turns the list into the
5-
//! [`SqlPageFunctionName`](super::functions::SqlPageFunctionName) enum the engine dispatches on, so
6-
//! functions register themselves just by existing. Adapting each signature to the uniform
4+
//! [`sqlpage_functions!`] turns the module list in [`functions`](super::functions) into the
5+
//! [`SqlPageFunctionName`](super::functions::SqlPageFunctionName) enum the engine dispatches on.
6+
//! Adapting each signature to the uniform
77
//! `(request, db, args) -> Option<string>` convention is done generically by [`Extract`] (per
88
//! argument type), [`Handler`] (per argument count, the trick `axum` uses) and [`IntoCowResult`]
99
//! (per return type); the macro itself carries no type-level glue.
@@ -217,17 +217,11 @@ impl<'a, T: IntoCow<'a>> IntoCow<'a> for Option<T> {
217217
}
218218

219219
/// Declares the listed function modules and builds the [`SqlPageFunctionName`] dispatch enum from
220-
/// them. The list is produced by `build.rs`, so there is no hand-maintained registry.
220+
/// them.
221221
macro_rules! sqlpage_functions {
222-
($($func:ident = $path:literal),* $(,)?) => {
222+
($($func:ident),* $(,)?) => {
223223
$(
224-
// `build.rs` passes the absolute path because the `mod` is expanded from a file
225-
// `include!`d out of `OUT_DIR`, where the default relative lookup would not find it.
226-
#[path = $path]
227224
mod $func;
228-
// Re-export so sibling modules can use each other's helpers via `use super::*`.
229-
#[allow(unused_imports)]
230-
use $func::*;
231225
)*
232226

233227
/// One variant per built-in `sqlpage.*` function.
@@ -238,12 +232,20 @@ macro_rules! sqlpage_functions {
238232
}
239233

240234
impl SqlPageFunctionName {
235+
const ALL: &'static [Self] = &[$(Self::$func),*];
236+
237+
fn name(self) -> &'static str {
238+
match self {
239+
$(Self::$func => stringify!($func)),*
240+
}
241+
}
242+
241243
pub(crate) async fn evaluate<'a, 'c>(
242244
self,
243-
request: &'a ExecutionContext,
244-
db_connection: &'c mut DbConn,
245-
arguments: Vec<Option<Cow<'a, str>>>,
246-
) -> anyhow::Result<Option<Cow<'a, str>>>
245+
request: &'a $crate::webserver::http_request_info::ExecutionContext,
246+
db_connection: &'c mut $crate::webserver::database::execute_queries::DbConn,
247+
arguments: Vec<Option<::std::borrow::Cow<'a, str>>>,
248+
) -> anyhow::Result<Option<::std::borrow::Cow<'a, str>>>
247249
where
248250
'a: 'c,
249251
{
@@ -256,32 +258,6 @@ macro_rules! sqlpage_functions {
256258
}
257259
}
258260
}
259-
260-
impl ::std::str::FromStr for SqlPageFunctionName {
261-
type Err = anyhow::Error;
262-
263-
fn from_str(name: &str) -> anyhow::Result<Self> {
264-
match name {
265-
$(stringify!($func) => Ok(SqlPageFunctionName::$func),)*
266-
unknown => anyhow::bail!(
267-
"Unknown function {unknown:?}. Supported functions:\n{}",
268-
[$(SqlPageFunctionName::$func),*]
269-
.iter()
270-
.map(|f| format!(" - {f}\n"))
271-
.collect::<String>()
272-
),
273-
}
274-
}
275-
}
276-
277-
impl ::std::fmt::Display for SqlPageFunctionName {
278-
fn fmt(&self, f: &mut ::std::fmt::Formatter<'_>) -> ::std::fmt::Result {
279-
f.write_str("sqlpage.")?;
280-
f.write_str(match self {
281-
$(SqlPageFunctionName::$func => stringify!($func)),*
282-
})
283-
}
284-
}
285261
};
286262
}
287263

Lines changed: 74 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -1,38 +1,83 @@
11
//! Built-in `SQLPage` SQL functions.
22
//!
33
//! Every function is a plain `async fn` in its own module under [`functions/`](self). To add one,
4-
//! just create `functions/<name>.rs` with an `async fn <name>`: `build.rs` discovers it and emits
5-
//! a [`sqlpage_functions!`](super::function_traits::sqlpage_functions) call (the file `include!`d
6-
//! below) that declares the module and adds it to the dispatch enum. Argument conversion and
4+
//! create `functions/<name>.rs` with an `async fn <name>` and add it to the
5+
//! [`sqlpage_functions!`](super::function_traits::sqlpage_functions) call below. The macro declares
6+
//! the module and adds it to the dispatch enum. Argument conversion and
77
//! dispatch are handled generically in [`super::function_traits`].
88
9-
// Every function module reaches the shared imports below, and its siblings' `pub(super)` helpers,
10-
// through `use super::*`. Allow it once here rather than on each module.
11-
#![allow(clippy::wildcard_imports)]
12-
13-
use super::{ExecutionContext, RequestInfo};
14-
use crate::filesystem::FileAccess;
15-
use crate::webserver::{
16-
ErrorWithStatus,
17-
database::{
18-
blob_to_data_url::vec_to_data_uri_with_mime,
19-
execute_queries::DbConn,
20-
sqlpage_functions::{http_fetch_request::HttpFetchRequest, url_parameters::URLParameters},
21-
},
22-
http_client::make_http_client,
23-
request_variables::SetVariablesMap,
24-
single_or_vec::SingleOrVec,
25-
};
26-
use anyhow::{Context, anyhow};
27-
use futures_util::StreamExt;
28-
use mime_guess::mime;
29-
use opentelemetry_semantic_conventions::attribute as otel;
309
use std::fmt::Write;
31-
use std::{borrow::Cow, ffi::OsStr, str::FromStr};
32-
use tracing::Instrument;
3310

3411
use super::function_traits::sqlpage_functions;
3512

36-
// Generated by build.rs: a `sqlpage_functions!` call listing every module file in `functions/`.
37-
// Discovering them there is what frees us from a hand-maintained registry.
38-
include!(concat!(env!("OUT_DIR"), "/sqlpage_functions.rs"));
13+
sqlpage_functions! {
14+
basic_auth_password,
15+
basic_auth_username,
16+
client_ip,
17+
configuration_directory,
18+
cookie,
19+
current_working_directory,
20+
environment_variable,
21+
exec,
22+
fetch,
23+
fetch_with_meta,
24+
hash_password,
25+
header,
26+
headers,
27+
hmac,
28+
link,
29+
oidc_logout_url,
30+
path,
31+
persist_uploaded_file,
32+
protocol,
33+
random_string,
34+
read_file_as_data_url,
35+
read_file_as_text,
36+
regex_match,
37+
request_body,
38+
request_body_base64,
39+
request_method,
40+
run_sql,
41+
set_variable,
42+
uploaded_file_mime_type,
43+
uploaded_file_name,
44+
uploaded_file_path,
45+
url_encode,
46+
user_info,
47+
user_info_token,
48+
variables,
49+
version,
50+
web_root,
51+
}
52+
53+
impl ::std::str::FromStr for SqlPageFunctionName {
54+
type Err = anyhow::Error;
55+
56+
fn from_str(name: &str) -> anyhow::Result<Self> {
57+
SqlPageFunctionName::ALL
58+
.iter()
59+
.copied()
60+
.find(|function| function.name() == name)
61+
.ok_or_else(|| {
62+
anyhow::anyhow!(
63+
"Unknown function {name:?}. Supported functions:\n{}",
64+
supported_function_list()
65+
)
66+
})
67+
}
68+
}
69+
70+
impl ::std::fmt::Display for SqlPageFunctionName {
71+
fn fmt(&self, f: &mut ::std::fmt::Formatter<'_>) -> ::std::fmt::Result {
72+
f.write_str("sqlpage.")?;
73+
f.write_str(self.name())
74+
}
75+
}
76+
77+
fn supported_function_list() -> String {
78+
let mut supported = String::new();
79+
for function in SqlPageFunctionName::ALL {
80+
writeln!(supported, " - {function}").expect("writing to a String cannot fail");
81+
}
82+
supported
83+
}

src/webserver/database/sqlpage_functions/functions/basic_auth_password.rs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
1-
use super::*;
1+
use anyhow::Context;
2+
3+
use crate::webserver::{ErrorWithStatus, http_request_info::RequestInfo};
24

35
/// Returns the password from the HTTP basic auth header, if present.
46
pub(super) async fn basic_auth_password(request: &RequestInfo) -> anyhow::Result<&str> {

src/webserver/database/sqlpage_functions/functions/basic_auth_username.rs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
1-
use super::*;
1+
use crate::webserver::http_request_info::RequestInfo;
2+
3+
use super::basic_auth_password::extract_basic_auth;
24

35
/// Returns the username from the HTTP basic auth header, if present.
46
/// Otherwise, returns an HTTP 401 Unauthorized error.

src/webserver/database/sqlpage_functions/functions/client_ip.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
use super::*;
1+
use crate::webserver::http_request_info::RequestInfo;
22

33
pub(super) async fn client_ip(request: &RequestInfo) -> Option<String> {
44
Some(request.client_ip?.to_string())

src/webserver/database/sqlpage_functions/functions/configuration_directory.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
use super::*;
1+
use crate::webserver::http_request_info::RequestInfo;
22

33
/// Returns the directory where the sqlpage.json configuration file, templates, and migrations are located.
44
pub(super) async fn configuration_directory(request: &RequestInfo) -> String {

src/webserver/database/sqlpage_functions/functions/cookie.rs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
1-
use super::*;
1+
use std::borrow::Cow;
2+
3+
use crate::webserver::{http_request_info::RequestInfo, single_or_vec::SingleOrVec};
24

35
pub(super) async fn cookie<'a>(request: &'a RequestInfo, name: Cow<'a, str>) -> Option<Cow<'a, str>> {
46
request.cookies.get(&*name).map(SingleOrVec::as_json_str)

src/webserver/database/sqlpage_functions/functions/current_working_directory.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
use super::*;
1+
use anyhow::Context;
22

33
pub(super) async fn current_working_directory() -> anyhow::Result<String> {
44
std::env::current_dir()

0 commit comments

Comments
 (0)