slumber-python 5.2.4__tar.gz → 5.2.5__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {slumber_python-5.2.4 → slumber_python-5.2.5}/Cargo.lock +27 -13
- {slumber_python-5.2.4 → slumber_python-5.2.5}/Cargo.toml +10 -9
- {slumber_python-5.2.4 → slumber_python-5.2.5}/PKG-INFO +1 -1
- slumber_python-5.2.5/crates/console/Cargo.toml +24 -0
- slumber_python-5.2.5/crates/console/src/lib.rs +100 -0
- {slumber_python-5.2.4 → slumber_python-5.2.5}/crates/core/src/collection/cereal.rs +3 -2
- slumber_python-5.2.5/crates/core/src/collection/value_template.rs +307 -0
- {slumber_python-5.2.4 → slumber_python-5.2.5}/crates/core/src/collection.rs +1 -1
- {slumber_python-5.2.4 → slumber_python-5.2.5}/crates/core/src/http/models.rs +0 -1
- {slumber_python-5.2.4 → slumber_python-5.2.5}/crates/core/src/http.rs +1 -1
- {slumber_python-5.2.4 → slumber_python-5.2.5}/crates/core/src/render/functions.rs +13 -19
- {slumber_python-5.2.4 → slumber_python-5.2.5}/crates/core/src/render/tests.rs +43 -64
- {slumber_python-5.2.4 → slumber_python-5.2.5}/crates/core/src/render.rs +26 -52
- {slumber_python-5.2.4 → slumber_python-5.2.5}/crates/core/src/test_util.rs +41 -36
- {slumber_python-5.2.4 → slumber_python-5.2.5}/crates/core/src/util/json.rs +2 -2
- {slumber_python-5.2.4 → slumber_python-5.2.5}/crates/python/Cargo.toml +1 -1
- {slumber_python-5.2.4 → slumber_python-5.2.5}/crates/python/src/lib.rs +3 -71
- {slumber_python-5.2.4 → slumber_python-5.2.5}/crates/template/src/display.rs +5 -0
- {slumber_python-5.2.4 → slumber_python-5.2.5}/crates/template/src/error.rs +0 -1
- {slumber_python-5.2.4 → slumber_python-5.2.5}/crates/template/src/expression.rs +2 -2
- {slumber_python-5.2.4 → slumber_python-5.2.5}/crates/template/src/lib.rs +9 -98
- {slumber_python-5.2.4 → slumber_python-5.2.5}/crates/template/src/tests.rs +4 -35
- {slumber_python-5.2.4 → slumber_python-5.2.5}/crates/template/src/value.rs +0 -4
- slumber_python-5.2.4/crates/core/src/collection/value_template.rs +0 -163
- {slumber_python-5.2.4 → slumber_python-5.2.5}/README.md +0 -0
- {slumber_python-5.2.4 → slumber_python-5.2.5}/crates/config/Cargo.toml +0 -0
- {slumber_python-5.2.4 → slumber_python-5.2.5}/crates/config/src/cereal.rs +0 -0
- {slumber_python-5.2.4 → slumber_python-5.2.5}/crates/config/src/default.yml +0 -0
- {slumber_python-5.2.4 → slumber_python-5.2.5}/crates/config/src/default_old.yml +0 -0
- {slumber_python-5.2.4 → slumber_python-5.2.5}/crates/config/src/lib.rs +0 -0
- {slumber_python-5.2.4 → slumber_python-5.2.5}/crates/config/src/tui/cereal.rs +0 -0
- {slumber_python-5.2.4 → slumber_python-5.2.5}/crates/config/src/tui/input.rs +0 -0
- {slumber_python-5.2.4 → slumber_python-5.2.5}/crates/config/src/tui/mime.rs +0 -0
- {slumber_python-5.2.4 → slumber_python-5.2.5}/crates/config/src/tui/theme.rs +0 -0
- {slumber_python-5.2.4 → slumber_python-5.2.5}/crates/config/src/tui.rs +0 -0
- {slumber_python-5.2.4 → slumber_python-5.2.5}/crates/core/Cargo.toml +0 -0
- {slumber_python-5.2.4 → slumber_python-5.2.5}/crates/core/src/collection/models.rs +0 -0
- {slumber_python-5.2.4 → slumber_python-5.2.5}/crates/core/src/collection/recipe_tree.rs +0 -0
- {slumber_python-5.2.4 → slumber_python-5.2.5}/crates/core/src/collection/schema.rs +0 -0
- {slumber_python-5.2.4 → slumber_python-5.2.5}/crates/core/src/database/convert.rs +0 -0
- {slumber_python-5.2.4 → slumber_python-5.2.5}/crates/core/src/database/migrations.rs +0 -0
- {slumber_python-5.2.4 → slumber_python-5.2.5}/crates/core/src/database/tests.rs +0 -0
- {slumber_python-5.2.4 → slumber_python-5.2.5}/crates/core/src/database.rs +0 -0
- {slumber_python-5.2.4 → slumber_python-5.2.5}/crates/core/src/http/curl.rs +0 -0
- {slumber_python-5.2.4 → slumber_python-5.2.5}/crates/core/src/http/tests.rs +0 -0
- {slumber_python-5.2.4 → slumber_python-5.2.5}/crates/core/src/lib.rs +0 -0
- {slumber_python-5.2.4 → slumber_python-5.2.5}/crates/core/src/render/util.rs +0 -0
- {slumber_python-5.2.4 → slumber_python-5.2.5}/crates/core/src/util.rs +0 -0
- {slumber_python-5.2.4 → slumber_python-5.2.5}/crates/macros/Cargo.toml +0 -0
- {slumber_python-5.2.4 → slumber_python-5.2.5}/crates/macros/src/lib.rs +0 -0
- {slumber_python-5.2.4 → slumber_python-5.2.5}/crates/python/README.md +0 -0
- {slumber_python-5.2.4 → slumber_python-5.2.5}/crates/python/dev.py +0 -0
- {slumber_python-5.2.4 → slumber_python-5.2.5}/crates/python/mise.toml +0 -0
- {slumber_python-5.2.4 → slumber_python-5.2.5}/crates/python/slumber.pyi +0 -0
- {slumber_python-5.2.4 → slumber_python-5.2.5}/crates/python/tests/slumber.yml +0 -0
- {slumber_python-5.2.4 → slumber_python-5.2.5}/crates/python/tests/test.py +0 -0
- {slumber_python-5.2.4 → slumber_python-5.2.5}/crates/python/uv.lock +0 -0
- {slumber_python-5.2.4 → slumber_python-5.2.5}/crates/template/Cargo.toml +0 -0
- {slumber_python-5.2.4 → slumber_python-5.2.5}/crates/template/proptest-regressions/parse.txt +0 -0
- {slumber_python-5.2.4 → slumber_python-5.2.5}/crates/template/src/cereal.rs +0 -0
- {slumber_python-5.2.4 → slumber_python-5.2.5}/crates/template/src/parse.rs +0 -0
- {slumber_python-5.2.4 → slumber_python-5.2.5}/crates/template/src/test_util.rs +0 -0
- {slumber_python-5.2.4 → slumber_python-5.2.5}/crates/util/Cargo.toml +0 -0
- {slumber_python-5.2.4 → slumber_python-5.2.5}/crates/util/src/lib.rs +0 -0
- {slumber_python-5.2.4 → slumber_python-5.2.5}/crates/util/src/paths.rs +0 -0
- {slumber_python-5.2.4 → slumber_python-5.2.5}/crates/util/src/test_util.rs +0 -0
- {slumber_python-5.2.4 → slumber_python-5.2.5}/crates/util/src/yaml/error.rs +0 -0
- {slumber_python-5.2.4 → slumber_python-5.2.5}/crates/util/src/yaml/resolve.rs +0 -0
- {slumber_python-5.2.4 → slumber_python-5.2.5}/crates/util/src/yaml.rs +0 -0
- {slumber_python-5.2.4 → slumber_python-5.2.5}/pyproject.toml +0 -0
|
@@ -809,7 +809,7 @@ dependencies = [
|
|
|
809
809
|
|
|
810
810
|
[[package]]
|
|
811
811
|
name = "doc_utils"
|
|
812
|
-
version = "5.2.
|
|
812
|
+
version = "5.2.5"
|
|
813
813
|
dependencies = [
|
|
814
814
|
"anyhow",
|
|
815
815
|
"clap",
|
|
@@ -3280,7 +3280,7 @@ checksum = "7a2ae44ef20feb57a68b23d846850f861394c2e02dc425a50098ae8c90267589"
|
|
|
3280
3280
|
|
|
3281
3281
|
[[package]]
|
|
3282
3282
|
name = "slumber"
|
|
3283
|
-
version = "5.2.
|
|
3283
|
+
version = "5.2.5"
|
|
3284
3284
|
dependencies = [
|
|
3285
3285
|
"anyhow",
|
|
3286
3286
|
"slumber_cli",
|
|
@@ -3292,7 +3292,7 @@ dependencies = [
|
|
|
3292
3292
|
|
|
3293
3293
|
[[package]]
|
|
3294
3294
|
name = "slumber_cli"
|
|
3295
|
-
version = "5.2.
|
|
3295
|
+
version = "5.2.5"
|
|
3296
3296
|
dependencies = [
|
|
3297
3297
|
"anyhow",
|
|
3298
3298
|
"assert_cmd",
|
|
@@ -3300,7 +3300,6 @@ dependencies = [
|
|
|
3300
3300
|
"chrono",
|
|
3301
3301
|
"clap",
|
|
3302
3302
|
"clap_complete",
|
|
3303
|
-
"dialoguer",
|
|
3304
3303
|
"env-lock",
|
|
3305
3304
|
"indexmap",
|
|
3306
3305
|
"itertools",
|
|
@@ -3314,6 +3313,7 @@ dependencies = [
|
|
|
3314
3313
|
"serde_urlencoded",
|
|
3315
3314
|
"serde_yaml",
|
|
3316
3315
|
"slumber_config",
|
|
3316
|
+
"slumber_console",
|
|
3317
3317
|
"slumber_core",
|
|
3318
3318
|
"slumber_import",
|
|
3319
3319
|
"slumber_template",
|
|
@@ -3327,7 +3327,7 @@ dependencies = [
|
|
|
3327
3327
|
|
|
3328
3328
|
[[package]]
|
|
3329
3329
|
name = "slumber_config"
|
|
3330
|
-
version = "5.2.
|
|
3330
|
+
version = "5.2.5"
|
|
3331
3331
|
dependencies = [
|
|
3332
3332
|
"derive_more",
|
|
3333
3333
|
"dirs",
|
|
@@ -3350,9 +3350,23 @@ dependencies = [
|
|
|
3350
3350
|
"tracing",
|
|
3351
3351
|
]
|
|
3352
3352
|
|
|
3353
|
+
[[package]]
|
|
3354
|
+
name = "slumber_console"
|
|
3355
|
+
version = "5.2.5"
|
|
3356
|
+
dependencies = [
|
|
3357
|
+
"anyhow",
|
|
3358
|
+
"async-trait",
|
|
3359
|
+
"dialoguer",
|
|
3360
|
+
"slumber_core",
|
|
3361
|
+
"slumber_template",
|
|
3362
|
+
"slumber_util",
|
|
3363
|
+
"tokio",
|
|
3364
|
+
"tracing",
|
|
3365
|
+
]
|
|
3366
|
+
|
|
3353
3367
|
[[package]]
|
|
3354
3368
|
name = "slumber_core"
|
|
3355
|
-
version = "5.2.
|
|
3369
|
+
version = "5.2.5"
|
|
3356
3370
|
dependencies = [
|
|
3357
3371
|
"async-trait",
|
|
3358
3372
|
"base64 0.22.1",
|
|
@@ -3400,7 +3414,7 @@ dependencies = [
|
|
|
3400
3414
|
|
|
3401
3415
|
[[package]]
|
|
3402
3416
|
name = "slumber_import"
|
|
3403
|
-
version = "5.2.
|
|
3417
|
+
version = "5.2.5"
|
|
3404
3418
|
dependencies = [
|
|
3405
3419
|
"anyhow",
|
|
3406
3420
|
"base64 0.22.1",
|
|
@@ -3432,7 +3446,7 @@ dependencies = [
|
|
|
3432
3446
|
|
|
3433
3447
|
[[package]]
|
|
3434
3448
|
name = "slumber_macros"
|
|
3435
|
-
version = "5.2.
|
|
3449
|
+
version = "5.2.5"
|
|
3436
3450
|
dependencies = [
|
|
3437
3451
|
"proc-macro2",
|
|
3438
3452
|
"quote",
|
|
@@ -3441,13 +3455,13 @@ dependencies = [
|
|
|
3441
3455
|
|
|
3442
3456
|
[[package]]
|
|
3443
3457
|
name = "slumber_python"
|
|
3444
|
-
version = "5.2.
|
|
3458
|
+
version = "5.2.5"
|
|
3445
3459
|
dependencies = [
|
|
3446
3460
|
"async-trait",
|
|
3447
|
-
"dialoguer",
|
|
3448
3461
|
"indexmap",
|
|
3449
3462
|
"pyo3",
|
|
3450
3463
|
"slumber_config",
|
|
3464
|
+
"slumber_console",
|
|
3451
3465
|
"slumber_core",
|
|
3452
3466
|
"slumber_template",
|
|
3453
3467
|
"tokio",
|
|
@@ -3455,7 +3469,7 @@ dependencies = [
|
|
|
3455
3469
|
|
|
3456
3470
|
[[package]]
|
|
3457
3471
|
name = "slumber_template"
|
|
3458
|
-
version = "5.2.
|
|
3472
|
+
version = "5.2.5"
|
|
3459
3473
|
dependencies = [
|
|
3460
3474
|
"bytes",
|
|
3461
3475
|
"derive_more",
|
|
@@ -3482,7 +3496,7 @@ dependencies = [
|
|
|
3482
3496
|
|
|
3483
3497
|
[[package]]
|
|
3484
3498
|
name = "slumber_tui"
|
|
3485
|
-
version = "5.2.
|
|
3499
|
+
version = "5.2.5"
|
|
3486
3500
|
dependencies = [
|
|
3487
3501
|
"anyhow",
|
|
3488
3502
|
"async-trait",
|
|
@@ -3524,7 +3538,7 @@ dependencies = [
|
|
|
3524
3538
|
|
|
3525
3539
|
[[package]]
|
|
3526
3540
|
name = "slumber_util"
|
|
3527
|
-
version = "5.2.
|
|
3541
|
+
version = "5.2.5"
|
|
3528
3542
|
dependencies = [
|
|
3529
3543
|
"console-subscriber",
|
|
3530
3544
|
"derive_more",
|
|
@@ -9,7 +9,7 @@ homepage = "https://slumber.lucaspickering.me"
|
|
|
9
9
|
keywords = ["rest", "http", "terminal", "tui"]
|
|
10
10
|
license = "MIT"
|
|
11
11
|
repository = "https://github.com/LucasPickering/slumber"
|
|
12
|
-
version = "5.2.
|
|
12
|
+
version = "5.2.5"
|
|
13
13
|
# Keep in sync w/ rust-toolchain.toml
|
|
14
14
|
rust-version = "1.90.0"
|
|
15
15
|
|
|
@@ -43,14 +43,15 @@ serde_json = {version = "1.0.120", default-features = false, features = ["preser
|
|
|
43
43
|
serde_json_path = "0.7.1"
|
|
44
44
|
serde_test = "1.0.176"
|
|
45
45
|
serde_yaml = {version = "0.9.0", default-features = false}
|
|
46
|
-
slumber_cli = {path = "./crates/cli", version = "5.2.
|
|
47
|
-
slumber_config = {path = "./crates/config", version = "5.2.
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
46
|
+
slumber_cli = {path = "./crates/cli", version = "5.2.5" }
|
|
47
|
+
slumber_config = {path = "./crates/config", version = "5.2.5", default-features = false }
|
|
48
|
+
slumber_console = {path = "./crates/console", version = "5.2.5" }
|
|
49
|
+
slumber_core = {path = "./crates/core", version = "5.2.5" }
|
|
50
|
+
slumber_import = {path = "./crates/import", version = "5.2.5" }
|
|
51
|
+
slumber_macros = {path = "./crates/macros", version = "5.2.5" }
|
|
52
|
+
slumber_template = {path = "./crates/template", version = "5.2.5" }
|
|
53
|
+
slumber_tui = {path = "./crates/tui", version = "5.2.5" }
|
|
54
|
+
slumber_util = {path = "./crates/util", version = "5.2.5" }
|
|
54
55
|
strum = {version = "0.27.0", default-features = false}
|
|
55
56
|
syn = "2.0.101"
|
|
56
57
|
terminput = "0.5.3"
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
[package]
|
|
2
|
+
authors = {workspace = true}
|
|
3
|
+
description = "Utitilies for console-based Slumber frontends. Not intended for external use."
|
|
4
|
+
edition = {workspace = true}
|
|
5
|
+
homepage = {workspace = true}
|
|
6
|
+
keywords = {workspace = true}
|
|
7
|
+
license = {workspace = true}
|
|
8
|
+
name = "slumber_console"
|
|
9
|
+
repository = {workspace = true}
|
|
10
|
+
rust-version = {workspace = true}
|
|
11
|
+
version = {workspace = true}
|
|
12
|
+
|
|
13
|
+
[package.metadata.release]
|
|
14
|
+
tag = false
|
|
15
|
+
|
|
16
|
+
[dependencies]
|
|
17
|
+
anyhow = {workspace = true}
|
|
18
|
+
async-trait = {workspace = true}
|
|
19
|
+
dialoguer = {workspace = true, features = ["password"]}
|
|
20
|
+
slumber_core = {workspace = true}
|
|
21
|
+
slumber_template = {workspace = true}
|
|
22
|
+
slumber_util = {workspace = true}
|
|
23
|
+
tokio = {workspace = true, features = ["sync"]}
|
|
24
|
+
tracing = {workspace = true}
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
//! Utilities for console-based Slumber frontends
|
|
2
|
+
|
|
3
|
+
use anyhow::Context as _;
|
|
4
|
+
use async_trait::async_trait;
|
|
5
|
+
use dialoguer::{Input, Password, Select};
|
|
6
|
+
use slumber_core::render::{Prompter, SelectOption};
|
|
7
|
+
use slumber_template::Value;
|
|
8
|
+
use slumber_util::ResultTracedAnyhow;
|
|
9
|
+
use tokio::sync::Mutex;
|
|
10
|
+
use tracing::warn;
|
|
11
|
+
|
|
12
|
+
/// A proxy for a lock on stdout
|
|
13
|
+
///
|
|
14
|
+
/// Forces prompts to run one at a time, so they don't fight over the console.
|
|
15
|
+
/// This is a static instead of being in [ConsolePrompter] because there could
|
|
16
|
+
/// theoretically be multiple prompters running simultaneously.
|
|
17
|
+
static CONSOLE_LOCK: Mutex<()> = Mutex::const_new(());
|
|
18
|
+
|
|
19
|
+
/// Prompt the user for input on the console
|
|
20
|
+
///
|
|
21
|
+
/// This uses [dialoguer] to generate the prompts. Since its API is blocking,
|
|
22
|
+
/// this runs each prompt in a tokio blocking thread.
|
|
23
|
+
#[derive(Debug)]
|
|
24
|
+
pub struct ConsolePrompter;
|
|
25
|
+
|
|
26
|
+
impl ConsolePrompter {
|
|
27
|
+
/// Spawn a blocking prompt in a background task
|
|
28
|
+
async fn spawn_prompt<T, F>(&self, f: F) -> Option<T>
|
|
29
|
+
where
|
|
30
|
+
T: 'static + Send,
|
|
31
|
+
F: 'static + FnOnce() -> anyhow::Result<T> + Send,
|
|
32
|
+
{
|
|
33
|
+
let guard = CONSOLE_LOCK.lock().await;
|
|
34
|
+
let result = tokio::task::spawn_blocking(f)
|
|
35
|
+
.await
|
|
36
|
+
.context("Prompt panicked")
|
|
37
|
+
.flatten()
|
|
38
|
+
.traced();
|
|
39
|
+
// Make sure the guard is held while the task is running. Since the lock
|
|
40
|
+
// doesn't actually contain the locked resource, I wanna be careful.
|
|
41
|
+
drop(guard);
|
|
42
|
+
result.ok()
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
#[async_trait(?Send)]
|
|
47
|
+
impl Prompter for ConsolePrompter {
|
|
48
|
+
async fn prompt_text(
|
|
49
|
+
&self,
|
|
50
|
+
message: String,
|
|
51
|
+
default: Option<String>,
|
|
52
|
+
sensitive: bool,
|
|
53
|
+
) -> Option<String> {
|
|
54
|
+
self.spawn_prompt(move || {
|
|
55
|
+
if sensitive {
|
|
56
|
+
// Dialoguer doesn't support default values here so there's
|
|
57
|
+
// nothing we can do
|
|
58
|
+
if default.is_some() {
|
|
59
|
+
warn!(
|
|
60
|
+
"Default value not supported for sensitive CLI prompts"
|
|
61
|
+
);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
Password::new()
|
|
65
|
+
.with_prompt(message)
|
|
66
|
+
.allow_empty_password(true)
|
|
67
|
+
.interact()
|
|
68
|
+
} else {
|
|
69
|
+
let mut input =
|
|
70
|
+
Input::new().with_prompt(message).allow_empty(true);
|
|
71
|
+
if let Some(default) = default {
|
|
72
|
+
input = input.default(default);
|
|
73
|
+
}
|
|
74
|
+
input.interact()
|
|
75
|
+
}
|
|
76
|
+
// If we failed to read the value, print an error and report nothing
|
|
77
|
+
.context("Error reading value from prompt")
|
|
78
|
+
})
|
|
79
|
+
.await
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
async fn prompt_select(
|
|
83
|
+
&self,
|
|
84
|
+
message: String,
|
|
85
|
+
mut options: Vec<SelectOption>,
|
|
86
|
+
) -> Option<Value> {
|
|
87
|
+
self.spawn_prompt(move || {
|
|
88
|
+
let index = Select::new()
|
|
89
|
+
.with_prompt(message)
|
|
90
|
+
.items(&options)
|
|
91
|
+
.default(0)
|
|
92
|
+
.interact()
|
|
93
|
+
// If we failed to read the value, print an error and report
|
|
94
|
+
// nothing
|
|
95
|
+
.context("Error reading value from select")?;
|
|
96
|
+
Ok(options.swap_remove(index).value)
|
|
97
|
+
})
|
|
98
|
+
.await
|
|
99
|
+
}
|
|
100
|
+
}
|
|
@@ -409,10 +409,11 @@ impl DeserializeYaml for ValueTemplate {
|
|
|
409
409
|
YamlData::Value(Scalar::FloatingPoint(f)) => Ok(Self::Float(f.0)),
|
|
410
410
|
// Parse string as a template
|
|
411
411
|
YamlData::Value(Scalar::String(s)) => {
|
|
412
|
-
|
|
412
|
+
// Template will be unpacked here if appropriate
|
|
413
|
+
let template = s.parse::<Self>().map_err(|error| {
|
|
413
414
|
LocatedError::other(error, yaml.location)
|
|
414
415
|
})?;
|
|
415
|
-
Ok(
|
|
416
|
+
Ok(template)
|
|
416
417
|
}
|
|
417
418
|
YamlData::Sequence(sequence) => {
|
|
418
419
|
let values = sequence
|
|
@@ -0,0 +1,307 @@
|
|
|
1
|
+
use crate::render::TemplateContext;
|
|
2
|
+
use bytes::BytesMut;
|
|
3
|
+
use futures::{FutureExt, TryStreamExt, future};
|
|
4
|
+
use serde::{Serialize, Serializer};
|
|
5
|
+
use slumber_template::{
|
|
6
|
+
Context, Expression, RenderError, RenderValue, RenderedChunk,
|
|
7
|
+
RenderedChunks, Template, TemplateChunk, TemplateParseError, Value,
|
|
8
|
+
ValueStream,
|
|
9
|
+
};
|
|
10
|
+
use std::str::FromStr;
|
|
11
|
+
|
|
12
|
+
/// A templated [Value]
|
|
13
|
+
///
|
|
14
|
+
/// This is a [Value], except the strings are templates. That means this can be
|
|
15
|
+
/// dynamically rendered to a [Value]. This is used for structured bodies (e.g.
|
|
16
|
+
/// JSON) as well as profile fields.
|
|
17
|
+
#[derive(Clone, Debug, derive_more::From, PartialEq, Serialize)]
|
|
18
|
+
#[serde(untagged)]
|
|
19
|
+
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
|
|
20
|
+
pub enum ValueTemplate {
|
|
21
|
+
Null,
|
|
22
|
+
Boolean(bool),
|
|
23
|
+
Integer(i64),
|
|
24
|
+
Float(f64),
|
|
25
|
+
/// An unpacked single-chunk template
|
|
26
|
+
#[serde(serialize_with = "serialize_expression")]
|
|
27
|
+
#[cfg_attr(feature = "schema", schemars(with = "String"))]
|
|
28
|
+
Expression(Expression),
|
|
29
|
+
/// A template that has not been unpacked
|
|
30
|
+
///
|
|
31
|
+
/// This is either a single raw chunk or a multi-chunk template
|
|
32
|
+
#[from(ignore)]
|
|
33
|
+
String(Template),
|
|
34
|
+
#[from(ignore)]
|
|
35
|
+
Array(Vec<Self>),
|
|
36
|
+
// A key-value mapping. Stored as a `Vec` instead of `IndexMap` because
|
|
37
|
+
// the keys are templates, which aren't hashable. We never do key lookups
|
|
38
|
+
// on this so there's no need for a map anyway.
|
|
39
|
+
#[from(ignore)]
|
|
40
|
+
#[serde(serialize_with = "slumber_util::serialize_mapping")]
|
|
41
|
+
#[cfg_attr(
|
|
42
|
+
feature = "schema",
|
|
43
|
+
schemars(with = "std::collections::HashMap<Template, Self>")
|
|
44
|
+
)]
|
|
45
|
+
Object(Vec<(Template, Self)>),
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
impl ValueTemplate {
|
|
49
|
+
/// Create a new string template from a raw string, without parsing it at
|
|
50
|
+
/// all
|
|
51
|
+
///
|
|
52
|
+
/// Useful when importing from external formats where the string isn't
|
|
53
|
+
/// expected to be a valid Slumber template
|
|
54
|
+
pub fn raw(template: String) -> Self {
|
|
55
|
+
Self::String(Template::raw(template))
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/// Does the template have at least one dynamic chunk? If this returns
|
|
59
|
+
/// `false`, the template will always render to its source text
|
|
60
|
+
pub fn is_dynamic(&self) -> bool {
|
|
61
|
+
match self {
|
|
62
|
+
Self::Null
|
|
63
|
+
| Self::Boolean(_)
|
|
64
|
+
| Self::Integer(_)
|
|
65
|
+
| Self::Float(_) => false,
|
|
66
|
+
Self::Expression(_) => true,
|
|
67
|
+
Self::String(template) => template.is_dynamic(),
|
|
68
|
+
Self::Array(array) => array.iter().any(Self::is_dynamic),
|
|
69
|
+
Self::Object(object) => object
|
|
70
|
+
.iter()
|
|
71
|
+
.any(|(key, value)| key.is_dynamic() || value.is_dynamic()),
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/// Render to a value or chunks with eager values
|
|
76
|
+
///
|
|
77
|
+
/// The return value is *usually* a single value, but if `self` is a
|
|
78
|
+
/// multi-chunk template string, then its multi-chunk output will be the
|
|
79
|
+
/// output for this.
|
|
80
|
+
///
|
|
81
|
+
/// Use this for cases where streaming is *not* allowed.
|
|
82
|
+
pub async fn render_value(
|
|
83
|
+
&self,
|
|
84
|
+
context: &TemplateContext,
|
|
85
|
+
) -> RenderedValue<Value> {
|
|
86
|
+
self.render_chunks_inner(context, Template::render_chunks)
|
|
87
|
+
.boxed_local() // Box for recursion
|
|
88
|
+
.await
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/// Render to a value or chunks with stream values
|
|
92
|
+
///
|
|
93
|
+
/// The return value is *usually* a single value, but if `self` is a
|
|
94
|
+
/// multi-chunk template string, then its multi-chunk output will be the
|
|
95
|
+
/// output for this.
|
|
96
|
+
///
|
|
97
|
+
/// Use this for cases where streaming is allowed.
|
|
98
|
+
pub async fn render_value_stream(
|
|
99
|
+
&self,
|
|
100
|
+
context: &TemplateContext,
|
|
101
|
+
) -> RenderedValue<ValueStream> {
|
|
102
|
+
self.render_chunks_inner(context, Template::render_chunks_stream)
|
|
103
|
+
.boxed_local() // Box for recursion
|
|
104
|
+
.await
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/// Render to chunks with dynamic output type
|
|
108
|
+
async fn render_chunks_inner<V>(
|
|
109
|
+
&self,
|
|
110
|
+
context: &TemplateContext,
|
|
111
|
+
render_string: impl AsyncFn(
|
|
112
|
+
&Template,
|
|
113
|
+
&TemplateContext,
|
|
114
|
+
) -> RenderedChunks<V>,
|
|
115
|
+
) -> RenderedValue<V>
|
|
116
|
+
where
|
|
117
|
+
V: RenderValue,
|
|
118
|
+
TemplateContext: Context<V>,
|
|
119
|
+
{
|
|
120
|
+
match self {
|
|
121
|
+
Self::Null => Value::Null.into(),
|
|
122
|
+
Self::Boolean(b) => Value::Boolean(*b).into(),
|
|
123
|
+
Self::Integer(i) => Value::Integer(*i).into(),
|
|
124
|
+
Self::Float(f) => Value::Float(*f).into(),
|
|
125
|
+
Self::Expression(expression) => {
|
|
126
|
+
let result = expression.render(context).await;
|
|
127
|
+
RenderedValue::Value(result)
|
|
128
|
+
}
|
|
129
|
+
Self::String(template) => {
|
|
130
|
+
RenderedValue::Chunks(render_string(template, context).await)
|
|
131
|
+
}
|
|
132
|
+
Self::Array(array) => {
|
|
133
|
+
// Render each value and collection into an Array
|
|
134
|
+
future::try_join_all(array.iter().map(|value| {
|
|
135
|
+
value
|
|
136
|
+
.render_value(context)
|
|
137
|
+
.map(RenderedValue::try_into_value)
|
|
138
|
+
}))
|
|
139
|
+
.await
|
|
140
|
+
.map(Value::Array)
|
|
141
|
+
.into() // Wrap into RenderedValue
|
|
142
|
+
}
|
|
143
|
+
Self::Object(map) => {
|
|
144
|
+
// Render each key/value and collect into an Object
|
|
145
|
+
future::try_join_all(map.iter().map(|(key, value)| async {
|
|
146
|
+
let key = key.render_string(context).await?;
|
|
147
|
+
let value =
|
|
148
|
+
value.render_value(context).await.try_into_value()?;
|
|
149
|
+
Ok::<_, RenderError>((key, value))
|
|
150
|
+
}))
|
|
151
|
+
.await
|
|
152
|
+
.map(Value::from)
|
|
153
|
+
.into() // Wrap into RenderedValue
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/// Parse template from a string literal. Panic if invalid
|
|
160
|
+
#[cfg(any(test, feature = "test"))]
|
|
161
|
+
impl From<&str> for ValueTemplate {
|
|
162
|
+
fn from(value: &str) -> Self {
|
|
163
|
+
value.parse().unwrap()
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
#[cfg(any(test, feature = "test"))]
|
|
168
|
+
impl<T: Into<ValueTemplate>> From<Vec<T>> for ValueTemplate {
|
|
169
|
+
fn from(value: Vec<T>) -> Self {
|
|
170
|
+
Self::Array(value.into_iter().map(T::into).collect())
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
#[cfg(any(test, feature = "test"))]
|
|
175
|
+
impl<T: Into<ValueTemplate>> From<Vec<(&str, T)>> for ValueTemplate {
|
|
176
|
+
fn from(value: Vec<(&str, T)>) -> Self {
|
|
177
|
+
Self::Object(
|
|
178
|
+
value
|
|
179
|
+
.into_iter()
|
|
180
|
+
.map(|(k, v)| (k.parse().unwrap(), v.into()))
|
|
181
|
+
.collect(),
|
|
182
|
+
)
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/// A value rendered from a [ValueTemplate]
|
|
187
|
+
#[derive(Debug)]
|
|
188
|
+
pub enum RenderedValue<V> {
|
|
189
|
+
/// A single-chunk dynamic template unpacked to a single value (or error)
|
|
190
|
+
Value(Result<V, RenderError>),
|
|
191
|
+
/// A raw or multi-chunk template that can be stitched into a string
|
|
192
|
+
Chunks(RenderedChunks<V>),
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
impl RenderedValue<Value> {
|
|
196
|
+
/// Collect the rendered value into a [Value] by these rules:
|
|
197
|
+
/// - If the value is a [RenderedValue::Value], return that value directly
|
|
198
|
+
/// - Any other template will be rendered to a string by stringifying each
|
|
199
|
+
/// dynamic chunk and concatenating them all together
|
|
200
|
+
/// - If rendering to a string fails because the bytes are not valid UTF-8,
|
|
201
|
+
/// concatenate into a bytes object instead
|
|
202
|
+
pub fn try_into_value(self) -> Result<Value, RenderError> {
|
|
203
|
+
let value = match self {
|
|
204
|
+
RenderedValue::Value(result) => result?,
|
|
205
|
+
RenderedValue::Chunks(chunks) => {
|
|
206
|
+
// Render to bytes
|
|
207
|
+
let bytes = chunks.try_into_bytes()?;
|
|
208
|
+
Value::Bytes(bytes)
|
|
209
|
+
}
|
|
210
|
+
};
|
|
211
|
+
Ok(value.decode_bytes())
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
impl RenderedValue<ValueStream> {
|
|
216
|
+
/// Does this output contain *any* stream chunks?
|
|
217
|
+
pub fn has_stream(&self) -> bool {
|
|
218
|
+
match self {
|
|
219
|
+
Self::Value(Ok(ValueStream::Stream { .. })) => true,
|
|
220
|
+
Self::Value(Ok(ValueStream::Value(_)) | Err(_)) => false,
|
|
221
|
+
Self::Chunks(chunks) => chunks.iter().any(|chunk| match chunk {
|
|
222
|
+
RenderedChunk::Raw(_)
|
|
223
|
+
| RenderedChunk::Dynamic(ValueStream::Value(_))
|
|
224
|
+
| RenderedChunk::Error(_) => false,
|
|
225
|
+
RenderedChunk::Dynamic(ValueStream::Stream { .. }) => true,
|
|
226
|
+
}),
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
/// Collect the rendered chunks into a [Value] by these rules:
|
|
231
|
+
/// - If the value is a [RenderedValue::Value], return that value directly
|
|
232
|
+
/// - If there are any streams, resolve them to bytes
|
|
233
|
+
/// - Any other template will be rendered to a string by stringifying each
|
|
234
|
+
/// dynamic chunk and concatenating them all together
|
|
235
|
+
/// - If rendering to a string fails because the bytes are not valid UTF-8,
|
|
236
|
+
/// concatenate into a bytes object instead
|
|
237
|
+
pub async fn try_collect_value(self) -> Result<Value, RenderError> {
|
|
238
|
+
// If we only have one chunk, unpack it into a value
|
|
239
|
+
let value = match self {
|
|
240
|
+
Self::Value(Ok(ValueStream::Value(value))) => value,
|
|
241
|
+
Self::Value(Ok(stream @ ValueStream::Stream { .. })) => {
|
|
242
|
+
stream.resolve().await?
|
|
243
|
+
}
|
|
244
|
+
Self::Value(Err(error)) => return Err(error),
|
|
245
|
+
Self::Chunks(chunks) => {
|
|
246
|
+
// Render to bytes
|
|
247
|
+
let bytes = chunks
|
|
248
|
+
.try_into_stream()?
|
|
249
|
+
.try_collect::<BytesMut>()
|
|
250
|
+
.await?
|
|
251
|
+
.into();
|
|
252
|
+
Value::Bytes(bytes)
|
|
253
|
+
}
|
|
254
|
+
};
|
|
255
|
+
|
|
256
|
+
// Try to convert bytes to string, because that's generally more
|
|
257
|
+
// useful to the consumer
|
|
258
|
+
Ok(value.decode_bytes())
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
/// Create render output of a single chunk with a value
|
|
263
|
+
impl<V: RenderValue> From<Value> for RenderedValue<V> {
|
|
264
|
+
fn from(value: Value) -> Self {
|
|
265
|
+
Self::Value(Ok(V::from_value(value)))
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
/// Create render output of a single chunk that may have failed
|
|
270
|
+
impl<V: RenderValue> From<Result<Value, RenderError>> for RenderedValue<V> {
|
|
271
|
+
fn from(result: Result<Value, RenderError>) -> Self {
|
|
272
|
+
Self::Value(result.map(V::from_value))
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
impl From<Template> for ValueTemplate {
|
|
277
|
+
fn from(template: Template) -> Self {
|
|
278
|
+
// Single dynamic chunk is unpacked
|
|
279
|
+
match <[_; 1]>::try_from(template.into_chunks()) {
|
|
280
|
+
Ok([TemplateChunk::Expression(expression)]) => {
|
|
281
|
+
Self::Expression(expression)
|
|
282
|
+
}
|
|
283
|
+
Ok(chunks) => Self::String(Template::from_chunks(chunks.into())),
|
|
284
|
+
Err(chunks) => Self::String(Template::from_chunks(chunks)),
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
/// Parse a template string to [ValueTemplate]
|
|
290
|
+
///
|
|
291
|
+
/// This will unpack the template to an expression if possible.
|
|
292
|
+
impl FromStr for ValueTemplate {
|
|
293
|
+
type Err = TemplateParseError;
|
|
294
|
+
|
|
295
|
+
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
|
296
|
+
let template: Template = s.parse()?;
|
|
297
|
+
Ok(template.into())
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
/// Serialize an expression as `{{ expression }}`
|
|
302
|
+
fn serialize_expression<S: Serializer>(
|
|
303
|
+
expression: &Expression,
|
|
304
|
+
serializer: S,
|
|
305
|
+
) -> Result<S::Ok, S::Error> {
|
|
306
|
+
serializer.serialize_str(&Template::display_expression(expression))
|
|
307
|
+
}
|
|
@@ -766,7 +766,7 @@ impl Recipe {
|
|
|
766
766
|
| (Some(RecipeBody::Json(_)), Some(BodyOverride::Json(json))) => {
|
|
767
767
|
// Render the value
|
|
768
768
|
let rendered_value = json
|
|
769
|
-
.
|
|
769
|
+
.render_value(context)
|
|
770
770
|
.await
|
|
771
771
|
.try_into_value()
|
|
772
772
|
.map_err(RequestBuildErrorKind::BodyRender)?;
|