slumber-python 5.2.3__tar.gz → 5.2.5__tar.gz

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