8000 chore(sessions): Add Primites for Paths and Patches by evanspearman · Pull Request #248 · gominimal/minimal · GitHub
[go: up one dir, main page]

Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
272 changes: 181 additions & 91 deletions Cargo.lock

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ anyhow = "1.0"
blake3 = { version = "1", features = ["serde"] }
bytes = "1.11" # keep in sync with reqwest
bzip2 = "0.6"
camino = { version = "1", features = ["serde1"] }
clap = { version = "4.6", features = ["derive", "env", "string"] }
clap_complete = { version = "4.6" }
codespan-reporting = { version = "0.13", features = ["serialization"] } # Keep in sync with nickel-lang-core
Expand Down
2 changes: 2 additions & 0 deletions crates/sessions/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ edition.workspace = true
publish.workspace = true

[dependencies]
globset.workspace = true
camino.workspace = true
serde.workspace = true

[dev-dependencies]
Expand Down
3 changes: 3 additions & 0 deletions crates/sessions/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,6 @@

pub mod lifecyclehook;
pub mod loadout;
pub mod patches;
pub mod paths;
pub mod vars;
39 changes: 30 additions & 9 deletions crates/sessions/src/lifecyclehook.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,8 @@
//! [`LifecycleHook::builder`] or deserialize them from configuration; both paths
//! enforce the same invariant.

use crate::paths::ConfigRelPath;
use core::fmt;
use std::path::PathBuf;

/// Errors produced when constructing a [`LifecycleHook`].
#[non_exhaustive]
Expand Down Expand Up @@ -39,6 +39,11 @@ impl std::error::Error for Error {}
/// Either the script body provided directly ([`Inline`](Self::Inline)) or a
/// path to a script file on disk ([`External`](Self::External)).
///
/// External scripts are [`ConfigRelPath`]s — anchored to the directory of the
/// `minimal.toml` they were decoded from. Absolute paths are rejected at
/// deserialization. To run an arbitrary host path, the executor binds the
/// `ConfigRelPath` against the known config directory.
///
/// Serialized with adjacent tagging — the wire form is a table with `type` and
/// `value` keys:
///
Expand All @@ -51,8 +56,8 @@ impl std::error::Error for Error {}
pub enum HookScript {
/// Script body stored inline.
Inline(String),
/// Path to a script file on disk.
External(PathBuf),
/// Path to a script file on disk, relative to the config directory.
External(ConfigRelPath),
}

/// A set of scripts to run at lifecycle transition points of a host resource.
Expand Down Expand Up @@ -257,9 +262,10 @@ mod tests {
"#;
let hook: LifecycleHook = toml::from_str(src).unwrap();
assert!(matches!(hook.on_activate(), Some(HookScript::Inline(s)) if s == "echo activated"));
assert!(
matches!(hook.on_destroy(), Some(HookScript::External(p)) if p == &PathBuf::from("./teardown.sh"))
);
assert!(matches!(
hook.on_destroy(),
Some(HookScript::External(p)) if p.as_str() == "./teardown.sh"
));
assert!(matches!(hook.on_failure(), Some(HookScript::Inline(s)) if s == "echo failed"));
}

Expand Down Expand Up @@ -287,18 +293,33 @@ mod tests {
fn serde_round_trip_through_toml() {
let original = LifecycleHook::builder()
.with_on_activate(HookScript::Inline("echo hi".into()))
.with_on_failure(HookScript::External(PathBuf::from("./fail.sh")))
.with_on_failure(HookScript::External(
ConfigRelPath::try_new("./fail.sh").unwrap(),
))
.build()
.unwrap();

let serialized = toml::to_string(&original).unwrap();
let parsed: LifecycleHook = toml::from_str(&serialized).unwrap();

assert!(matches!(parsed.on_activate(), Some(HookScript::Inline(s)) if s == "echo hi"));
assert!(matches!(
parsed.on_failure(),
Some(HookScript::External(p)) if p.as_str() == "./fail.sh"
));
assert!(parsed.on_destroy().is_none());
}

#[test]
fn external_rejects_absolute_path_at_deserialize() {
let src = r#"
on_activate = { type = "external", value = "/etc/hook.sh" }
"#;
let err = toml::from_str::<LifecycleHook>(src).unwrap_err();
assert!(
matches!(parsed.on_failure(), Some(HookScript::External(p)) if p == &PathBuf::from("./fail.sh"))
err.to_string().contains("relative"),
"expected realm error, got: {err}",
);
assert!(parsed.on_destroy().is_none());
}

#[test]
Expand Down
7 changes: 5 additions & 2 deletions crates/sessions/src/loadout.rs
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
use std::collections::HashMap;
use std::collections::BTreeMap;

use crate::lifecyclehook::LifecycleHook;
use crate::vars::Var;

#[derive(Clone, Debug, serde::Serialize, serde::Deserialize)]
pub struct Loadout {
env_vars: HashMap<String, String>,
#[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
vars: BTreeMap<String, Var>,
Comment on lines +8 to +9
Copy link
Copy Markdown
8BD0 Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🌐 Web query:

In Serde's default struct deserialization, are unknown fields ignored unless deny_unknown_fields is set?

💡 Result:

Yes. In Serde’s default struct deserialization behavior (for self-describing formats like JSON), unknown fields are ignored unless you opt into stricter checking with #[serde(deny_unknown_fields)]. Serdes’s container attribute docs state that #[serde(deny_unknown_fields)] “Always error during deserialization when encountering unknown fields” and that “When this attribute is not present, by default unknown fields are ignored for self-describing formats like JSON.” [1][2]

Citations:


🏁 Script executed:

#!/bin/bash
set -euo pipefail
rg -n "struct Loadout|env_vars|vars:|skip_serializing_if|serde.*default|deny_unknown_fields" crates/sessions/src/loadout.rs
nl -ba crates/sessions/src/loadout.rs | sed -n '1,80p'

Repository: gominimal/minimal

Length of output: 408


🏁 Script executed:

#!/bin/bash
set -euo pipefail
sed -n '1,120p' crates/sessions/src/loadout.rs
sed -n '120,240p' crates/sessions/src/loadout.rs

Repository: gominimal/minimal

Length of output: 513


🏁 Script executed:

#!/bin/bash
set -euo pipefail
rg -n "deny_unknown_fields|rename\s*=|alias\s*=|env_vars|vars:" crates/sessions/src/loadout.rs

Repository: gominimal/minimal

Length of output: 120


🏁 Script executed:

#!/bin/bash
set -euo pipefail
rg -n "env_vars" -S .
rg -n "Loadout" crates/sessions/src -S
rg -n "serde\(([^)]*rename|[^)]*alias|[^)]*deny_unknown_fields)" -S crates/sessions/src

Repository: gominimal/minimal

Length of output: 6582


Prevent legacy env_vars from being ignored during Serde deserialization

  • Loadout’s vars field (#[serde(default, skip_serializing_if = "BTreeMap::is_empty")]) has no alias = "env_vars"/rename and there’s no deny_unknown_fields, so legacy configs providing env_vars will be treated as unknown and effectively dropped (deserializing vars as the default empty map).
  • Remove the commented-out #[serde(...)] attribute in crates/sessions/src/loadout.rs (near the lifecycle_hooks field) to avoid dead/commented-out code.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/sessions/src/loadout.rs` around lines 8 - 9, The Loadout struct's vars
field currently uses #[serde(default, skip_serializing_if =
"BTreeMap::is_empty")] but does not accept legacy "env_vars", so legacy configs
are dropped; update the vars field on the Loadout struct to include a serde
alias/rename (e.g., add alias = "env_vars" or rename = "env_vars" in the
attribute) so deserialization accepts both names, and also remove the
commented-out serde attribute near the lifecycle_hooks field to eliminate
dead/commented-out code; reference the vars field and lifecycle_hooks in
crates/sessions/src/loadout.rs when making these edits.

#[serde(default, skip_serializing_if = "Vec::is_empty")]
lifecycle_hooks: Vec<LifecycleHook>,
// #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
}
Loading
Loading
0