diff --git a/CHANGELOG.md b/CHANGELOG.md index 4215d805..e52a0bfb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,6 +17,9 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), - When using the CLI, the `--profile` argument can be omitted to use the default profile - Reset edited recipe values to their default using `r` - You can [customize the key](https://slumber.lucaspickering.me/book/api/configuration/input_bindings.html) to whatever you want +- Add `selector_mode` field to chains, to control how single vs multiple results from a JSONPath selector are handled + - Previously, if a selector returned multiple results, an error was returned. Now, the result list will be rendered as a JSON array. To return to the previous behavior, set `selector_mode: single` in your chain. + - [See docs for more](https://slumber.lucaspickering.me/book/api/request_collection/chain.html#selector-mode) ### Changed diff --git a/crates/core/src/collection.rs b/crates/core/src/collection.rs index 49537979..9c4f089e 100644 --- a/crates/core/src/collection.rs +++ b/crates/core/src/collection.rs @@ -280,6 +280,7 @@ mod tests { source: ChainSource::command(["whoami"]), sensitive: false, selector: None, + selector_mode: SelectorMode::default(), content_type: None, trim: ChainOutputTrim::None, }, @@ -291,6 +292,7 @@ mod tests { }, sensitive: false, selector: None, + selector_mode: SelectorMode::default(), content_type: None, trim: ChainOutputTrim::None, }, @@ -299,6 +301,7 @@ mod tests { source: ChainSource::command(["whoami"]), sensitive: false, selector: None, + selector_mode: SelectorMode::default(), content_type: None, trim: ChainOutputTrim::None, }, @@ -307,6 +310,7 @@ mod tests { source: ChainSource::command(["whoami"]), sensitive: false, selector: None, + selector_mode: SelectorMode::default(), content_type: None, trim: ChainOutputTrim::Start, }, @@ -315,6 +319,7 @@ mod tests { source: ChainSource::command(["whoami"]), sensitive: false, selector: None, + selector_mode: SelectorMode::default(), content_type: None, trim: ChainOutputTrim::End, }, @@ -323,6 +328,7 @@ mod tests { source: ChainSource::command(["whoami"]), sensitive: false, selector: None, + selector_mode: SelectorMode::default(), content_type: None, trim: ChainOutputTrim::Both, }, @@ -334,6 +340,7 @@ mod tests { }, sensitive: true, selector: None, + selector_mode: SelectorMode::default(), content_type: None, trim: ChainOutputTrim::None, }, @@ -345,6 +352,7 @@ mod tests { }, sensitive: false, selector: None, + selector_mode: SelectorMode::default(), content_type: None, trim: ChainOutputTrim::None, }, @@ -355,6 +363,7 @@ mod tests { }, sensitive: false, selector: None, + selector_mode: SelectorMode::default(), content_type: None, trim: ChainOutputTrim::None, }, @@ -365,6 +374,7 @@ mod tests { }, sensitive: false, selector: None, + selector_mode: SelectorMode::default(), content_type: Some(ContentType::Json), trim: ChainOutputTrim::None, }, @@ -377,6 +387,7 @@ mod tests { }, sensitive: false, selector: Some("$.data".parse().unwrap()), + selector_mode: SelectorMode::default(), content_type: None, trim: ChainOutputTrim::None, }, @@ -389,6 +400,7 @@ mod tests { }, sensitive: false, selector: None, + selector_mode: SelectorMode::default(), content_type: None, trim: ChainOutputTrim::None, }, @@ -401,6 +413,7 @@ mod tests { }, sensitive: false, selector: None, + selector_mode: SelectorMode::default(), content_type: None, trim: ChainOutputTrim::None, }, @@ -415,6 +428,7 @@ mod tests { }, sensitive: false, selector: None, + selector_mode: SelectorMode::default(), content_type: None, trim: ChainOutputTrim::None, }, @@ -427,6 +441,7 @@ mod tests { }, sensitive: false, selector: None, + selector_mode: SelectorMode::default(), content_type: None, trim: ChainOutputTrim::None, }, @@ -439,6 +454,7 @@ mod tests { }, sensitive: false, selector: None, + selector_mode: SelectorMode::default(), content_type: None, trim: ChainOutputTrim::None, }, @@ -453,6 +469,7 @@ mod tests { }, sensitive: false, selector: None, + selector_mode: SelectorMode::default(), content_type: None, trim: ChainOutputTrim::None, }, diff --git a/crates/core/src/collection/cereal.rs b/crates/core/src/collection/cereal.rs index 95f6dcbd..469c2c4c 100644 --- a/crates/core/src/collection/cereal.rs +++ b/crates/core/src/collection/cereal.rs @@ -3,9 +3,9 @@ use crate::{ collection::{ recipe_tree::RecipeNode, Chain, ChainId, Profile, ProfileId, Recipe, - RecipeBody, RecipeId, SelectOptions, + RecipeBody, RecipeId, }, - http::{content_type::ContentType, query::Query}, + http::content_type::ContentType, template::Template, }; use anyhow::Context; @@ -16,14 +16,10 @@ use serde::{ self, EnumAccess, Error as _, MapAccess, SeqAccess, VariantAccess, Visitor, }, - ser::{Error as _, SerializeStructVariant}, + ser::Error as _, Deserialize, Deserializer, Serialize, Serializer, }; -use std::{ - fmt::{self, Display}, - hash::Hash, - str::FromStr, -}; +use std::{fmt::Display, hash::Hash, str::FromStr}; /// A type that has an `id` field. This is ripe for a derive macro, maybe a fun /// project some day? @@ -388,135 +384,6 @@ impl<'de> Deserialize<'de> for RecipeBody { } } -impl SelectOptions { - // Constants for serialize/deserialization. Typically these are generated - // by macros, but we need custom implementation - const STRUCT_NAME: &'static str = "SelectOptions"; - const VARIANT_DYNAMIC: &'static str = "dynamic"; - const ALL_VARIANTS: &'static [&'static str] = &[Self::VARIANT_DYNAMIC]; - const DYNAMIC_FIELD_SOURCE: &'static str = "source"; - const DYNAMIC_FIELD_SELECTOR: &'static str = "selector"; - const ALL_DYNAMIC_FIELDS: &'static [&'static str] = - &[Self::DYNAMIC_FIELD_SOURCE, Self::DYNAMIC_FIELD_SELECTOR]; -} - -/// Serialize [SelectOptions] to a list of templates or tagged struct -impl Serialize for SelectOptions { - fn serialize(&self, serializer: S) -> Result - where - S: Serializer, - { - match self { - // Fixed options are serialized as a plain list, for convenience - SelectOptions::Fixed(options) => options.serialize(serializer), - // Dynamic options become a tagged struct - SelectOptions::Dynamic { source, selector } => { - let mut state = serializer.serialize_struct_variant( - Self::STRUCT_NAME, - 1, - Self::VARIANT_DYNAMIC, - Self::ALL_DYNAMIC_FIELDS.len(), - )?; - state.serialize_field(Self::DYNAMIC_FIELD_SOURCE, &source)?; - state - .serialize_field(Self::DYNAMIC_FIELD_SELECTOR, &selector)?; - state.end() - } - } - } -} - -/// Deserialize a list of templates or tagged struct to [SelectOptions] -impl<'de> Deserialize<'de> for SelectOptions { - fn deserialize(deserializer: D) -> Result - where - D: Deserializer<'de>, - { - struct SelectOptionsVisitor; - - impl<'de> Visitor<'de> for SelectOptionsVisitor { - type Value = SelectOptions; - - fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { - formatter.write_str("array of templates or !dynamic tag") - } - - fn visit_seq(self, mut seq: A) -> Result - where - A: SeqAccess<'de>, - { - // Parse sequence as fixed options - let mut options = - Vec::with_capacity(seq.size_hint().unwrap_or(5)); - while let Some(value) = seq.next_element::