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.
Files changed (68) hide show
  1. {slumber_python-5.1.1 → slumber_python-5.2.0}/Cargo.lock +12 -11
  2. {slumber_python-5.1.1 → slumber_python-5.2.0}/Cargo.toml +9 -9
  3. {slumber_python-5.1.1 → slumber_python-5.2.0}/PKG-INFO +1 -1
  4. {slumber_python-5.1.1 → slumber_python-5.2.0}/crates/core/src/collection/cereal.rs +6 -17
  5. {slumber_python-5.1.1 → slumber_python-5.2.0}/crates/core/src/collection/models.rs +8 -6
  6. slumber_python-5.2.0/crates/core/src/collection/value_template.rs +163 -0
  7. {slumber_python-5.1.1 → slumber_python-5.2.0}/crates/core/src/collection.rs +14 -4
  8. {slumber_python-5.1.1 → slumber_python-5.2.0}/crates/core/src/http/models.rs +6 -4
  9. {slumber_python-5.1.1 → slumber_python-5.2.0}/crates/core/src/http/tests.rs +11 -4
  10. {slumber_python-5.1.1 → slumber_python-5.2.0}/crates/core/src/http.rs +14 -9
  11. {slumber_python-5.1.1 → slumber_python-5.2.0}/crates/core/src/render/functions.rs +10 -10
  12. {slumber_python-5.1.1 → slumber_python-5.2.0}/crates/core/src/render/tests.rs +69 -50
  13. {slumber_python-5.1.1 → slumber_python-5.2.0}/crates/core/src/render.rs +129 -100
  14. slumber_python-5.2.0/crates/core/src/util/json.rs +306 -0
  15. {slumber_python-5.1.1 → slumber_python-5.2.0}/crates/core/src/util.rs +2 -21
  16. {slumber_python-5.1.1 → slumber_python-5.2.0}/crates/macros/src/lib.rs +1 -1
  17. {slumber_python-5.1.1 → slumber_python-5.2.0}/crates/python/mise.toml +2 -2
  18. {slumber_python-5.1.1 → slumber_python-5.2.0}/crates/python/src/lib.rs +1 -1
  19. {slumber_python-5.1.1 → slumber_python-5.2.0}/crates/template/src/expression.rs +27 -17
  20. {slumber_python-5.1.1 → slumber_python-5.2.0}/crates/template/src/lib.rs +211 -122
  21. {slumber_python-5.1.1 → slumber_python-5.2.0}/crates/template/src/tests.rs +33 -14
  22. {slumber_python-5.1.1 → slumber_python-5.2.0}/crates/template/src/value.rs +73 -56
  23. {slumber_python-5.1.1 → slumber_python-5.2.0}/crates/util/src/lib.rs +23 -1
  24. {slumber_python-5.1.1 → slumber_python-5.2.0}/crates/util/src/test_util.rs +13 -3
  25. slumber_python-5.1.1/crates/core/src/collection/json.rs +0 -252
  26. {slumber_python-5.1.1 → slumber_python-5.2.0}/README.md +0 -0
  27. {slumber_python-5.1.1 → slumber_python-5.2.0}/crates/config/Cargo.toml +0 -0
  28. {slumber_python-5.1.1 → slumber_python-5.2.0}/crates/config/src/cereal.rs +0 -0
  29. {slumber_python-5.1.1 → slumber_python-5.2.0}/crates/config/src/default.yml +0 -0
  30. {slumber_python-5.1.1 → slumber_python-5.2.0}/crates/config/src/default_old.yml +0 -0
  31. {slumber_python-5.1.1 → slumber_python-5.2.0}/crates/config/src/lib.rs +0 -0
  32. {slumber_python-5.1.1 → slumber_python-5.2.0}/crates/config/src/tui/cereal.rs +0 -0
  33. {slumber_python-5.1.1 → slumber_python-5.2.0}/crates/config/src/tui/input.rs +0 -0
  34. {slumber_python-5.1.1 → slumber_python-5.2.0}/crates/config/src/tui/mime.rs +0 -0
  35. {slumber_python-5.1.1 → slumber_python-5.2.0}/crates/config/src/tui/theme.rs +0 -0
  36. {slumber_python-5.1.1 → slumber_python-5.2.0}/crates/config/src/tui.rs +0 -0
  37. {slumber_python-5.1.1 → slumber_python-5.2.0}/crates/core/Cargo.toml +1 -1
  38. {slumber_python-5.1.1 → slumber_python-5.2.0}/crates/core/src/collection/recipe_tree.rs +0 -0
  39. {slumber_python-5.1.1 → slumber_python-5.2.0}/crates/core/src/collection/schema.rs +0 -0
  40. {slumber_python-5.1.1 → slumber_python-5.2.0}/crates/core/src/database/convert.rs +0 -0
  41. {slumber_python-5.1.1 → slumber_python-5.2.0}/crates/core/src/database/migrations.rs +0 -0
  42. {slumber_python-5.1.1 → slumber_python-5.2.0}/crates/core/src/database/tests.rs +0 -0
  43. {slumber_python-5.1.1 → slumber_python-5.2.0}/crates/core/src/database.rs +0 -0
  44. {slumber_python-5.1.1 → slumber_python-5.2.0}/crates/core/src/http/curl.rs +0 -0
  45. {slumber_python-5.1.1 → slumber_python-5.2.0}/crates/core/src/lib.rs +0 -0
  46. {slumber_python-5.1.1 → slumber_python-5.2.0}/crates/core/src/render/util.rs +0 -0
  47. {slumber_python-5.1.1 → slumber_python-5.2.0}/crates/core/src/test_util.rs +0 -0
  48. {slumber_python-5.1.1 → slumber_python-5.2.0}/crates/macros/Cargo.toml +0 -0
  49. {slumber_python-5.1.1 → slumber_python-5.2.0}/crates/python/Cargo.toml +0 -0
  50. {slumber_python-5.1.1 → slumber_python-5.2.0}/crates/python/README.md +0 -0
  51. {slumber_python-5.1.1 → slumber_python-5.2.0}/crates/python/dev.py +0 -0
  52. {slumber_python-5.1.1 → slumber_python-5.2.0}/crates/python/slumber.pyi +0 -0
  53. {slumber_python-5.1.1 → slumber_python-5.2.0}/crates/python/tests/slumber.yml +0 -0
  54. {slumber_python-5.1.1 → slumber_python-5.2.0}/crates/python/tests/test.py +0 -0
  55. {slumber_python-5.1.1 → slumber_python-5.2.0}/crates/python/uv.lock +0 -0
  56. {slumber_python-5.1.1 → slumber_python-5.2.0}/crates/template/Cargo.toml +0 -0
  57. {slumber_python-5.1.1 → slumber_python-5.2.0}/crates/template/proptest-regressions/parse.txt +0 -0
  58. {slumber_python-5.1.1 → slumber_python-5.2.0}/crates/template/src/cereal.rs +0 -0
  59. {slumber_python-5.1.1 → slumber_python-5.2.0}/crates/template/src/display.rs +0 -0
  60. {slumber_python-5.1.1 → slumber_python-5.2.0}/crates/template/src/error.rs +0 -0
  61. {slumber_python-5.1.1 → slumber_python-5.2.0}/crates/template/src/parse.rs +0 -0
  62. {slumber_python-5.1.1 → slumber_python-5.2.0}/crates/template/src/test_util.rs +0 -0
  63. {slumber_python-5.1.1 → slumber_python-5.2.0}/crates/util/Cargo.toml +0 -0
  64. {slumber_python-5.1.1 → slumber_python-5.2.0}/crates/util/src/paths.rs +0 -0
  65. {slumber_python-5.1.1 → slumber_python-5.2.0}/crates/util/src/yaml/error.rs +0 -0
  66. {slumber_python-5.1.1 → slumber_python-5.2.0}/crates/util/src/yaml/resolve.rs +0 -0
  67. {slumber_python-5.1.1 → slumber_python-5.2.0}/crates/util/src/yaml.rs +0 -0
  68. {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.1.1"
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.1.1"
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.1.1"
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.1.1"
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.1.1"
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.1.1"
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.1.1"
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.1.1"
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.1.1"
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.1.1"
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.1.1"
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.1.1"
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.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.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"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: slumber-python
3
- Version: 5.1.1
3
+ Version: 5.2.0
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 {
@@ -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, LazyValue, RenderError, StreamSource, TryFromValue, Value,
17
- ValueError, WithValue, impl_try_from_value_str,
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. May be returned as a stream (LazyValue).
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<LazyValue, FunctionError> {
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(LazyValue::Stream {
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) -> LazyValue {
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
- LazyValue::Stream {
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 output = rx.await.map_err(|_| FunctionError::PromptNoReply)?;
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, output))
891
+ Ok(mask_sensitive(context, chunks))
892
892
  } else {
893
- Ok(output)
893
+ Ok(chunks)
894
894
  }
895
895
  }
896
896