monosecret 0.4.3

Declarative secrets, every environment, any provider
Documentation
//! Semantic compilation of a parsed specification.
//!
//! [`Config`](crate::config::Config) is the syntax tree: its `Option` fields
//! record what a particular source/profile wrote. Runtime resolution and
//! generated types instead consume this module's effective view, where profile
//! inheritance and missing-value behavior have already been decided once.

use std::collections::BTreeMap;
use std::collections::BTreeSet;

use crate::composition::Template;
use crate::config::Config;
use crate::config::Secret;

/// What resolution does when no provider returns a value.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum MissingPolicy {
	/// Absence fails resolution.
	Error,
	/// Absence is a valid optional result.
	Omit,
	/// The committed spec default supplies the value.
	UseDefault,
	/// The configured generator supplies the value.
	Generate,
	/// `monosecret run` securely asks the operator; the selected provider
	/// decides whether the answer is persisted.
	Prompt,
}

impl MissingPolicy {
	/// Whether a successful resolution necessarily contains this field.
	pub(crate) fn guaranteed_on_success(self) -> bool {
		self != Self::Omit
	}
}

/// One fully merged secret in an effective profile.
#[derive(Debug, Clone)]
pub(crate) struct CompiledSecret {
	pub(crate) config: Secret,
	pub(crate) missing: MissingPolicy,
	/// The effective `required` flag for reporting. Kept distinct from
	/// `missing`: a required generated/defaulted field is still guaranteed.
	pub(crate) declared_required: bool,
	/// The parsed `composed` template, decided once here so graph validation,
	/// planning, and the executor's render pass all read one parse. A
	/// malformed template compiles to `None`; `Secret::validate_semantics`
	/// rejects it before any of those consumers run.
	pub(crate) composition: Option<Template>,
}

impl CompiledSecret {
	fn new(config: Secret, conditionally_required: bool) -> Self {
		// An inline default makes an omitted `required` behave as false. This
		// preserves the spec shorthand while keeping an explicit inherited
		// `required = true` visible in reports. Membership in a profile
		// presence constraint likewise replaces the implicit per-secret
		// requirement, while an explicit `required = true` remains independent.
		let declared_required = config
			.required
			.unwrap_or(config.default.is_none() && !conditionally_required);
		let missing = if config.prompt == Some(true) {
			MissingPolicy::Prompt
		} else if config.would_generate() {
			MissingPolicy::Generate
		} else if config.default.is_some() {
			MissingPolicy::UseDefault
		} else if declared_required {
			MissingPolicy::Error
		} else {
			MissingPolicy::Omit
		};

		let composition = config
			.composed
			.as_deref()
			.and_then(|source| Template::parse(source).ok());
		Self {
			config,
			missing,
			declared_required,
			composition,
		}
	}
}

#[cfg(test)]
mod tests {
	use super::*;

	#[test]
	fn missing_policy_compiles_presence_once() {
		let required = CompiledSecret::new(Secret::default(), false);
		assert_eq!(required.missing, MissingPolicy::Error);
		assert!(required.declared_required);
		assert!(required.missing.guaranteed_on_success());

		let defaulted = CompiledSecret::new(
			Secret {
				default: Some("fallback".to_string()),
				..Default::default()
			},
			false,
		);
		assert_eq!(defaulted.missing, MissingPolicy::UseDefault);
		assert!(!defaulted.declared_required);
		assert!(defaulted.missing.guaranteed_on_success());

		let optional = CompiledSecret::new(
			Secret {
				required: Some(false),
				..Default::default()
			},
			false,
		);
		assert_eq!(optional.missing, MissingPolicy::Omit);
		assert!(!optional.missing.guaranteed_on_success());

		let alternative = CompiledSecret::new(Secret::default(), true);
		assert_eq!(alternative.missing, MissingPolicy::Omit);
		assert!(!alternative.declared_required);

		let prompted = CompiledSecret::new(
			Secret {
				prompt: Some(true),
				..Default::default()
			},
			false,
		);
		assert_eq!(prompted.missing, MissingPolicy::Prompt);
		assert!(prompted.declared_required);
		assert!(prompted.missing.guaranteed_on_success());
	}
}

/// One effective profile, including fields inherited from `default`.
#[derive(Debug, Clone)]
pub(crate) struct CompiledProfile {
	pub(crate) secrets: BTreeMap<String, CompiledSecret>,
	pub(crate) constraints: CompiledConstraints,
}

/// One named cross-secret presence group.
#[derive(Debug, Clone)]
pub(crate) struct CompiledConstraintGroup {
	pub(crate) name: String,
	pub(crate) members: Vec<String>,
}

/// Named presence groups compiled from each effective secret's membership.
#[derive(Debug, Clone, Default)]
pub(crate) struct CompiledConstraints {
	pub(crate) at_least_one: Vec<CompiledConstraintGroup>,
	pub(crate) exactly_one: Vec<CompiledConstraintGroup>,
}

/// A parsed spec reduced to the semantics shared by runtime and codegen.
#[derive(Debug, Clone)]
pub(crate) struct CompiledSpec {
	pub(crate) project: String,
	pub(crate) profiles: BTreeMap<String, CompiledProfile>,
}

impl CompiledSpec {
	pub(crate) fn compile(config: &Config) -> Self {
		let default_profile = config.profiles.get("default");
		let mut profiles = BTreeMap::new();

		for (profile_name, profile) in &config.profiles {
			let inherited = (profile_name != "default" && profile.inherits_default())
				.then_some(default_profile)
				.flatten();
			// A `BTreeSet` unions the profile's own names with the inherited
			// ones already deduplicated and sorted, which is the deterministic
			// order every surface consuming the spec expects.
			let mut names: BTreeSet<&String> = profile.secrets.keys().collect();

			if let Some(default) = inherited {
				names.extend(default.secrets.keys());
			}

			let effective: BTreeMap<String, Secret> = names
				.into_iter()
				.map(|name| {
					let current = profile.secrets.get(name);
					let default = inherited.and_then(|p| p.secrets.get(name));
					let effective = Secret::resolved(
						current,
						default,
						profile.defaults.as_ref(),
						config.defaults.as_ref(),
					)
					.expect("an effective name comes from current or default");
					(name.clone(), effective)
				})
				.collect();

			let mut at_least_one: BTreeMap<String, Vec<String>> = BTreeMap::new();
			let mut exactly_one: BTreeMap<String, Vec<String>> = BTreeMap::new();

			for (name, secret) in &effective {
				if let Some(groups) = &secret.at_least_one {
					for group in groups {
						at_least_one
							.entry(group.clone())
							.or_default()
							.push(name.clone());
					}
				}

				if let Some(groups) = &secret.exactly_one {
					for group in groups {
						exactly_one
							.entry(group.clone())
							.or_default()
							.push(name.clone());
					}
				}
			}

			fn groups(grouped: BTreeMap<String, Vec<String>>) -> Vec<CompiledConstraintGroup> {
				grouped
					.into_iter()
					.map(|(name, members)| CompiledConstraintGroup { name, members })
					.collect()
			}
			let constraints = CompiledConstraints {
				at_least_one: groups(at_least_one),
				exactly_one: groups(exactly_one),
			};

			let secrets = effective
				.into_iter()
				.map(|(name, secret)| {
					let conditionally_required = secret
						.at_least_one
						.as_ref()
						.is_some_and(|groups| !groups.is_empty())
						|| secret
							.exactly_one
							.as_ref()
							.is_some_and(|groups| !groups.is_empty());
					(name, CompiledSecret::new(secret, conditionally_required))
				})
				.collect();

			profiles.insert(
				profile_name.clone(),
				CompiledProfile {
					secrets,
					constraints,
				},
			);
		}

		Self {
			project: config.project.name.clone(),
			profiles,
		}
	}

	pub(crate) fn profile(&self, name: &str) -> Option<&CompiledProfile> {
		self.profiles.get(name)
	}
}