From 7e51d9c6820622a7f9099e7ee5f7e247f6815043 Mon Sep 17 00:00:00 2001 From: Lucas Pickering Date: Wed, 17 Apr 2024 20:32:00 -0400 Subject: [PATCH] Deny unknown fields during config/collection deserialization Closes #154 --- CHANGELOG.md | 3 +++ docs/src/api/request_collection/index.md | 22 ++++++++++++---------- docs/src/user_guide/chaining_requests.md | 9 +++++---- docs/src/user_guide/filter_query.md | 9 +++++---- slumber.yml | 11 ++++++----- src/collection/insomnia.rs | 1 + src/collection/models.rs | 17 ++++++++++++++--- src/collection/recipe_tree.rs | 2 +- src/config.rs | 2 +- 9 files changed, 48 insertions(+), 28 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 24232d1b..de391179 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,9 @@ - Profile values are always treated as templates now - Any profile values that were previously the "raw" variant (the default) that contain template syntax (e.g. `{{user_id}}`) will now be rendered as templates. In reality this is very unlikely, so this probably isn't going to break your setup - If you have an existing profile value tagged with `!template` it **won't** break, but it will no longer do anything +- Unknown fields in config/collection files will now be rejected ([#154](https://github.com/LucasPickering/slumber/issues/154)) + - In most cases this field is a mistake, so this is meant to make debugging easier + - If you have an intentional unknown field, you can now nest it under `.ignore` to ignore it ### Added diff --git a/docs/src/api/request_collection/index.md b/docs/src/api/request_collection/index.md index 129dc5ad..6435c0f5 100644 --- a/docs/src/api/request_collection/index.md +++ b/docs/src/api/request_collection/index.md @@ -37,11 +37,12 @@ slumber collections list A request collection supports the following top-level fields: -| Field | Type | Description | Default | -| ---------- | ------------------------------------------------------- | ------------------------- | ------- | -| `profiles` | [`mapping[string, Profile]`](./profile.md) | Static template values | `{}` | -| `requests` | [`mapping[string, RequestRecipe]`](./request_recipe.md) | Requests Slumber can send | `{}` | -| `chains` | [`mapping[string, Chain]`](./chain.md) | Complex template values | `{}` | +| Field | Type | Description | Default | +| ---------- | ------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------- | ------- | +| `profiles` | [`mapping[string, Profile]`](./profile.md) | Static template values | `{}` | +| `requests` | [`mapping[string, RequestRecipe]`](./request_recipe.md) | Requests Slumber can send | `{}` | +| `chains` | [`mapping[string, Chain]`](./chain.md) | Complex template values | `{}` | +| `.ignore` | Any | Extra data to be ignored by Slumber (useful with [YAML anchors](https://support.atlassian.com/bitbucket-cloud/docs/yaml-anchors/)) | | ## Examples @@ -71,11 +72,12 @@ chains: recipe: login selector: $.token -# Use YAML anchors for de-duplication -base: &base - headers: - Accept: application/json - Content-Type: application/json +# Use YAML anchors for de-duplication (Anything under .ignore is ignored) +.ignore: + base: &base + headers: + Accept: application/json + Content-Type: application/json requests: login: !request diff --git a/docs/src/user_guide/chaining_requests.md b/docs/src/user_guide/chaining_requests.md index 21da592a..b8773dff 100644 --- a/docs/src/user_guide/chaining_requests.md +++ b/docs/src/user_guide/chaining_requests.md @@ -9,10 +9,11 @@ chains: recipe: login selector: $.token -base: &base - headers: - Accept: application/json - Content-Type: application/json +.ignore: + base: &base + headers: + Accept: application/json + Content-Type: application/json requests: login: !request diff --git a/docs/src/user_guide/filter_query.md b/docs/src/user_guide/filter_query.md index 4bea1503..13a7dd47 100644 --- a/docs/src/user_guide/filter_query.md +++ b/docs/src/user_guide/filter_query.md @@ -40,10 +40,11 @@ chains: selector: $.token # Use YAML anchors for de-duplication -base: &base - headers: - Accept: application/json - Content-Type: application/json +.ignore: + base: &base + headers: + Accept: application/json + Content-Type: application/json requests: login: !request diff --git a/slumber.yml b/slumber.yml index 9c7ff2a8..08cff895 100644 --- a/slumber.yml +++ b/slumber.yml @@ -29,11 +29,12 @@ chains: trigger: !expire 12h selector: $.headers["X-Amzn-Trace-Id"] -base: &base - authentication: !bearer "{{chains.auth_token}}" - headers: - Accept: application/json - Content-Type: application/json +.ignore: + base: &base + authentication: !bearer "{{chains.auth_token}}" + headers: + Accept: application/json + Content-Type: application/json requests: login: !request diff --git a/src/collection/insomnia.rs b/src/collection/insomnia.rs index c1fbdda8..30ea4266 100644 --- a/src/collection/insomnia.rs +++ b/src/collection/insomnia.rs @@ -63,6 +63,7 @@ impl Collection { profiles, recipes, chains: IndexMap::new(), + _ignore: serde::de::IgnoredAny, }) } } diff --git a/src/collection/models.rs b/src/collection/models.rs index 8c8d60d4..cde69dd8 100644 --- a/src/collection/models.rs +++ b/src/collection/models.rs @@ -18,6 +18,7 @@ use std::{path::PathBuf, time::Duration}; /// of configuration. #[derive(Clone, Debug, Default, Serialize, Deserialize)] #[cfg_attr(test, derive(PartialEq))] +#[serde(deny_unknown_fields)] pub struct Collection { #[serde(default, deserialize_with = "cereal::deserialize_id_map")] pub profiles: IndexMap, @@ -27,11 +28,18 @@ pub struct Collection { /// intuitive #[serde(default, rename = "requests")] pub recipes: RecipeTree, + /// A hack-ish to allow users to add arbitrary data to their collection + /// file without triggering a unknown field error. Ideally we could + /// ignore anything that starts with `.` (recursively) but that + /// requires a custom serde impl for each type, or changes to the macro + #[serde(skip_serializing, rename = ".ignore")] + pub(super) _ignore: serde::de::IgnoredAny, } /// Mutually exclusive hot-swappable config group #[derive(Clone, Debug, Serialize, Deserialize)] #[cfg_attr(test, derive(PartialEq))] +#[serde(deny_unknown_fields)] pub struct Profile { #[serde(skip)] // This will be auto-populated from the map key pub id: ProfileId, @@ -57,6 +65,7 @@ pub struct ProfileId(String); /// A gathering of like-minded recipes and/or folders #[derive(Clone, Debug, Serialize, Deserialize)] #[cfg_attr(test, derive(PartialEq))] +#[serde(deny_unknown_fields)] pub struct Folder { #[serde(skip)] // This will be auto-populated from the map key pub id: RecipeId, @@ -76,6 +85,7 @@ pub struct Folder { /// meaning related to string interpolation. #[derive(Clone, Debug, Serialize, Deserialize)] #[cfg_attr(test, derive(PartialEq))] +#[serde(deny_unknown_fields)] pub struct Recipe { #[serde(skip)] // This will be auto-populated from the map key pub id: RecipeId, @@ -112,7 +122,7 @@ pub struct RecipeId(String); /// request twice. #[derive(Clone, Debug, Serialize, Deserialize)] #[cfg_attr(test, derive(PartialEq))] -#[serde(rename_all = "snake_case")] +#[serde(rename_all = "snake_case", deny_unknown_fields)] pub enum Authentication { /// `Authorization: Basic {username:password | base64}` Basic { @@ -128,6 +138,7 @@ pub enum Authentication { /// can use it in a template via `{{chains.}}`. #[derive(Clone, Debug, Serialize, Deserialize)] #[cfg_attr(test, derive(PartialEq))] +#[serde(deny_unknown_fields)] pub struct Chain { #[serde(skip)] // This will be auto-populated from the map key pub id: ChainId, @@ -186,7 +197,7 @@ impl Equivalent for ChainId<&str> { /// The source of data for a chain #[derive(Clone, Debug, Serialize, Deserialize)] #[cfg_attr(test, derive(PartialEq))] -#[serde(rename_all = "snake_case")] +#[serde(rename_all = "snake_case", deny_unknown_fields)] pub enum ChainSource { /// Load data from the most recent response of a particular request recipe Request { @@ -207,7 +218,7 @@ pub enum ChainSource { /// dependency request. #[derive(Copy, Clone, Debug, Default, Serialize, Deserialize)] #[cfg_attr(test, derive(PartialEq))] -#[serde(rename_all = "snake_case")] +#[serde(rename_all = "snake_case", deny_unknown_fields)] pub enum ChainRequestTrigger { /// Never trigger the request. This is the default because upstream /// requests could be mutating, so we want the user to explicitly opt into diff --git a/src/collection/recipe_tree.rs b/src/collection/recipe_tree.rs index dc671a85..6ef626d3 100644 --- a/src/collection/recipe_tree.rs +++ b/src/collection/recipe_tree.rs @@ -32,7 +32,7 @@ pub struct RecipeLookupKey(Vec); /// A node in the recipe tree, either a folder or recipe #[derive(Clone, Debug, From, Serialize, Deserialize)] #[cfg_attr(test, derive(PartialEq))] -#[serde(rename_all = "snake_case")] +#[serde(rename_all = "snake_case", deny_unknown_fields)] #[allow(clippy::large_enum_variant)] pub enum RecipeNode { Folder(Folder), diff --git a/src/config.rs b/src/config.rs index 3d11a3ae..38a8438e 100644 --- a/src/config.rs +++ b/src/config.rs @@ -16,7 +16,7 @@ use tracing::info; /// are made to the config file while a session is running, they won't be /// picked up until the app restarts. #[derive(Debug, Deserialize)] -#[serde(default)] +#[serde(default, deny_unknown_fields)] pub struct Config { /// The path that the config was loaded from, or tried to be loaded from if /// the file didn't exist