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.
Files changed (68) hide show
  1. {slumber_python-5.1.1 → slumber_python-5.2.3}/Cargo.lock +13 -13
  2. {slumber_python-5.1.1 → slumber_python-5.2.3}/Cargo.toml +23 -11
  3. {slumber_python-5.1.1 → slumber_python-5.2.3}/PKG-INFO +1 -1
  4. {slumber_python-5.1.1 → slumber_python-5.2.3}/crates/core/src/collection/cereal.rs +6 -17
  5. {slumber_python-5.1.1 → slumber_python-5.2.3}/crates/core/src/collection/models.rs +8 -6
  6. slumber_python-5.2.3/crates/core/src/collection/value_template.rs +163 -0
  7. {slumber_python-5.1.1 → slumber_python-5.2.3}/crates/core/src/collection.rs +14 -4
  8. {slumber_python-5.1.1 → slumber_python-5.2.3}/crates/core/src/http/models.rs +6 -4
  9. {slumber_python-5.1.1 → slumber_python-5.2.3}/crates/core/src/http/tests.rs +11 -4
  10. {slumber_python-5.1.1 → slumber_python-5.2.3}/crates/core/src/http.rs +14 -9
  11. {slumber_python-5.1.1 → slumber_python-5.2.3}/crates/core/src/render/functions.rs +10 -10
  12. {slumber_python-5.1.1 → slumber_python-5.2.3}/crates/core/src/render/tests.rs +69 -50
  13. {slumber_python-5.1.1 → slumber_python-5.2.3}/crates/core/src/render.rs +129 -100
  14. slumber_python-5.2.3/crates/core/src/util/json.rs +306 -0
  15. {slumber_python-5.1.1 → slumber_python-5.2.3}/crates/core/src/util.rs +2 -21
  16. {slumber_python-5.1.1 → slumber_python-5.2.3}/crates/macros/src/lib.rs +1 -1
  17. {slumber_python-5.1.1 → slumber_python-5.2.3}/crates/python/mise.toml +2 -2
  18. {slumber_python-5.1.1 → slumber_python-5.2.3}/crates/python/src/lib.rs +1 -1
  19. {slumber_python-5.1.1 → slumber_python-5.2.3}/crates/template/src/expression.rs +27 -17
  20. {slumber_python-5.1.1 → slumber_python-5.2.3}/crates/template/src/lib.rs +207 -122
  21. {slumber_python-5.1.1 → slumber_python-5.2.3}/crates/template/src/tests.rs +33 -14
  22. {slumber_python-5.1.1 → slumber_python-5.2.3}/crates/template/src/value.rs +73 -56
  23. {slumber_python-5.1.1 → slumber_python-5.2.3}/crates/util/Cargo.toml +5 -3
  24. {slumber_python-5.1.1 → slumber_python-5.2.3}/crates/util/src/lib.rs +97 -2
  25. {slumber_python-5.1.1 → slumber_python-5.2.3}/crates/util/src/test_util.rs +13 -3
  26. slumber_python-5.1.1/crates/core/src/collection/json.rs +0 -252
  27. {slumber_python-5.1.1 → slumber_python-5.2.3}/README.md +0 -0
  28. {slumber_python-5.1.1 → slumber_python-5.2.3}/crates/config/Cargo.toml +0 -0
  29. {slumber_python-5.1.1 → slumber_python-5.2.3}/crates/config/src/cereal.rs +0 -0
  30. {slumber_python-5.1.1 → slumber_python-5.2.3}/crates/config/src/default.yml +0 -0
  31. {slumber_python-5.1.1 → slumber_python-5.2.3}/crates/config/src/default_old.yml +0 -0
  32. {slumber_python-5.1.1 → slumber_python-5.2.3}/crates/config/src/lib.rs +0 -0
  33. {slumber_python-5.1.1 → slumber_python-5.2.3}/crates/config/src/tui/cereal.rs +0 -0
  34. {slumber_python-5.1.1 → slumber_python-5.2.3}/crates/config/src/tui/input.rs +0 -0
  35. {slumber_python-5.1.1 → slumber_python-5.2.3}/crates/config/src/tui/mime.rs +0 -0
  36. {slumber_python-5.1.1 → slumber_python-5.2.3}/crates/config/src/tui/theme.rs +0 -0
  37. {slumber_python-5.1.1 → slumber_python-5.2.3}/crates/config/src/tui.rs +0 -0
  38. {slumber_python-5.1.1 → slumber_python-5.2.3}/crates/core/Cargo.toml +1 -1
  39. {slumber_python-5.1.1 → slumber_python-5.2.3}/crates/core/src/collection/recipe_tree.rs +0 -0
  40. {slumber_python-5.1.1 → slumber_python-5.2.3}/crates/core/src/collection/schema.rs +0 -0
  41. {slumber_python-5.1.1 → slumber_python-5.2.3}/crates/core/src/database/convert.rs +0 -0
  42. {slumber_python-5.1.1 → slumber_python-5.2.3}/crates/core/src/database/migrations.rs +0 -0
  43. {slumber_python-5.1.1 → slumber_python-5.2.3}/crates/core/src/database/tests.rs +0 -0
  44. {slumber_python-5.1.1 → slumber_python-5.2.3}/crates/core/src/database.rs +0 -0
  45. {slumber_python-5.1.1 → slumber_python-5.2.3}/crates/core/src/http/curl.rs +0 -0
  46. {slumber_python-5.1.1 → slumber_python-5.2.3}/crates/core/src/lib.rs +0 -0
  47. {slumber_python-5.1.1 → slumber_python-5.2.3}/crates/core/src/render/util.rs +0 -0
  48. {slumber_python-5.1.1 → slumber_python-5.2.3}/crates/core/src/test_util.rs +0 -0
  49. {slumber_python-5.1.1 → slumber_python-5.2.3}/crates/macros/Cargo.toml +0 -0
  50. {slumber_python-5.1.1 → slumber_python-5.2.3}/crates/python/Cargo.toml +0 -0
  51. {slumber_python-5.1.1 → slumber_python-5.2.3}/crates/python/README.md +0 -0
  52. {slumber_python-5.1.1 → slumber_python-5.2.3}/crates/python/dev.py +0 -0
  53. {slumber_python-5.1.1 → slumber_python-5.2.3}/crates/python/slumber.pyi +0 -0
  54. {slumber_python-5.1.1 → slumber_python-5.2.3}/crates/python/tests/slumber.yml +0 -0
  55. {slumber_python-5.1.1 → slumber_python-5.2.3}/crates/python/tests/test.py +0 -0
  56. {slumber_python-5.1.1 → slumber_python-5.2.3}/crates/python/uv.lock +0 -0
  57. {slumber_python-5.1.1 → slumber_python-5.2.3}/crates/template/Cargo.toml +0 -0
  58. {slumber_python-5.1.1 → slumber_python-5.2.3}/crates/template/proptest-regressions/parse.txt +0 -0
  59. {slumber_python-5.1.1 → slumber_python-5.2.3}/crates/template/src/cereal.rs +0 -0
  60. {slumber_python-5.1.1 → slumber_python-5.2.3}/crates/template/src/display.rs +0 -0
  61. {slumber_python-5.1.1 → slumber_python-5.2.3}/crates/template/src/error.rs +0 -0
  62. {slumber_python-5.1.1 → slumber_python-5.2.3}/crates/template/src/parse.rs +0 -0
  63. {slumber_python-5.1.1 → slumber_python-5.2.3}/crates/template/src/test_util.rs +0 -0
  64. {slumber_python-5.1.1 → slumber_python-5.2.3}/crates/util/src/paths.rs +0 -0
  65. {slumber_python-5.1.1 → slumber_python-5.2.3}/crates/util/src/yaml/error.rs +0 -0
  66. {slumber_python-5.1.1 → slumber_python-5.2.3}/crates/util/src/yaml/resolve.rs +0 -0
  67. {slumber_python-5.1.1 → slumber_python-5.2.3}/crates/util/src/yaml.rs +0 -0
  68. {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.1.1"
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.1.1"
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.1.1"
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.1.1"
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.1.1"
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.1.1"
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.1.1"
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.1.1"
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.1.1"
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.1.1"
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.1.1"
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.1.1"
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.1.1" }
47
- slumber_config = {path = "./crates/config", version = "5.1.1", default-features = false }
48
- slumber_core = {path = "./crates/core", version = "5.1.1" }
49
- slumber_import = {path = "./crates/import", version = "5.1.1" }
50
- slumber_macros = {path = "./crates/macros", version = "5.1.1" }
51
- slumber_template = {path = "./crates/template", version = "5.1.1" }
52
- slumber_tui = {path = "./crates/tui", version = "5.1.1" }
53
- slumber_util = {path = "./crates/util", version = "5.1.1" }
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 = ["aarch64-apple-darwin", "x86_64-apple-darwin", "x86_64-unknown-linux-gnu", "x86_64-unknown-linux-musl", "x86_64-pc-windows-msvc"]
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
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: slumber-python
3
- Version: 5.1.1
3
+ Version: 5.2.3
4
4
  Classifier: Programming Language :: Rust
5
5
  Classifier: Programming Language :: Python :: Implementation :: CPython
6
6
  Classifier: Programming Language :: Python :: Implementation :: PyPy
@@ -9,9 +9,9 @@
9
9
 
10
10
  use crate::{
11
11
  collection::{
12
- Authentication, Collection, Folder, JsonTemplate, Profile, ProfileId,
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 JsonTemplate {
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::Bool(b)),
408
- YamlData::Value(Scalar::Integer(i)) => Ok(Self::Number(i.into())),
409
- YamlData::Value(Scalar::FloatingPoint(f)) => Ok(Self::Number(
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
- r#"{"message": "hello"}"#.parse().unwrap(),
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
- pub data: IndexMap<String, Template>,
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(JsonTemplate),
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(JsonTemplate::raw(value))
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
- "host".into() => "https://httpbin.org".into(),
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::collection::{
7
- Authentication, JsonTemplate, JsonTemplateError, ProfileId, RecipeId,
8
- UnknownRecipeError,
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(JsonTemplate),
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").parse().unwrap(),
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().parse().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'])) | trim() }}"
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::stream_multichunk(
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 output = template.render(&context.stream()).await;
751
- let source = output.stream_source().cloned();
752
- let stream = output
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
- json.render_json(context)
767
+ // Render the value
768
+ let rendered_value = json
769
+ .render_chunks(context)
767
770
  .await
768
- .map(|value| Some(RenderedBody::Json(value)))
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 output = template.render(&context.stream()).await;
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 = output.stream_source().cloned();
798
- let stream = output
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 {