From 3d6e6910e63ed07ad98bf435d059d04ad4bb9c06 Mon Sep 17 00:00:00 2001 From: Lucas Pickering Date: Wed, 15 Nov 2023 06:05:18 -0500 Subject: [PATCH 1/2] Decrease framerate from 10fps to 4fps This will lower CPU usage without a noticeable change to responsiveness. --- src/tui.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/tui.rs b/src/tui.rs index 59f84ed2..39b22e9b 100644 --- a/src/tui.rs +++ b/src/tui.rs @@ -58,7 +58,7 @@ pub struct Tui { impl Tui { /// Rough maximum time for each iteration of the main loop - const TICK_TIME: Duration = Duration::from_millis(100); + const TICK_TIME: Duration = Duration::from_millis(250); /// Start the TUI. Any errors that occur during startup will be panics, /// because they prevent TUI execution. From 6308118891bb06cef830e781d1795de2c2c64882 Mon Sep 17 00:00:00 2001 From: Lucas Pickering Date: Tue, 14 Nov 2023 19:57:11 -0500 Subject: [PATCH 2/2] Add recursive templates for profile values --- CHANGELOG.md | 4 + docs/src/SUMMARY.md | 1 + docs/src/api/chain_source.md | 4 +- docs/src/api/profile.md | 15 ++-- docs/src/api/profile_value.md | 23 +++++ slumber.yml | 3 +- src/cli.rs | 2 +- src/collection.rs | 161 +++++++++++++++++++++++++++++++++- src/collection/insomnia.rs | 6 +- src/http.rs | 2 +- src/http/parse.rs | 2 +- src/template.rs | 57 ++++++------ src/template/error.rs | 25 +++--- src/template/render.rs | 21 ++++- src/tui.rs | 4 +- src/tui/input.rs | 2 +- src/util.rs | 10 +-- 17 files changed, 277 insertions(+), 65 deletions(-) create mode 100644 docs/src/api/profile_value.md diff --git a/CHANGELOG.md b/CHANGELOG.md index 1c035666..98eeaff6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,10 @@ ## [Unreleased] - ReleaseDate +### Added + +- Added recursive templates for profile values, using the `!template` tag before a value + ### Changed - Parse templates up front instead of during render diff --git a/docs/src/SUMMARY.md b/docs/src/SUMMARY.md index ab10c26c..bcef889c 100644 --- a/docs/src/SUMMARY.md +++ b/docs/src/SUMMARY.md @@ -15,6 +15,7 @@ - [Request Collection](./api/request_collection.md) - [Profile](./api/profile.md) + - [Profile Value](./api/profile_value.md) - [Request Recipe](./api/request_recipe.md) - [Chain](./api/chain.md) - [Chain Source](./api/chain_source.md) diff --git a/docs/src/api/chain_source.md b/docs/src/api/chain_source.md index 060a54ad..80fefb6d 100644 --- a/docs/src/api/chain_source.md +++ b/docs/src/api/chain_source.md @@ -2,9 +2,9 @@ A chain source defines how a [Chain](./chain.md) gets its value. It populates the `source` field of a chain. There are multiple source types, and the type is specified using [YAML's tag syntax](https://yaml.org/spec/1.2.2/#24-tags). -## Types +## Variants -| Type | Type | Value | Chained Value | +| Variant | Type | Value | Chained Value | | --------- | ---------- | ------------------------------------ | --------------------------------------------------------------- | | `request` | `string` | Request Recipe ID | Body of the most recent response for a specific request recipe. | | `command` | `string[]` | `[program, ...arguments]` | Stdout of the executed command | diff --git a/docs/src/api/profile.md b/docs/src/api/profile.md index f17445f4..b0fbc8a7 100644 --- a/docs/src/api/profile.md +++ b/docs/src/api/profile.md @@ -2,13 +2,15 @@ A profile is a collection of static template values. It's useful for configuring and switching between multiple different environments/settings/etc. +Profiles also support nested templates, via the `!template` tag. + ## Fields -| Field | Type | Description | Default | -| ------ | ------------------------- | ------------------------------------- | ------------- | -| `id` | `string` | Unique identifier for this profile | Required | -| `name` | `string` | Descriptive name to use in the UI | Value of `id` | -| `data` | `mapping[string, string]` | Fields, mapped to their static values | `{}` | +| โ‰ˆ | Field | Type | Description | Default | +| ------ | ----------------------------------------------------- | ---------------------------------- | ------------- | ------- | +| `id` | `string` | Unique identifier for this profile | Required | +| `name` | `string` | Descriptive name to use in the UI | Value of `id` | +| `data` | [`mapping[string, ProfileValue]`](./profile_value.md) | Fields, mapped to their values | `{}` | ## Examples @@ -16,6 +18,7 @@ A profile is a collection of static template values. It's useful for configuring id: local name: Local data: - host: http://localhost:5000 + host: localhost:5000 + url: !template "https://{{host}}" user_guid: abc123 ``` diff --git a/docs/src/api/profile_value.md b/docs/src/api/profile_value.md new file mode 100644 index 00000000..32f4ca49 --- /dev/null +++ b/docs/src/api/profile_value.md @@ -0,0 +1,23 @@ +# Profile Value + +A profile value is the value associated with a particular key in a profile. Typically profile values are just simple strings, but they can also be other variants. + +In the case of a nested template, the inner template will be rendered into its own value, then injected into the outer string. + +## Variants + +| Variant | Type | Description | +| ---------- | --------------------------- | --------------------------------------------- | +| `raw` | `string` | Static string (key can optionally be omitted) | +| `template` | [`Template`](./template.md) | Nested template, to be rendered inline | + +## Examples + +```yaml +!raw http://localhost:5000 +--- +# The !raw key is the default, and can be omitted +http://localhost:5000 +--- +!template http://{{hostname}} +``` diff --git a/slumber.yml b/slumber.yml index 7b436388..8e9a073a 100644 --- a/slumber.yml +++ b/slumber.yml @@ -5,6 +5,7 @@ profiles: name: Works data: host: https://httpbin.org + username: !template "xX{{chains.username}}Xx" user_guid: abc123 - id: init-fails name: Request Init Fails @@ -39,7 +40,7 @@ requests: sudo: yes_please body: | { - "username": "{{chains.username}}", + "username": "{{username}}", "password": "{{chains.password}}" } diff --git a/src/cli.rs b/src/cli.rs index 825929cd..2b1df837 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -188,6 +188,6 @@ where { let (key, value) = s .split_once('=') - .ok_or_else(|| format!("invalid key=value: no \"=\" found in {s:?}"))?; + .ok_or_else(|| format!("invalid key=value: no \"=\" found in `{s}`"))?; Ok((key.parse()?, value.parse()?)) } diff --git a/src/collection.rs b/src/collection.rs index bd0d6ab8..cabcf8af 100644 --- a/src/collection.rs +++ b/src/collection.rs @@ -7,10 +7,15 @@ use crate::template::Template; use anyhow::{anyhow, Context}; use derive_more::{Deref, Display, From}; use indexmap::IndexMap; -use serde::{Deserialize, Serialize}; +use serde::{ + de::{EnumAccess, VariantAccess}, + Deserialize, Deserializer, Serialize, +}; use serde_json_path::JsonPath; use std::{ + fmt, future::Future, + marker::PhantomData, path::{Path, PathBuf}, }; use tokio::fs; @@ -57,7 +62,7 @@ pub struct CollectionId(String); pub struct Profile { pub id: ProfileId, pub name: Option, - pub data: IndexMap, + pub data: IndexMap, } #[derive( @@ -75,6 +80,21 @@ pub struct Profile { )] pub struct ProfileId(String); +/// The value type of a profile's data mapping +#[derive(Clone, Debug, Serialize)] +#[cfg_attr(test, derive(PartialEq))] +#[serde(rename_all = "snake_case")] +pub enum ProfileValue { + /// A raw text string + Raw(String), + /// A nested template, which allows for recursion. By requiring the user to + /// declare this up front, we can parse the template during collection + /// deserialization. It also keeps a cap on the complexity of nested + /// templates, which is a balance between usability and simplicity (both + /// for the user and the code). + Template(Template), +} + /// A definition of how to make a request. This is *not* called `Request` in /// order to distinguish it from a single instance of an HTTP request. And it's /// not called `RequestTemplate` because the word "template" has a specific @@ -234,9 +254,146 @@ impl Profile { } } +impl From for ProfileValue { + fn from(value: String) -> Self { + Self::Raw(value) + } +} + +impl From<&str> for ProfileValue { + fn from(value: &str) -> Self { + Self::Raw(value.into()) + } +} + impl RequestRecipe { /// Get a presentable name for this recipe pub fn name(&self) -> &str { self.name.as_deref().unwrap_or(&self.id) } } + +/// Deserialize a string OR enum into a ProfileValue. This is based on the +/// generated derive code, with extra logic to default to !raw for a string. +impl<'de> Deserialize<'de> for ProfileValue { + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + const VARIANTS: &[&str] = &["raw", "template"]; + + enum Field { + Raw, + Template, + } + + struct FieldVisitor; + impl<'de> serde::de::Visitor<'de> for FieldVisitor { + type Value = Field; + + fn expecting(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "variant identifier") + } + + fn visit_u64(self, value: u64) -> Result + where + E: serde::de::Error, + { + match value { + 0u64 => Ok(Field::Raw), + 1u64 => Ok(Field::Template), + _ => Err(serde::de::Error::invalid_value( + serde::de::Unexpected::Unsigned(value), + &"variant index 0 <= i < 2", + )), + } + } + + fn visit_str(self, value: &str) -> Result + where + E: serde::de::Error, + { + match value { + "raw" => Ok(Field::Raw), + "template" => Ok(Field::Template), + _ => { + Err(serde::de::Error::unknown_variant(value, VARIANTS)) + } + } + } + + fn visit_bytes(self, value: &[u8]) -> Result + where + E: serde::de::Error, + { + match value { + b"raw" => Ok(Field::Raw), + b"template" => Ok(Field::Template), + _ => { + let value = String::from_utf8_lossy(value); + Err(serde::de::Error::unknown_variant(&value, VARIANTS)) + } + } + } + } + + impl<'de> serde::Deserialize<'de> for Field { + #[inline] + fn deserialize(deserializer: D) -> Result + where + D: serde::Deserializer<'de>, + { + serde::Deserializer::deserialize_identifier( + deserializer, + FieldVisitor, + ) + } + } + + struct Visitor<'de> { + lifetime: PhantomData<&'de ()>, + } + + impl<'de> serde::de::Visitor<'de> for Visitor<'de> { + type Value = ProfileValue; + + fn expecting(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "enum ProfileValue or string",) + } + + fn visit_enum(self, data: A) -> Result + where + A: EnumAccess<'de>, + { + match EnumAccess::variant(data)? { + (Field::Raw, variant) => Result::map( + VariantAccess::newtype_variant::(variant), + ProfileValue::Raw, + ), + (Field::Template, variant) => Result::map( + VariantAccess::newtype_variant::