slumber-python 5.1.1__tar.gz → 5.2.3__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.1.1 → slumber_python-5.2.3}/Cargo.lock +13 -13
- {slumber_python-5.1.1 → slumber_python-5.2.3}/Cargo.toml +23 -11
- {slumber_python-5.1.1 → slumber_python-5.2.3}/PKG-INFO +1 -1
- {slumber_python-5.1.1 → slumber_python-5.2.3}/crates/core/src/collection/cereal.rs +6 -17
- {slumber_python-5.1.1 → slumber_python-5.2.3}/crates/core/src/collection/models.rs +8 -6
- slumber_python-5.2.3/crates/core/src/collection/value_template.rs +163 -0
- {slumber_python-5.1.1 → slumber_python-5.2.3}/crates/core/src/collection.rs +14 -4
- {slumber_python-5.1.1 → slumber_python-5.2.3}/crates/core/src/http/models.rs +6 -4
- {slumber_python-5.1.1 → slumber_python-5.2.3}/crates/core/src/http/tests.rs +11 -4
- {slumber_python-5.1.1 → slumber_python-5.2.3}/crates/core/src/http.rs +14 -9
- {slumber_python-5.1.1 → slumber_python-5.2.3}/crates/core/src/render/functions.rs +10 -10
- {slumber_python-5.1.1 → slumber_python-5.2.3}/crates/core/src/render/tests.rs +69 -50
- {slumber_python-5.1.1 → slumber_python-5.2.3}/crates/core/src/render.rs +129 -100
- slumber_python-5.2.3/crates/core/src/util/json.rs +306 -0
- {slumber_python-5.1.1 → slumber_python-5.2.3}/crates/core/src/util.rs +2 -21
- {slumber_python-5.1.1 → slumber_python-5.2.3}/crates/macros/src/lib.rs +1 -1
- {slumber_python-5.1.1 → slumber_python-5.2.3}/crates/python/mise.toml +2 -2
- {slumber_python-5.1.1 → slumber_python-5.2.3}/crates/python/src/lib.rs +1 -1
- {slumber_python-5.1.1 → slumber_python-5.2.3}/crates/template/src/expression.rs +27 -17
- {slumber_python-5.1.1 → slumber_python-5.2.3}/crates/template/src/lib.rs +207 -122
- {slumber_python-5.1.1 → slumber_python-5.2.3}/crates/template/src/tests.rs +33 -14
- {slumber_python-5.1.1 → slumber_python-5.2.3}/crates/template/src/value.rs +73 -56
- {slumber_python-5.1.1 → slumber_python-5.2.3}/crates/util/Cargo.toml +5 -3
- {slumber_python-5.1.1 → slumber_python-5.2.3}/crates/util/src/lib.rs +97 -2
- {slumber_python-5.1.1 → slumber_python-5.2.3}/crates/util/src/test_util.rs +13 -3
- slumber_python-5.1.1/crates/core/src/collection/json.rs +0 -252
- {slumber_python-5.1.1 → slumber_python-5.2.3}/README.md +0 -0
- {slumber_python-5.1.1 → slumber_python-5.2.3}/crates/config/Cargo.toml +0 -0
- {slumber_python-5.1.1 → slumber_python-5.2.3}/crates/config/src/cereal.rs +0 -0
- {slumber_python-5.1.1 → slumber_python-5.2.3}/crates/config/src/default.yml +0 -0
- {slumber_python-5.1.1 → slumber_python-5.2.3}/crates/config/src/default_old.yml +0 -0
- {slumber_python-5.1.1 → slumber_python-5.2.3}/crates/config/src/lib.rs +0 -0
- {slumber_python-5.1.1 → slumber_python-5.2.3}/crates/config/src/tui/cereal.rs +0 -0
- {slumber_python-5.1.1 → slumber_python-5.2.3}/crates/config/src/tui/input.rs +0 -0
- {slumber_python-5.1.1 → slumber_python-5.2.3}/crates/config/src/tui/mime.rs +0 -0
- {slumber_python-5.1.1 → slumber_python-5.2.3}/crates/config/src/tui/theme.rs +0 -0
- {slumber_python-5.1.1 → slumber_python-5.2.3}/crates/config/src/tui.rs +0 -0
- {slumber_python-5.1.1 → slumber_python-5.2.3}/crates/core/Cargo.toml +1 -1
- {slumber_python-5.1.1 → slumber_python-5.2.3}/crates/core/src/collection/recipe_tree.rs +0 -0
- {slumber_python-5.1.1 → slumber_python-5.2.3}/crates/core/src/collection/schema.rs +0 -0
- {slumber_python-5.1.1 → slumber_python-5.2.3}/crates/core/src/database/convert.rs +0 -0
- {slumber_python-5.1.1 → slumber_python-5.2.3}/crates/core/src/database/migrations.rs +0 -0
- {slumber_python-5.1.1 → slumber_python-5.2.3}/crates/core/src/database/tests.rs +0 -0
- {slumber_python-5.1.1 → slumber_python-5.2.3}/crates/core/src/database.rs +0 -0
- {slumber_python-5.1.1 → slumber_python-5.2.3}/crates/core/src/http/curl.rs +0 -0
- {slumber_python-5.1.1 → slumber_python-5.2.3}/crates/core/src/lib.rs +0 -0
- {slumber_python-5.1.1 → slumber_python-5.2.3}/crates/core/src/render/util.rs +0 -0
- {slumber_python-5.1.1 → slumber_python-5.2.3}/crates/core/src/test_util.rs +0 -0
- {slumber_python-5.1.1 → slumber_python-5.2.3}/crates/macros/Cargo.toml +0 -0
- {slumber_python-5.1.1 → slumber_python-5.2.3}/crates/python/Cargo.toml +0 -0
- {slumber_python-5.1.1 → slumber_python-5.2.3}/crates/python/README.md +0 -0
- {slumber_python-5.1.1 → slumber_python-5.2.3}/crates/python/dev.py +0 -0
- {slumber_python-5.1.1 → slumber_python-5.2.3}/crates/python/slumber.pyi +0 -0
- {slumber_python-5.1.1 → slumber_python-5.2.3}/crates/python/tests/slumber.yml +0 -0
- {slumber_python-5.1.1 → slumber_python-5.2.3}/crates/python/tests/test.py +0 -0
- {slumber_python-5.1.1 → slumber_python-5.2.3}/crates/python/uv.lock +0 -0
- {slumber_python-5.1.1 → slumber_python-5.2.3}/crates/template/Cargo.toml +0 -0
- {slumber_python-5.1.1 → slumber_python-5.2.3}/crates/template/proptest-regressions/parse.txt +0 -0
- {slumber_python-5.1.1 → slumber_python-5.2.3}/crates/template/src/cereal.rs +0 -0
- {slumber_python-5.1.1 → slumber_python-5.2.3}/crates/template/src/display.rs +0 -0
- {slumber_python-5.1.1 → slumber_python-5.2.3}/crates/template/src/error.rs +0 -0
- {slumber_python-5.1.1 → slumber_python-5.2.3}/crates/template/src/parse.rs +0 -0
- {slumber_python-5.1.1 → slumber_python-5.2.3}/crates/template/src/test_util.rs +0 -0
- {slumber_python-5.1.1 → slumber_python-5.2.3}/crates/util/src/paths.rs +0 -0
- {slumber_python-5.1.1 → slumber_python-5.2.3}/crates/util/src/yaml/error.rs +0 -0
- {slumber_python-5.1.1 → slumber_python-5.2.3}/crates/util/src/yaml/resolve.rs +0 -0
- {slumber_python-5.1.1 → slumber_python-5.2.3}/crates/util/src/yaml.rs +0 -0
- {slumber_python-5.1.1 → slumber_python-5.2.3}/pyproject.toml +0 -0
|
@@ -809,7 +809,7 @@ dependencies = [
|
|
|
809
809
|
|
|
810
810
|
[[package]]
|
|
811
811
|
name = "doc_utils"
|
|
812
|
-
version = "5.
|
|
812
|
+
version = "5.2.3"
|
|
813
813
|
dependencies = [
|
|
814
814
|
"anyhow",
|
|
815
815
|
"clap",
|
|
@@ -3280,21 +3280,19 @@ checksum = "7a2ae44ef20feb57a68b23d846850f861394c2e02dc425a50098ae8c90267589"
|
|
|
3280
3280
|
|
|
3281
3281
|
[[package]]
|
|
3282
3282
|
name = "slumber"
|
|
3283
|
-
version = "5.
|
|
3283
|
+
version = "5.2.3"
|
|
3284
3284
|
dependencies = [
|
|
3285
3285
|
"anyhow",
|
|
3286
|
-
"console-subscriber",
|
|
3287
3286
|
"slumber_cli",
|
|
3288
3287
|
"slumber_tui",
|
|
3289
3288
|
"slumber_util",
|
|
3290
3289
|
"tokio",
|
|
3291
3290
|
"tracing",
|
|
3292
|
-
"tracing-subscriber",
|
|
3293
3291
|
]
|
|
3294
3292
|
|
|
3295
3293
|
[[package]]
|
|
3296
3294
|
name = "slumber_cli"
|
|
3297
|
-
version = "5.
|
|
3295
|
+
version = "5.2.3"
|
|
3298
3296
|
dependencies = [
|
|
3299
3297
|
"anyhow",
|
|
3300
3298
|
"assert_cmd",
|
|
@@ -3329,7 +3327,7 @@ dependencies = [
|
|
|
3329
3327
|
|
|
3330
3328
|
[[package]]
|
|
3331
3329
|
name = "slumber_config"
|
|
3332
|
-
version = "5.
|
|
3330
|
+
version = "5.2.3"
|
|
3333
3331
|
dependencies = [
|
|
3334
3332
|
"derive_more",
|
|
3335
3333
|
"dirs",
|
|
@@ -3354,7 +3352,7 @@ dependencies = [
|
|
|
3354
3352
|
|
|
3355
3353
|
[[package]]
|
|
3356
3354
|
name = "slumber_core"
|
|
3357
|
-
version = "5.
|
|
3355
|
+
version = "5.2.3"
|
|
3358
3356
|
dependencies = [
|
|
3359
3357
|
"async-trait",
|
|
3360
3358
|
"base64 0.22.1",
|
|
@@ -3402,7 +3400,7 @@ dependencies = [
|
|
|
3402
3400
|
|
|
3403
3401
|
[[package]]
|
|
3404
3402
|
name = "slumber_import"
|
|
3405
|
-
version = "5.
|
|
3403
|
+
version = "5.2.3"
|
|
3406
3404
|
dependencies = [
|
|
3407
3405
|
"anyhow",
|
|
3408
3406
|
"base64 0.22.1",
|
|
@@ -3434,7 +3432,7 @@ dependencies = [
|
|
|
3434
3432
|
|
|
3435
3433
|
[[package]]
|
|
3436
3434
|
name = "slumber_macros"
|
|
3437
|
-
version = "5.
|
|
3435
|
+
version = "5.2.3"
|
|
3438
3436
|
dependencies = [
|
|
3439
3437
|
"proc-macro2",
|
|
3440
3438
|
"quote",
|
|
@@ -3443,7 +3441,7 @@ dependencies = [
|
|
|
3443
3441
|
|
|
3444
3442
|
[[package]]
|
|
3445
3443
|
name = "slumber_python"
|
|
3446
|
-
version = "5.
|
|
3444
|
+
version = "5.2.3"
|
|
3447
3445
|
dependencies = [
|
|
3448
3446
|
"async-trait",
|
|
3449
3447
|
"dialoguer",
|
|
@@ -3457,7 +3455,7 @@ dependencies = [
|
|
|
3457
3455
|
|
|
3458
3456
|
[[package]]
|
|
3459
3457
|
name = "slumber_template"
|
|
3460
|
-
version = "5.
|
|
3458
|
+
version = "5.2.3"
|
|
3461
3459
|
dependencies = [
|
|
3462
3460
|
"bytes",
|
|
3463
3461
|
"derive_more",
|
|
@@ -3484,7 +3482,7 @@ dependencies = [
|
|
|
3484
3482
|
|
|
3485
3483
|
[[package]]
|
|
3486
3484
|
name = "slumber_tui"
|
|
3487
|
-
version = "5.
|
|
3485
|
+
version = "5.2.3"
|
|
3488
3486
|
dependencies = [
|
|
3489
3487
|
"anyhow",
|
|
3490
3488
|
"async-trait",
|
|
@@ -3520,13 +3518,15 @@ dependencies = [
|
|
|
3520
3518
|
"tree-sitter-json",
|
|
3521
3519
|
"unicode-width",
|
|
3522
3520
|
"uuid",
|
|
3521
|
+
"winnow",
|
|
3523
3522
|
"wiremock",
|
|
3524
3523
|
]
|
|
3525
3524
|
|
|
3526
3525
|
[[package]]
|
|
3527
3526
|
name = "slumber_util"
|
|
3528
|
-
version = "5.
|
|
3527
|
+
version = "5.2.3"
|
|
3529
3528
|
dependencies = [
|
|
3529
|
+
"console-subscriber",
|
|
3530
3530
|
"derive_more",
|
|
3531
3531
|
"dirs",
|
|
3532
3532
|
"env-lock",
|
|
@@ -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.
|
|
12
|
+
version = "5.2.3"
|
|
13
13
|
# Keep in sync w/ rust-toolchain.toml
|
|
14
14
|
rust-version = "1.90.0"
|
|
15
15
|
|
|
@@ -43,14 +43,14 @@ 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.
|
|
47
|
-
slumber_config = {path = "./crates/config", version = "5.
|
|
48
|
-
slumber_core = {path = "./crates/core", version = "5.
|
|
49
|
-
slumber_import = {path = "./crates/import", version = "5.
|
|
50
|
-
slumber_macros = {path = "./crates/macros", version = "5.
|
|
51
|
-
slumber_template = {path = "./crates/template", version = "5.
|
|
52
|
-
slumber_tui = {path = "./crates/tui", version = "5.
|
|
53
|
-
slumber_util = {path = "./crates/util", version = "5.
|
|
46
|
+
slumber_cli = {path = "./crates/cli", version = "5.2.3" }
|
|
47
|
+
slumber_config = {path = "./crates/config", version = "5.2.3", default-features = false }
|
|
48
|
+
slumber_core = {path = "./crates/core", version = "5.2.3" }
|
|
49
|
+
slumber_import = {path = "./crates/import", version = "5.2.3" }
|
|
50
|
+
slumber_macros = {path = "./crates/macros", version = "5.2.3" }
|
|
51
|
+
slumber_template = {path = "./crates/template", version = "5.2.3" }
|
|
52
|
+
slumber_tui = {path = "./crates/tui", version = "5.2.3" }
|
|
53
|
+
slumber_util = {path = "./crates/util", version = "5.2.3" }
|
|
54
54
|
strum = {version = "0.27.0", default-features = false}
|
|
55
55
|
syn = "2.0.101"
|
|
56
56
|
terminput = "0.5.3"
|
|
@@ -58,7 +58,6 @@ thiserror = "2.0.12"
|
|
|
58
58
|
tokio = {version = "1.39.2", default-features = false}
|
|
59
59
|
tokio-util = "0.7.13"
|
|
60
60
|
tracing = "0.1.40"
|
|
61
|
-
tracing-subscriber = {version = "0.3.17", default-features = false, features = ["ansi", "fmt", "registry"]}
|
|
62
61
|
url = "2.0.0"
|
|
63
62
|
uuid = {version = "1.10.0", default-features = false}
|
|
64
63
|
winnow = "0.7.0"
|
|
@@ -118,7 +117,20 @@ installers = ["shell", "powershell", "homebrew"]
|
|
|
118
117
|
# A GitHub repo to push Homebrew formulas to
|
|
119
118
|
tap = "LucasPickering/homebrew-tap"
|
|
120
119
|
# Target platforms to build apps for (Rust target-triple syntax)
|
|
121
|
-
targets = [
|
|
120
|
+
targets = [
|
|
121
|
+
"aarch64-apple-darwin",
|
|
122
|
+
"aarch64-unknown-linux-gnu",
|
|
123
|
+
"aarch64-unknown-linux-musl",
|
|
124
|
+
"aarch64-pc-windows-msvc",
|
|
125
|
+
"x86_64-apple-darwin",
|
|
126
|
+
"x86_64-unknown-linux-gnu",
|
|
127
|
+
"x86_64-unknown-linux-musl",
|
|
128
|
+
"x86_64-pc-windows-msvc",
|
|
129
|
+
"armv7-unknown-linux-gnueabi",
|
|
130
|
+
"armv7-unknown-linux-gnueabihf",
|
|
131
|
+
"armv7-unknown-linux-musleabi",
|
|
132
|
+
"armv7-unknown-linux-musleabihf",
|
|
133
|
+
]
|
|
122
134
|
# Publish jobs to run in CI
|
|
123
135
|
publish-jobs = ["homebrew"]
|
|
124
136
|
# Which actions to run on pull requests
|
|
@@ -9,9 +9,9 @@
|
|
|
9
9
|
|
|
10
10
|
use crate::{
|
|
11
11
|
collection::{
|
|
12
|
-
Authentication, Collection, Folder,
|
|
12
|
+
Authentication, Collection, Folder, Profile, ProfileId,
|
|
13
13
|
QueryParameterValue, Recipe, RecipeBody, RecipeId, RecipeTree,
|
|
14
|
-
recipe_tree::RecipeNode,
|
|
14
|
+
ValueTemplate, recipe_tree::RecipeNode,
|
|
15
15
|
},
|
|
16
16
|
http::HttpMethod,
|
|
17
17
|
};
|
|
@@ -383,7 +383,7 @@ impl DeserializeYaml for RecipeBody {
|
|
|
383
383
|
}
|
|
384
384
|
}
|
|
385
385
|
|
|
386
|
-
impl DeserializeYaml for
|
|
386
|
+
impl DeserializeYaml for ValueTemplate {
|
|
387
387
|
fn expected() -> Expected {
|
|
388
388
|
Expected::OneOf(&[
|
|
389
389
|
&Expected::Null,
|
|
@@ -404,16 +404,9 @@ impl DeserializeYaml for JsonTemplate {
|
|
|
404
404
|
| YamlData::BadValue
|
|
405
405
|
| YamlData::Alias(_) => yaml_parse_panic(),
|
|
406
406
|
YamlData::Value(Scalar::Null) => Ok(Self::Null),
|
|
407
|
-
YamlData::Value(Scalar::Boolean(b)) => Ok(Self::
|
|
408
|
-
YamlData::Value(Scalar::Integer(i)) => Ok(Self::
|
|
409
|
-
YamlData::Value(Scalar::FloatingPoint(f)) => Ok(Self::
|
|
410
|
-
serde_json::Number::from_f64(f.0).ok_or_else(|| {
|
|
411
|
-
LocatedError::other(
|
|
412
|
-
CerealError::InvalidJsonFloat(f.0),
|
|
413
|
-
yaml.location,
|
|
414
|
-
)
|
|
415
|
-
})?,
|
|
416
|
-
)),
|
|
407
|
+
YamlData::Value(Scalar::Boolean(b)) => Ok(Self::Boolean(b)),
|
|
408
|
+
YamlData::Value(Scalar::Integer(i)) => Ok(Self::Integer(i)),
|
|
409
|
+
YamlData::Value(Scalar::FloatingPoint(f)) => Ok(Self::Float(f.0)),
|
|
417
410
|
// Parse string as a template
|
|
418
411
|
YamlData::Value(Scalar::String(s)) => {
|
|
419
412
|
let template = s.parse::<Template>().map_err(|error| {
|
|
@@ -451,10 +444,6 @@ impl DeserializeYaml for JsonTemplate {
|
|
|
451
444
|
/// only holds errors specific to collection deserialization.
|
|
452
445
|
#[derive(Debug, thiserror::Error)]
|
|
453
446
|
enum CerealError {
|
|
454
|
-
/// JSON body contained a float value that isn't representable in JSON
|
|
455
|
-
#[error("Invalid float `{0}`; JSON does not support NaN or Infinity")]
|
|
456
|
-
InvalidJsonFloat(f64),
|
|
457
|
-
|
|
458
447
|
#[error(
|
|
459
448
|
"Cannot set profile `{second}` as default; `{first}` is already default"
|
|
460
449
|
)]
|
|
@@ -2,8 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
use crate::{
|
|
4
4
|
collection::{
|
|
5
|
-
cereal,
|
|
6
|
-
json::JsonTemplate,
|
|
5
|
+
ValueTemplate, cereal,
|
|
7
6
|
recipe_tree::{RecipeNode, RecipeTree},
|
|
8
7
|
},
|
|
9
8
|
http::HttpMethod,
|
|
@@ -119,10 +118,12 @@ impl Collection {
|
|
|
119
118
|
impl slumber_util::Factory for Collection {
|
|
120
119
|
fn factory((): ()) -> Self {
|
|
121
120
|
use crate::test_util::by_id;
|
|
121
|
+
use serde_json::json;
|
|
122
|
+
|
|
122
123
|
// Include a body in the recipe, so body-related behavior can be tested
|
|
123
124
|
let recipe = Recipe {
|
|
124
125
|
body: Some(RecipeBody::Json(
|
|
125
|
-
|
|
126
|
+
json!({"message": "hello"}).try_into().unwrap(),
|
|
126
127
|
)),
|
|
127
128
|
..Recipe::factory(())
|
|
128
129
|
};
|
|
@@ -157,7 +158,8 @@ pub struct Profile {
|
|
|
157
158
|
#[serde(skip_serializing_if = "cereal::is_false")] // Skip if default
|
|
158
159
|
#[cfg_attr(feature = "schema", schemars(default))]
|
|
159
160
|
pub default: bool,
|
|
160
|
-
|
|
161
|
+
/// Arbitrary data that can be used in templates
|
|
162
|
+
pub data: IndexMap<String, ValueTemplate>,
|
|
161
163
|
}
|
|
162
164
|
|
|
163
165
|
impl Profile {
|
|
@@ -513,7 +515,7 @@ impl<const N: usize> From<[&'static str; N]> for QueryParameterValue {
|
|
|
513
515
|
pub enum RecipeBody {
|
|
514
516
|
/// `application/json` body. Value can be any JSON value, with strings being
|
|
515
517
|
/// interpreted as templates
|
|
516
|
-
Json(
|
|
518
|
+
Json(ValueTemplate),
|
|
517
519
|
/// `application/x-www-form-urlencoded` body. Value is a mapping of form
|
|
518
520
|
/// field name to value. Values are templates while field names are not.
|
|
519
521
|
/// Values must render to strings.
|
|
@@ -541,7 +543,7 @@ impl RecipeBody {
|
|
|
541
543
|
/// Build a JSON body *without* parsing the internal strings as templates.
|
|
542
544
|
/// Useful for importing from external formats.
|
|
543
545
|
pub fn untemplated_json(value: serde_json::Value) -> Self {
|
|
544
|
-
Self::Json(
|
|
546
|
+
Self::Json(ValueTemplate::from_raw_json(value))
|
|
545
547
|
}
|
|
546
548
|
|
|
547
549
|
/// Get the anticipated MIME type that will appear in the `Content-Type`
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
use crate::render::TemplateContext;
|
|
2
|
+
use futures::{FutureExt, future};
|
|
3
|
+
use serde::Serialize;
|
|
4
|
+
use slumber_template::{
|
|
5
|
+
RenderError, RenderedChunks, Template, Value, ValueStream,
|
|
6
|
+
};
|
|
7
|
+
|
|
8
|
+
/// A templated [Value]
|
|
9
|
+
///
|
|
10
|
+
/// This is a [Value], except the strings are templates. That means this can be
|
|
11
|
+
/// dynamically rendered to a [Value]. This is used for structured bodies (e.g.
|
|
12
|
+
/// JSON) as well as profile fields.
|
|
13
|
+
#[derive(Clone, Debug, derive_more::From, PartialEq, Serialize)]
|
|
14
|
+
#[serde(untagged)]
|
|
15
|
+
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
|
|
16
|
+
pub enum ValueTemplate {
|
|
17
|
+
Null,
|
|
18
|
+
Boolean(bool),
|
|
19
|
+
Integer(i64),
|
|
20
|
+
Float(f64),
|
|
21
|
+
String(Template),
|
|
22
|
+
#[from(ignore)]
|
|
23
|
+
Array(Vec<Self>),
|
|
24
|
+
// A key-value mapping. Stored as a `Vec` instead of `IndexMap` because
|
|
25
|
+
// the keys are templates, which aren't hashable. We never do key lookups
|
|
26
|
+
// on this so there's no need for a map anyway.
|
|
27
|
+
#[from(ignore)]
|
|
28
|
+
#[serde(serialize_with = "slumber_util::serialize_mapping")]
|
|
29
|
+
#[cfg_attr(
|
|
30
|
+
feature = "schema",
|
|
31
|
+
schemars(with = "std::collections::HashMap<Template, Self>")
|
|
32
|
+
)]
|
|
33
|
+
Object(Vec<(Template, Self)>),
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
impl ValueTemplate {
|
|
37
|
+
/// Create a new string template from a raw string, without parsing it at
|
|
38
|
+
/// all
|
|
39
|
+
///
|
|
40
|
+
/// Useful when importing from external formats where the string isn't
|
|
41
|
+
/// expected to be a valid Slumber template
|
|
42
|
+
pub fn raw(template: String) -> Self {
|
|
43
|
+
Self::String(Template::raw(template))
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/// Does the template have at least one dynamic chunk? If this returns
|
|
47
|
+
/// `false`, the template will always render to its source text
|
|
48
|
+
pub fn is_dynamic(&self) -> bool {
|
|
49
|
+
match self {
|
|
50
|
+
Self::Null
|
|
51
|
+
| Self::Boolean(_)
|
|
52
|
+
| Self::Integer(_)
|
|
53
|
+
| Self::Float(_) => false,
|
|
54
|
+
Self::String(template) => template.is_dynamic(),
|
|
55
|
+
Self::Array(array) => array.iter().any(Self::is_dynamic),
|
|
56
|
+
Self::Object(object) => object
|
|
57
|
+
.iter()
|
|
58
|
+
.any(|(key, value)| key.is_dynamic() || value.is_dynamic()),
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/// Render to chunks with eager values
|
|
63
|
+
///
|
|
64
|
+
/// The return value is *usually* a single chunk, but if `self` is a
|
|
65
|
+
/// multi-chunk template string, then its multi-chunk output will be the
|
|
66
|
+
/// output for this.
|
|
67
|
+
///
|
|
68
|
+
/// Use this for cases where streaming is *not* allowed.
|
|
69
|
+
pub async fn render_chunks(
|
|
70
|
+
&self,
|
|
71
|
+
context: &TemplateContext,
|
|
72
|
+
) -> RenderedChunks<Value> {
|
|
73
|
+
self.render_chunks_inner(context, Template::render_chunks)
|
|
74
|
+
.await
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/// Render to chunks with stream values
|
|
78
|
+
///
|
|
79
|
+
/// The return value is *usually* a single chunk, but if `self` is a
|
|
80
|
+
/// multi-chunk template string, then its multi-chunk output will be the
|
|
81
|
+
/// output for this.
|
|
82
|
+
///
|
|
83
|
+
/// Use this for cases where streaming is allowed.
|
|
84
|
+
pub async fn render_chunks_stream(
|
|
85
|
+
&self,
|
|
86
|
+
context: &TemplateContext,
|
|
87
|
+
) -> RenderedChunks<ValueStream> {
|
|
88
|
+
self.render_chunks_inner(context, Template::render_chunks_stream)
|
|
89
|
+
.await
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/// Render to chunks with dynamic output type
|
|
93
|
+
async fn render_chunks_inner<V>(
|
|
94
|
+
&self,
|
|
95
|
+
context: &TemplateContext,
|
|
96
|
+
render_string: impl AsyncFn(
|
|
97
|
+
&Template,
|
|
98
|
+
&TemplateContext,
|
|
99
|
+
) -> RenderedChunks<V>,
|
|
100
|
+
) -> RenderedChunks<V>
|
|
101
|
+
where
|
|
102
|
+
V: From<Value>,
|
|
103
|
+
{
|
|
104
|
+
match self {
|
|
105
|
+
Self::Null => Value::Null.into(),
|
|
106
|
+
Self::Boolean(b) => Value::Boolean(*b).into(),
|
|
107
|
+
Self::Integer(i) => Value::Integer(*i).into(),
|
|
108
|
+
Self::Float(f) => Value::Float(*f).into(),
|
|
109
|
+
Self::String(template) => render_string(template, context).await,
|
|
110
|
+
Self::Array(array) => {
|
|
111
|
+
// Render each value and collection into an Array
|
|
112
|
+
future::try_join_all(array.iter().map(|value| {
|
|
113
|
+
value
|
|
114
|
+
.render_chunks(context)
|
|
115
|
+
.map(RenderedChunks::try_into_value)
|
|
116
|
+
}))
|
|
117
|
+
.await
|
|
118
|
+
.map(Value::from)
|
|
119
|
+
.into() // Wrap into RenderedOutput
|
|
120
|
+
}
|
|
121
|
+
Self::Object(map) => {
|
|
122
|
+
// Render each key/value and collect into an Object
|
|
123
|
+
future::try_join_all(map.iter().map(|(key, value)| async {
|
|
124
|
+
let key = key.render_string(context).await?;
|
|
125
|
+
let value =
|
|
126
|
+
value.render_chunks(context).await.try_into_value()?;
|
|
127
|
+
Ok::<_, RenderError>((key, value))
|
|
128
|
+
}))
|
|
129
|
+
.await
|
|
130
|
+
.map(Value::from)
|
|
131
|
+
.into() // Wrap into RenderedOutput
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/// Parse template from a string literal. Panic if invalid
|
|
138
|
+
#[cfg(any(test, feature = "test"))]
|
|
139
|
+
impl From<&str> for ValueTemplate {
|
|
140
|
+
fn from(value: &str) -> Self {
|
|
141
|
+
let template = value.parse().unwrap();
|
|
142
|
+
Self::String(template)
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
#[cfg(any(test, feature = "test"))]
|
|
147
|
+
impl<T: Into<ValueTemplate>> From<Vec<T>> for ValueTemplate {
|
|
148
|
+
fn from(value: Vec<T>) -> Self {
|
|
149
|
+
Self::Array(value.into_iter().map(T::into).collect())
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
#[cfg(any(test, feature = "test"))]
|
|
154
|
+
impl<T: Into<ValueTemplate>> From<Vec<(&str, T)>> for ValueTemplate {
|
|
155
|
+
fn from(value: Vec<(&str, T)>) -> Self {
|
|
156
|
+
Self::Object(
|
|
157
|
+
value
|
|
158
|
+
.into_iter()
|
|
159
|
+
.map(|(k, v)| (k.parse().unwrap(), v.into()))
|
|
160
|
+
.collect(),
|
|
161
|
+
)
|
|
162
|
+
}
|
|
163
|
+
}
|
|
@@ -2,16 +2,16 @@
|
|
|
2
2
|
//! possible
|
|
3
3
|
|
|
4
4
|
mod cereal;
|
|
5
|
-
mod json;
|
|
6
5
|
mod models;
|
|
7
6
|
mod recipe_tree;
|
|
8
7
|
#[cfg(feature = "schema")]
|
|
9
8
|
mod schema;
|
|
9
|
+
mod value_template;
|
|
10
10
|
|
|
11
11
|
pub use cereal::HasId;
|
|
12
|
-
pub use json::{JsonTemplate, JsonTemplateError};
|
|
13
12
|
pub use models::*;
|
|
14
13
|
pub use recipe_tree::*;
|
|
14
|
+
pub use value_template::ValueTemplate;
|
|
15
15
|
|
|
16
16
|
use itertools::Itertools;
|
|
17
17
|
use std::{
|
|
@@ -337,11 +337,21 @@ requests:
|
|
|
337
337
|
name: Some("Profile 1".into()),
|
|
338
338
|
default: false,
|
|
339
339
|
data: indexmap! {
|
|
340
|
+
"host".into() => "https://httpbin.org".into(),
|
|
340
341
|
"user_guid".into() => "abc123".into(),
|
|
341
342
|
"username".into() =>
|
|
342
343
|
"xX{{ command(['whoami']) | trim() }}Xx".into(),
|
|
343
|
-
"
|
|
344
|
-
|
|
344
|
+
"null".into() => ValueTemplate::Null,
|
|
345
|
+
"bool".into() => true.into(),
|
|
346
|
+
"int".into() => 4.into(),
|
|
347
|
+
"float".into() => 123.45.into(),
|
|
348
|
+
"array".into() => ValueTemplate::Array(
|
|
349
|
+
vec![1.into(), 2.into(), "test".into()]
|
|
350
|
+
),
|
|
351
|
+
"object".into() => ValueTemplate::Object(vec![
|
|
352
|
+
("a".into(), 1.into()),
|
|
353
|
+
("b".into(), vec![1, 2, 7].into()),
|
|
354
|
+
]),
|
|
345
355
|
},
|
|
346
356
|
..Profile::factory(())
|
|
347
357
|
},
|
|
@@ -3,9 +3,11 @@
|
|
|
3
3
|
//! stages, meaning the request or response may not actually be present, if the
|
|
4
4
|
//! exchange is incomplete or failed.
|
|
5
5
|
|
|
6
|
-
use crate::
|
|
7
|
-
|
|
8
|
-
|
|
6
|
+
use crate::{
|
|
7
|
+
collection::{
|
|
8
|
+
Authentication, ProfileId, RecipeId, UnknownRecipeError, ValueTemplate,
|
|
9
|
+
},
|
|
10
|
+
util::json::JsonTemplateError,
|
|
9
11
|
};
|
|
10
12
|
use bytes::Bytes;
|
|
11
13
|
use chrono::{DateTime, Duration, Utc};
|
|
@@ -343,7 +345,7 @@ pub enum BodyOverride {
|
|
|
343
345
|
/// stream
|
|
344
346
|
Raw(Template),
|
|
345
347
|
/// Override with a JSON value
|
|
346
|
-
Json(
|
|
348
|
+
Json(ValueTemplate),
|
|
347
349
|
}
|
|
348
350
|
|
|
349
351
|
#[cfg(any(test, feature = "test"))]
|
|
@@ -34,18 +34,19 @@ thread_local! {
|
|
|
34
34
|
/// collection
|
|
35
35
|
fn template_context(recipe: Recipe, host: Option<&str>) -> TemplateContext {
|
|
36
36
|
let profile_data = indexmap! {
|
|
37
|
-
"host".into() => host.unwrap_or("http://localhost").
|
|
37
|
+
"host".into() => host.unwrap_or("http://localhost").into(),
|
|
38
38
|
"mode".into() => "sudo".into(),
|
|
39
39
|
"user_id".into() => "1".into(),
|
|
40
40
|
"group_id".into() => "3".into(),
|
|
41
41
|
"username".into() => "user".into(),
|
|
42
42
|
"password".into() => "hunter2".into(),
|
|
43
43
|
"token".into() => "tokenzzz".into(),
|
|
44
|
-
"test_data_dir".into() => test_data_dir().to_str().unwrap().
|
|
44
|
+
"test_data_dir".into() => test_data_dir().to_str().unwrap().into(),
|
|
45
45
|
"prompt".into() => "{{ prompt() }}".into(),
|
|
46
46
|
"stream".into() => "{{ file('data.json') }}".into(),
|
|
47
47
|
// Streamed value that we can use to test deduping
|
|
48
48
|
"stream_prompt".into() => "{{ file(concat([prompt(), '.txt'])) }}".into(),
|
|
49
|
+
"stream_compound".into() => "inner: {{ file('first.txt') }}".into(),
|
|
49
50
|
"error".into() => "{{ fake_fn() }}".into(),
|
|
50
51
|
};
|
|
51
52
|
let profile = Profile {
|
|
@@ -401,7 +402,7 @@ async fn test_authentication(
|
|
|
401
402
|
// JSON data is loaded as a string and NOT unpacked. file() returns bytes
|
|
402
403
|
// which automatically get interpreted as a string.
|
|
403
404
|
RecipeBody::json(json!(
|
|
404
|
-
"{{ file(concat([test_data_dir, '/data.json']))
|
|
405
|
+
"{{ file(concat([test_data_dir, '/data.json'])) }}"
|
|
405
406
|
)).unwrap(),
|
|
406
407
|
None,
|
|
407
408
|
Some("application/json"),
|
|
@@ -498,12 +499,18 @@ async fn test_body(
|
|
|
498
499
|
None,
|
|
499
500
|
r#"{ "a": 1, "b": 2 }"#,
|
|
500
501
|
)]
|
|
501
|
-
#[case::
|
|
502
|
+
#[case::stream_compound(
|
|
502
503
|
// This gets streamed one chunk at a time
|
|
503
504
|
RecipeBody::Stream(r#"{ "data": {{ file('data.json') }} }"#.into()),
|
|
504
505
|
None,
|
|
505
506
|
r#"{ "data": { "a": 1, "b": 2 } }"#,
|
|
506
507
|
)]
|
|
508
|
+
// Stream multiple chunks of data via a profile field
|
|
509
|
+
#[case::stream_compound_nested(
|
|
510
|
+
RecipeBody::Stream("outer: {{ stream_compound }}".into()),
|
|
511
|
+
None,
|
|
512
|
+
"outer: inner: first",
|
|
513
|
+
)]
|
|
507
514
|
#[case::form_multipart(
|
|
508
515
|
RecipeBody::FormMultipart(indexmap! {
|
|
509
516
|
"user_id".into() => "{{ user_id }}".into(),
|
|
@@ -47,6 +47,7 @@ use crate::{
|
|
|
47
47
|
database::CollectionDatabase,
|
|
48
48
|
http::curl::CurlBuilder,
|
|
49
49
|
render::TemplateContext,
|
|
50
|
+
util::json::value_to_json,
|
|
50
51
|
};
|
|
51
52
|
use bytes::{Bytes, BytesMut};
|
|
52
53
|
use chrono::Utc;
|
|
@@ -747,9 +748,9 @@ impl Recipe {
|
|
|
747
748
|
Some(BodyOverride::Raw(template)),
|
|
748
749
|
) => {
|
|
749
750
|
// Stream body is rendered as a stream (!!)
|
|
750
|
-
let
|
|
751
|
-
let source =
|
|
752
|
-
let stream =
|
|
751
|
+
let chunks = template.render_chunks_stream(context).await;
|
|
752
|
+
let source = chunks.stream_source().cloned();
|
|
753
|
+
let stream = chunks
|
|
753
754
|
.try_into_stream()
|
|
754
755
|
.map_err(RequestBuildErrorKind::BodyRender)?
|
|
755
756
|
.boxed();
|
|
@@ -763,10 +764,14 @@ impl Recipe {
|
|
|
763
764
|
| (Some(RecipeBody::Raw(_)), Some(BodyOverride::Json(json)))
|
|
764
765
|
| (Some(RecipeBody::Stream(_)), Some(BodyOverride::Json(json)))
|
|
765
766
|
| (Some(RecipeBody::Json(_)), Some(BodyOverride::Json(json))) => {
|
|
766
|
-
|
|
767
|
+
// Render the value
|
|
768
|
+
let rendered_value = json
|
|
769
|
+
.render_chunks(context)
|
|
767
770
|
.await
|
|
768
|
-
.
|
|
769
|
-
.map_err(RequestBuildErrorKind::BodyRender)
|
|
771
|
+
.try_into_value()
|
|
772
|
+
.map_err(RequestBuildErrorKind::BodyRender)?;
|
|
773
|
+
let json = value_to_json(rendered_value);
|
|
774
|
+
Ok(Some(RenderedBody::Json(json)))
|
|
770
775
|
}
|
|
771
776
|
|
|
772
777
|
// Form bodies
|
|
@@ -789,13 +794,13 @@ impl Recipe {
|
|
|
789
794
|
(Some(RecipeBody::FormMultipart(fields)), None) => {
|
|
790
795
|
let merged = apply_overrides(fields, &options.form_fields);
|
|
791
796
|
let iter = merged.into_iter().map(async |(field, template)| {
|
|
792
|
-
let
|
|
797
|
+
let chunks = template.render_chunks_stream(context).await;
|
|
793
798
|
// If this is a single-chunk template, we might be able to
|
|
794
799
|
// load directly from the source, since we support file
|
|
795
800
|
// streams natively. In that case, the stream will be thrown
|
|
796
801
|
// away
|
|
797
|
-
let source =
|
|
798
|
-
let stream =
|
|
802
|
+
let source = chunks.stream_source().cloned();
|
|
803
|
+
let stream = chunks
|
|
799
804
|
.try_into_stream()
|
|
800
805
|
.map_err(|error| {
|
|
801
806
|
RequestBuildErrorKind::BodyFormFieldRender {
|