slumber-python 5.1.1__tar.gz → 5.2.0__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.0}/Cargo.lock +12 -11
- {slumber_python-5.1.1 → slumber_python-5.2.0}/Cargo.toml +9 -9
- {slumber_python-5.1.1 → slumber_python-5.2.0}/PKG-INFO +1 -1
- {slumber_python-5.1.1 → slumber_python-5.2.0}/crates/core/src/collection/cereal.rs +6 -17
- {slumber_python-5.1.1 → slumber_python-5.2.0}/crates/core/src/collection/models.rs +8 -6
- slumber_python-5.2.0/crates/core/src/collection/value_template.rs +163 -0
- {slumber_python-5.1.1 → slumber_python-5.2.0}/crates/core/src/collection.rs +14 -4
- {slumber_python-5.1.1 → slumber_python-5.2.0}/crates/core/src/http/models.rs +6 -4
- {slumber_python-5.1.1 → slumber_python-5.2.0}/crates/core/src/http/tests.rs +11 -4
- {slumber_python-5.1.1 → slumber_python-5.2.0}/crates/core/src/http.rs +14 -9
- {slumber_python-5.1.1 → slumber_python-5.2.0}/crates/core/src/render/functions.rs +10 -10
- {slumber_python-5.1.1 → slumber_python-5.2.0}/crates/core/src/render/tests.rs +69 -50
- {slumber_python-5.1.1 → slumber_python-5.2.0}/crates/core/src/render.rs +129 -100
- slumber_python-5.2.0/crates/core/src/util/json.rs +306 -0
- {slumber_python-5.1.1 → slumber_python-5.2.0}/crates/core/src/util.rs +2 -21
- {slumber_python-5.1.1 → slumber_python-5.2.0}/crates/macros/src/lib.rs +1 -1
- {slumber_python-5.1.1 → slumber_python-5.2.0}/crates/python/mise.toml +2 -2
- {slumber_python-5.1.1 → slumber_python-5.2.0}/crates/python/src/lib.rs +1 -1
- {slumber_python-5.1.1 → slumber_python-5.2.0}/crates/template/src/expression.rs +27 -17
- {slumber_python-5.1.1 → slumber_python-5.2.0}/crates/template/src/lib.rs +211 -122
- {slumber_python-5.1.1 → slumber_python-5.2.0}/crates/template/src/tests.rs +33 -14
- {slumber_python-5.1.1 → slumber_python-5.2.0}/crates/template/src/value.rs +73 -56
- {slumber_python-5.1.1 → slumber_python-5.2.0}/crates/util/src/lib.rs +23 -1
- {slumber_python-5.1.1 → slumber_python-5.2.0}/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.0}/README.md +0 -0
- {slumber_python-5.1.1 → slumber_python-5.2.0}/crates/config/Cargo.toml +0 -0
- {slumber_python-5.1.1 → slumber_python-5.2.0}/crates/config/src/cereal.rs +0 -0
- {slumber_python-5.1.1 → slumber_python-5.2.0}/crates/config/src/default.yml +0 -0
- {slumber_python-5.1.1 → slumber_python-5.2.0}/crates/config/src/default_old.yml +0 -0
- {slumber_python-5.1.1 → slumber_python-5.2.0}/crates/config/src/lib.rs +0 -0
- {slumber_python-5.1.1 → slumber_python-5.2.0}/crates/config/src/tui/cereal.rs +0 -0
- {slumber_python-5.1.1 → slumber_python-5.2.0}/crates/config/src/tui/input.rs +0 -0
- {slumber_python-5.1.1 → slumber_python-5.2.0}/crates/config/src/tui/mime.rs +0 -0
- {slumber_python-5.1.1 → slumber_python-5.2.0}/crates/config/src/tui/theme.rs +0 -0
- {slumber_python-5.1.1 → slumber_python-5.2.0}/crates/config/src/tui.rs +0 -0
- {slumber_python-5.1.1 → slumber_python-5.2.0}/crates/core/Cargo.toml +1 -1
- {slumber_python-5.1.1 → slumber_python-5.2.0}/crates/core/src/collection/recipe_tree.rs +0 -0
- {slumber_python-5.1.1 → slumber_python-5.2.0}/crates/core/src/collection/schema.rs +0 -0
- {slumber_python-5.1.1 → slumber_python-5.2.0}/crates/core/src/database/convert.rs +0 -0
- {slumber_python-5.1.1 → slumber_python-5.2.0}/crates/core/src/database/migrations.rs +0 -0
- {slumber_python-5.1.1 → slumber_python-5.2.0}/crates/core/src/database/tests.rs +0 -0
- {slumber_python-5.1.1 → slumber_python-5.2.0}/crates/core/src/database.rs +0 -0
- {slumber_python-5.1.1 → slumber_python-5.2.0}/crates/core/src/http/curl.rs +0 -0
- {slumber_python-5.1.1 → slumber_python-5.2.0}/crates/core/src/lib.rs +0 -0
- {slumber_python-5.1.1 → slumber_python-5.2.0}/crates/core/src/render/util.rs +0 -0
- {slumber_python-5.1.1 → slumber_python-5.2.0}/crates/core/src/test_util.rs +0 -0
- {slumber_python-5.1.1 → slumber_python-5.2.0}/crates/macros/Cargo.toml +0 -0
- {slumber_python-5.1.1 → slumber_python-5.2.0}/crates/python/Cargo.toml +0 -0
- {slumber_python-5.1.1 → slumber_python-5.2.0}/crates/python/README.md +0 -0
- {slumber_python-5.1.1 → slumber_python-5.2.0}/crates/python/dev.py +0 -0
- {slumber_python-5.1.1 → slumber_python-5.2.0}/crates/python/slumber.pyi +0 -0
- {slumber_python-5.1.1 → slumber_python-5.2.0}/crates/python/tests/slumber.yml +0 -0
- {slumber_python-5.1.1 → slumber_python-5.2.0}/crates/python/tests/test.py +0 -0
- {slumber_python-5.1.1 → slumber_python-5.2.0}/crates/python/uv.lock +0 -0
- {slumber_python-5.1.1 → slumber_python-5.2.0}/crates/template/Cargo.toml +0 -0
- {slumber_python-5.1.1 → slumber_python-5.2.0}/crates/template/proptest-regressions/parse.txt +0 -0
- {slumber_python-5.1.1 → slumber_python-5.2.0}/crates/template/src/cereal.rs +0 -0
- {slumber_python-5.1.1 → slumber_python-5.2.0}/crates/template/src/display.rs +0 -0
- {slumber_python-5.1.1 → slumber_python-5.2.0}/crates/template/src/error.rs +0 -0
- {slumber_python-5.1.1 → slumber_python-5.2.0}/crates/template/src/parse.rs +0 -0
- {slumber_python-5.1.1 → slumber_python-5.2.0}/crates/template/src/test_util.rs +0 -0
- {slumber_python-5.1.1 → slumber_python-5.2.0}/crates/util/Cargo.toml +0 -0
- {slumber_python-5.1.1 → slumber_python-5.2.0}/crates/util/src/paths.rs +0 -0
- {slumber_python-5.1.1 → slumber_python-5.2.0}/crates/util/src/yaml/error.rs +0 -0
- {slumber_python-5.1.1 → slumber_python-5.2.0}/crates/util/src/yaml/resolve.rs +0 -0
- {slumber_python-5.1.1 → slumber_python-5.2.0}/crates/util/src/yaml.rs +0 -0
- {slumber_python-5.1.1 → slumber_python-5.2.0}/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.0"
|
|
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.
|
|
3283
|
+
version = "5.2.0"
|
|
3284
3284
|
dependencies = [
|
|
3285
3285
|
"anyhow",
|
|
3286
3286
|
"console-subscriber",
|
|
@@ -3294,7 +3294,7 @@ dependencies = [
|
|
|
3294
3294
|
|
|
3295
3295
|
[[package]]
|
|
3296
3296
|
name = "slumber_cli"
|
|
3297
|
-
version = "5.
|
|
3297
|
+
version = "5.2.0"
|
|
3298
3298
|
dependencies = [
|
|
3299
3299
|
"anyhow",
|
|
3300
3300
|
"assert_cmd",
|
|
@@ -3329,7 +3329,7 @@ dependencies = [
|
|
|
3329
3329
|
|
|
3330
3330
|
[[package]]
|
|
3331
3331
|
name = "slumber_config"
|
|
3332
|
-
version = "5.
|
|
3332
|
+
version = "5.2.0"
|
|
3333
3333
|
dependencies = [
|
|
3334
3334
|
"derive_more",
|
|
3335
3335
|
"dirs",
|
|
@@ -3354,7 +3354,7 @@ dependencies = [
|
|
|
3354
3354
|
|
|
3355
3355
|
[[package]]
|
|
3356
3356
|
name = "slumber_core"
|
|
3357
|
-
version = "5.
|
|
3357
|
+
version = "5.2.0"
|
|
3358
3358
|
dependencies = [
|
|
3359
3359
|
"async-trait",
|
|
3360
3360
|
"base64 0.22.1",
|
|
@@ -3402,7 +3402,7 @@ dependencies = [
|
|
|
3402
3402
|
|
|
3403
3403
|
[[package]]
|
|
3404
3404
|
name = "slumber_import"
|
|
3405
|
-
version = "5.
|
|
3405
|
+
version = "5.2.0"
|
|
3406
3406
|
dependencies = [
|
|
3407
3407
|
"anyhow",
|
|
3408
3408
|
"base64 0.22.1",
|
|
@@ -3434,7 +3434,7 @@ dependencies = [
|
|
|
3434
3434
|
|
|
3435
3435
|
[[package]]
|
|
3436
3436
|
name = "slumber_macros"
|
|
3437
|
-
version = "5.
|
|
3437
|
+
version = "5.2.0"
|
|
3438
3438
|
dependencies = [
|
|
3439
3439
|
"proc-macro2",
|
|
3440
3440
|
"quote",
|
|
@@ -3443,7 +3443,7 @@ dependencies = [
|
|
|
3443
3443
|
|
|
3444
3444
|
[[package]]
|
|
3445
3445
|
name = "slumber_python"
|
|
3446
|
-
version = "5.
|
|
3446
|
+
version = "5.2.0"
|
|
3447
3447
|
dependencies = [
|
|
3448
3448
|
"async-trait",
|
|
3449
3449
|
"dialoguer",
|
|
@@ -3457,7 +3457,7 @@ dependencies = [
|
|
|
3457
3457
|
|
|
3458
3458
|
[[package]]
|
|
3459
3459
|
name = "slumber_template"
|
|
3460
|
-
version = "5.
|
|
3460
|
+
version = "5.2.0"
|
|
3461
3461
|
dependencies = [
|
|
3462
3462
|
"bytes",
|
|
3463
3463
|
"derive_more",
|
|
@@ -3484,7 +3484,7 @@ dependencies = [
|
|
|
3484
3484
|
|
|
3485
3485
|
[[package]]
|
|
3486
3486
|
name = "slumber_tui"
|
|
3487
|
-
version = "5.
|
|
3487
|
+
version = "5.2.0"
|
|
3488
3488
|
dependencies = [
|
|
3489
3489
|
"anyhow",
|
|
3490
3490
|
"async-trait",
|
|
@@ -3520,12 +3520,13 @@ dependencies = [
|
|
|
3520
3520
|
"tree-sitter-json",
|
|
3521
3521
|
"unicode-width",
|
|
3522
3522
|
"uuid",
|
|
3523
|
+
"winnow",
|
|
3523
3524
|
"wiremock",
|
|
3524
3525
|
]
|
|
3525
3526
|
|
|
3526
3527
|
[[package]]
|
|
3527
3528
|
name = "slumber_util"
|
|
3528
|
-
version = "5.
|
|
3529
|
+
version = "5.2.0"
|
|
3529
3530
|
dependencies = [
|
|
3530
3531
|
"derive_more",
|
|
3531
3532
|
"dirs",
|
|
@@ -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.0"
|
|
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.0" }
|
|
47
|
+
slumber_config = {path = "./crates/config", version = "5.2.0", default-features = false }
|
|
48
|
+
slumber_core = {path = "./crates/core", version = "5.2.0" }
|
|
49
|
+
slumber_import = {path = "./crates/import", version = "5.2.0" }
|
|
50
|
+
slumber_macros = {path = "./crates/macros", version = "5.2.0" }
|
|
51
|
+
slumber_template = {path = "./crates/template", version = "5.2.0" }
|
|
52
|
+
slumber_tui = {path = "./crates/tui", version = "5.2.0" }
|
|
53
|
+
slumber_util = {path = "./crates/util", version = "5.2.0" }
|
|
54
54
|
strum = {version = "0.27.0", default-features = false}
|
|
55
55
|
syn = "2.0.101"
|
|
56
56
|
terminput = "0.5.3"
|
|
@@ -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 {
|
|
@@ -13,8 +13,8 @@ use regex::Regex;
|
|
|
13
13
|
use serde::{Deserialize, de::IntoDeserializer};
|
|
14
14
|
use slumber_macros::template;
|
|
15
15
|
use slumber_template::{
|
|
16
|
-
Expected,
|
|
17
|
-
|
|
16
|
+
Expected, RenderError, StreamSource, TryFromValue, Value, ValueError,
|
|
17
|
+
ValueStream, WithValue, impl_try_from_value_str,
|
|
18
18
|
};
|
|
19
19
|
use slumber_util::{TimeSpan, paths::expand_home};
|
|
20
20
|
use std::{
|
|
@@ -112,7 +112,7 @@ pub fn boolean(value: Value) -> bool {
|
|
|
112
112
|
/// stdin:
|
|
113
113
|
/// description: Data to pipe to the subprocess's stdin
|
|
114
114
|
/// default: "b''"
|
|
115
|
-
/// return: Stdout output as bytes
|
|
115
|
+
/// return: Stdout output as bytes
|
|
116
116
|
/// errors:
|
|
117
117
|
/// - If the command fails to initialize (e.g. program unknown)
|
|
118
118
|
/// - If the subprocess exits with a non-zero status code
|
|
@@ -128,7 +128,7 @@ pub fn command(
|
|
|
128
128
|
command: Vec<String>,
|
|
129
129
|
#[kwarg] cwd: Option<String>,
|
|
130
130
|
#[kwarg] stdin: Option<Bytes>,
|
|
131
|
-
) -> Result<
|
|
131
|
+
) -> Result<ValueStream, FunctionError> {
|
|
132
132
|
/// Wrap an IO error
|
|
133
133
|
fn io_error(
|
|
134
134
|
program: &str,
|
|
@@ -222,7 +222,7 @@ pub fn command(
|
|
|
222
222
|
|
|
223
223
|
let stream = future.try_flatten_stream().boxed();
|
|
224
224
|
|
|
225
|
-
Ok(
|
|
225
|
+
Ok(ValueStream::Stream {
|
|
226
226
|
source: StreamSource::Command { command },
|
|
227
227
|
stream,
|
|
228
228
|
})
|
|
@@ -304,7 +304,7 @@ pub fn env(variable: String, #[kwarg] default: String) -> String {
|
|
|
304
304
|
/// output: Contents of config.json file
|
|
305
305
|
/// ```
|
|
306
306
|
#[template]
|
|
307
|
-
pub fn file(#[context] context: &TemplateContext, path: String) ->
|
|
307
|
+
pub fn file(#[context] context: &TemplateContext, path: String) -> ValueStream {
|
|
308
308
|
let path = context.root_dir.join(expand_home(PathBuf::from(path)));
|
|
309
309
|
let source = StreamSource::File { path: path.clone() };
|
|
310
310
|
// Return the file as a stream. If streaming isn't available here, it will
|
|
@@ -317,7 +317,7 @@ pub fn file(#[context] context: &TemplateContext, path: String) -> LazyValue {
|
|
|
317
317
|
.map_err(|error| FunctionError::File { path, error })?;
|
|
318
318
|
Ok(reader_stream(file))
|
|
319
319
|
};
|
|
320
|
-
|
|
320
|
+
ValueStream::Stream {
|
|
321
321
|
source,
|
|
322
322
|
stream: future.try_flatten_stream().boxed(),
|
|
323
323
|
}
|
|
@@ -879,7 +879,7 @@ pub async fn prompt(
|
|
|
879
879
|
sensitive,
|
|
880
880
|
channel: tx.into(),
|
|
881
881
|
});
|
|
882
|
-
let
|
|
882
|
+
let chunks = rx.await.map_err(|_| FunctionError::PromptNoReply)?;
|
|
883
883
|
|
|
884
884
|
// If the input was sensitive, we should mask the output as well. This only
|
|
885
885
|
// impacts previews as show_sensitive is enabled for request renders. This
|
|
@@ -888,9 +888,9 @@ pub async fn prompt(
|
|
|
888
888
|
// "<prompt>", we show "••••••••". It's "technically" right and plays well
|
|
889
889
|
// in tests. Also it reminds users that a prompt is sensitive in the TUI
|
|
890
890
|
if sensitive {
|
|
891
|
-
Ok(mask_sensitive(context,
|
|
891
|
+
Ok(mask_sensitive(context, chunks))
|
|
892
892
|
} else {
|
|
893
|
-
Ok(
|
|
893
|
+
Ok(chunks)
|
|
894
894
|
}
|
|
895
895
|
}
|
|
896
896
|
|