Sweep every first-party crate source (1956 .rs files) to the project comment guidelines: delete redundant restatements, decorative banners, change narration, and end-of-line comments; keep and tighten the crucial ones (invariants, bug rationale, SAFETY blocks, ported-source attribution). No functional code changed. Every edit is proven comment-only against the prior tree by a comment-stripping lexer (string/char/raw-string aware) plus a separate doctest-fence check. Where removing a comment made rustfmt or clippy want to re-lay-out adjacent code, the minimal triggering comment is restored so code tokens stay byte-identical. Gates green: cargo fmt --all --check (0 diffs), cargo check and cargo clippy --workspace --all-targets (0 warnings). Adds scripts/check_codegen_comment_guidelines.py — the enforcement gate for these guidelines (flags banners, end-of-line comments, change narration, and commented-out code).
290 lines
9.6 KiB
Rust
290 lines
9.6 KiB
Rust
//! `[[campaigns]]` overlays. Priority (first id wins): requirements > remote >
|
|
//! user > managed > system_managed. Applied after layer merge.
|
|
|
|
use serde::{Deserialize, Serialize};
|
|
|
|
use crate::config_override::{
|
|
ConfigOverrideEntry, PATCH_STRIP_KEYS, PatchPath, apply_patches, patch_touches_any,
|
|
take_patch_array,
|
|
};
|
|
|
|
pub const CAMPAIGNS_KEY: &str = "campaigns";
|
|
|
|
#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq)]
|
|
pub struct CampaignMeta {
|
|
#[serde(default, alias = "campaign_id")]
|
|
pub id: Option<String>,
|
|
}
|
|
|
|
#[derive(Debug, Clone)]
|
|
pub struct CampaignEntry {
|
|
pub id: String,
|
|
pub patch: toml::Table,
|
|
}
|
|
|
|
/// Disk campaigns grouped by source layer. Merged with the remote layer (by
|
|
/// priority, first id wins) in [`crate::loader::ConfigLayers::resolve_campaigns`].
|
|
#[derive(Debug, Clone, Default)]
|
|
pub struct CampaignOverrides {
|
|
pub requirements: Vec<CampaignEntry>,
|
|
pub user: Vec<CampaignEntry>,
|
|
pub managed: Vec<CampaignEntry>,
|
|
pub system_managed: Vec<CampaignEntry>,
|
|
}
|
|
|
|
pub fn take_campaigns(config: &mut toml::Value) -> Vec<ConfigOverrideEntry<CampaignMeta>> {
|
|
match take_patch_array::<CampaignMeta>(config, CAMPAIGNS_KEY) {
|
|
Ok(entries) => entries,
|
|
Err(e) => {
|
|
tracing::warn!(error = %e, "campaigns: failed to deserialize; ignoring entries");
|
|
Vec::new()
|
|
}
|
|
}
|
|
}
|
|
|
|
pub fn build_campaign_entries(
|
|
taken: Vec<ConfigOverrideEntry<CampaignMeta>>,
|
|
layer: &'static str,
|
|
) -> Vec<CampaignEntry> {
|
|
let mut out = Vec::with_capacity(taken.len());
|
|
for entry in taken {
|
|
let Some(id) = entry
|
|
.meta
|
|
.id
|
|
.as_deref()
|
|
.map(str::trim)
|
|
.filter(|s| !s.is_empty())
|
|
.map(str::to_owned)
|
|
else {
|
|
tracing::warn!(layer, "campaigns: entry missing id; skipped");
|
|
continue;
|
|
};
|
|
if entry.patch.is_empty() {
|
|
continue;
|
|
}
|
|
out.push(CampaignEntry {
|
|
id,
|
|
patch: entry.patch,
|
|
});
|
|
}
|
|
out
|
|
}
|
|
|
|
/// `sources` in priority order; first `id` wins.
|
|
pub fn merge_campaign_entries(sources: &[&[CampaignEntry]]) -> Vec<CampaignEntry> {
|
|
let mut seen = std::collections::HashSet::<String>::new();
|
|
let mut out = Vec::new();
|
|
for source in sources {
|
|
for entry in *source {
|
|
if !seen.insert(entry.id.clone()) {
|
|
continue;
|
|
}
|
|
out.push(entry.clone());
|
|
}
|
|
}
|
|
out
|
|
}
|
|
|
|
/// Drop dismissed ids from a priority-merged list, preserving order.
|
|
pub fn filter_active_campaigns(
|
|
merged: Vec<CampaignEntry>,
|
|
dismissed_ids: &std::collections::HashSet<String>,
|
|
) -> Vec<CampaignEntry> {
|
|
merged
|
|
.into_iter()
|
|
.filter(|e| !dismissed_ids.contains(&e.id))
|
|
.collect()
|
|
}
|
|
|
|
/// Ids of `active` campaigns whose patch touches any of `paths` — used to dismiss
|
|
/// campaigns when the user persists a value at one of those paths.
|
|
pub fn ids_touching_paths(active: &[CampaignEntry], paths: &[PatchPath]) -> Vec<String> {
|
|
active
|
|
.iter()
|
|
.filter(|e| patch_touches_any(&e.patch, paths))
|
|
.map(|e| e.id.clone())
|
|
.collect()
|
|
}
|
|
|
|
/// `active` is highest-priority-first; patches apply lowest-first (`.rev()`) so the
|
|
/// highest-priority source wins a leaf conflict.
|
|
pub fn apply_active_campaign_patches(effective: &mut toml::Value, active: &[CampaignEntry]) {
|
|
apply_patches(
|
|
effective,
|
|
active.iter().rev().map(|e| e.patch.clone()),
|
|
PATCH_STRIP_KEYS,
|
|
);
|
|
}
|
|
|
|
pub fn take_campaign_entries(config: &mut toml::Value, layer: &'static str) -> Vec<CampaignEntry> {
|
|
build_campaign_entries(take_campaigns(config), layer)
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
fn parse(s: &str) -> toml::Value {
|
|
toml::from_str(s).unwrap()
|
|
}
|
|
|
|
fn models_default_patch(default: &str) -> toml::Table {
|
|
let mut models = toml::map::Map::new();
|
|
models.insert("default".into(), toml::Value::String(default.into()));
|
|
let mut t = toml::map::Map::new();
|
|
t.insert("models".into(), toml::Value::Table(models));
|
|
t
|
|
}
|
|
|
|
#[test]
|
|
fn campaign_overlays_any_field_over_user_config() {
|
|
let mut layer = parse(
|
|
r#"
|
|
[[campaigns]]
|
|
id = "c1"
|
|
[campaigns.models]
|
|
default = "new-model"
|
|
[campaigns.features]
|
|
web_fetch = true
|
|
"#,
|
|
);
|
|
let entries = take_campaign_entries(&mut layer, "managed");
|
|
assert!(layer.get(CAMPAIGNS_KEY).is_none());
|
|
assert_eq!(entries.len(), 1);
|
|
|
|
let mut effective =
|
|
parse("[models]\ndefault = \"old-model\"\n[features]\nweb_fetch = false\n");
|
|
apply_active_campaign_patches(&mut effective, &entries);
|
|
assert_eq!(effective["models"]["default"].as_str(), Some("new-model"));
|
|
assert_eq!(effective["features"]["web_fetch"].as_bool(), Some(true));
|
|
}
|
|
|
|
#[test]
|
|
fn merge_first_source_wins_duplicate_id() {
|
|
let req = [CampaignEntry {
|
|
id: "same".into(),
|
|
patch: models_default_patch("from-req"),
|
|
}];
|
|
let remote = [CampaignEntry {
|
|
id: "same".into(),
|
|
patch: models_default_patch("from-remote"),
|
|
}];
|
|
let merged = merge_campaign_entries(&[&req, &remote]);
|
|
assert_eq!(merged.len(), 1);
|
|
assert_eq!(
|
|
merged[0].patch["models"]["default"].as_str(),
|
|
Some("from-req")
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn apply_highest_priority_wins_on_leaf_conflict() {
|
|
// Two *distinct* ids both set models.default, so dedup by id does not
|
|
// apply and the leaf conflict is settled by apply order alone.
|
|
let req = [CampaignEntry {
|
|
id: "req".into(),
|
|
patch: models_default_patch("from-req"),
|
|
}];
|
|
let managed = [CampaignEntry {
|
|
id: "managed".into(),
|
|
patch: models_default_patch("from-managed"),
|
|
}];
|
|
let merged = merge_campaign_entries(&[&req, &managed]);
|
|
assert_eq!(merged.len(), 2);
|
|
|
|
let mut effective = parse("[models]\ndefault = \"user-old\"\n");
|
|
apply_active_campaign_patches(&mut effective, &merged);
|
|
assert_eq!(effective["models"]["default"].as_str(), Some("from-req"));
|
|
}
|
|
|
|
#[test]
|
|
fn build_campaign_entries_skips_missing_id() {
|
|
let taken = vec![
|
|
ConfigOverrideEntry {
|
|
meta: CampaignMeta { id: None },
|
|
patch: models_default_patch("dropped-none"),
|
|
},
|
|
ConfigOverrideEntry {
|
|
meta: CampaignMeta {
|
|
id: Some(" ".into()),
|
|
},
|
|
patch: models_default_patch("dropped-blank"),
|
|
},
|
|
ConfigOverrideEntry {
|
|
meta: CampaignMeta {
|
|
id: Some("valid".into()),
|
|
},
|
|
patch: models_default_patch("kept"),
|
|
},
|
|
];
|
|
let out = build_campaign_entries(taken, "managed");
|
|
assert_eq!(out.len(), 1);
|
|
assert_eq!(out[0].id, "valid");
|
|
assert_eq!(out[0].patch["models"]["default"].as_str(), Some("kept"));
|
|
}
|
|
|
|
#[test]
|
|
fn campaign_id_alias_is_accepted_in_toml_and_does_not_leak_into_patch() {
|
|
for src in [
|
|
"[[campaigns]]\ncampaign_id = \"c1\"\n[campaigns.models]\ndefault = \"m\"\n",
|
|
"[[campaigns]]\nid = \"c1\"\n[campaigns.models]\ndefault = \"m\"\n",
|
|
] {
|
|
let mut layer = parse(src);
|
|
let entries = take_campaign_entries(&mut layer, "user");
|
|
assert_eq!(entries.len(), 1);
|
|
assert_eq!(entries[0].id, "c1");
|
|
// A leaked id key would deep-merge a junk top-level `id` into every
|
|
// effective config.
|
|
assert!(
|
|
entries[0].patch.get("id").is_none()
|
|
&& entries[0].patch.get("campaign_id").is_none(),
|
|
"id keys must not leak into the patch: {src}"
|
|
);
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn requirements_win_over_campaign() {
|
|
use crate::loader::ConfigLayers;
|
|
// A campaign (even from a lower layer) can't override a field the admin
|
|
// set in requirements: `apply_campaign_overrides` re-merges requirements on top.
|
|
let mut layers = ConfigLayers {
|
|
user: parse("[models]\ndefault = \"user-old\"\n"),
|
|
user_requirements: Some(parse("[models]\ndefault = \"pinned\"\n")),
|
|
..Default::default()
|
|
};
|
|
layers.campaigns.user = vec![CampaignEntry {
|
|
id: "c1".into(),
|
|
patch: models_default_patch("campaign"),
|
|
}];
|
|
let effective =
|
|
layers.effective_config_with_campaigns(&[], &std::collections::HashSet::new());
|
|
assert_eq!(
|
|
effective["models"]["default"].as_str(),
|
|
Some("pinned"),
|
|
"requirements must beat a campaign for the same field"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn effective_config_honors_dismiss() {
|
|
use crate::loader::ConfigLayers;
|
|
let mut layers = ConfigLayers {
|
|
user: parse("[models]\ndefault = \"user-old\"\n"),
|
|
..Default::default()
|
|
};
|
|
layers.campaigns.managed = vec![CampaignEntry {
|
|
id: "c1".into(),
|
|
patch: models_default_patch("new"),
|
|
}];
|
|
|
|
let none = std::collections::HashSet::new();
|
|
let active = layers.effective_config_with_campaigns(&[], &none);
|
|
assert_eq!(active["models"]["default"].as_str(), Some("new"));
|
|
|
|
let dismissed: std::collections::HashSet<_> = ["c1".into()].into_iter().collect();
|
|
let off = layers.effective_config_with_campaigns(&[], &dismissed);
|
|
assert_eq!(off["models"]["default"].as_str(), Some("user-old"));
|
|
}
|
|
}
|