use std::str::FromStr;
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum SyncProfile {
EnterpriseStrict,
Permissive,
Development,
}
impl std::str::FromStr for SyncProfile {
type Err = String;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s {
"enterprise-strict" => Ok(Self::EnterpriseStrict),
"permissive" => Ok(Self::Permissive),
"development" | "dev" => Ok(Self::Development),
other => Err(format!(
"Unknown profile '{}'. Known: enterprise-strict, permissive, development",
other
)),
}
}
}
impl SyncProfile {
pub fn as_str(&self) -> &str {
match self {
Self::EnterpriseStrict => "enterprise-strict",
Self::Permissive => "permissive",
Self::Development => "development",
}
}
pub fn requires_lockfile(&self) -> bool {
matches!(self, Self::EnterpriseStrict)
}
pub fn allows_unsigned_packs(&self) -> bool {
!matches!(self, Self::EnterpriseStrict)
}
}
pub fn validate_sync_preconditions(
profile: Option<&str>, locked: bool, workspace_root: &std::path::Path,
) -> Result<(), String> {
let profile = match profile {
Some(p) => SyncProfile::from_str(p)?,
None => {
if locked {
let lockfile = workspace_root.join(".ggen").join("packs.lock");
if !lockfile.exists() {
return Err(
"Lockfile required (--locked) but .ggen/packs.lock not found. \
Run `ggen packs add <pack>` first."
.to_string(),
);
}
}
return Ok(());
}
};
if locked || profile.requires_lockfile() {
let lockfile = workspace_root.join(".ggen").join("packs.lock");
if !lockfile.exists() {
return Err(format!(
"Lockfile required (profile='{}', --locked={}) but .ggen/packs.lock not found. \
Run `ggen packs add <pack>` first.",
profile.as_str(),
locked
));
}
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
use std::fs;
use tempfile::TempDir;
#[test]
fn parse_known_profiles() {
assert_eq!(
SyncProfile::from_str("enterprise-strict").unwrap(),
SyncProfile::EnterpriseStrict
);
assert_eq!(
SyncProfile::from_str("permissive").unwrap(),
SyncProfile::Permissive
);
assert_eq!(
SyncProfile::from_str("development").unwrap(),
SyncProfile::Development
);
assert_eq!(
SyncProfile::from_str("dev").unwrap(),
SyncProfile::Development
);
}
#[test]
fn parse_unknown_profile_returns_error() {
let err = SyncProfile::from_str("bogus").unwrap_err();
assert!(err.contains("Unknown profile 'bogus'"), "error was: {err}");
assert!(err.contains("enterprise-strict"), "error was: {err}");
}
#[test]
fn enterprise_strict_requires_lockfile() {
assert!(SyncProfile::EnterpriseStrict.requires_lockfile());
assert!(!SyncProfile::Permissive.requires_lockfile());
assert!(!SyncProfile::Development.requires_lockfile());
}
#[test]
fn enterprise_strict_forbids_unsigned_packs() {
assert!(!SyncProfile::EnterpriseStrict.allows_unsigned_packs());
assert!(SyncProfile::Permissive.allows_unsigned_packs());
}
#[test]
fn as_str_round_trips() {
for (input, expected) in [
("enterprise-strict", "enterprise-strict"),
("permissive", "permissive"),
("development", "development"),
("dev", "development"),
] {
let p = SyncProfile::from_str(input).unwrap();
assert_eq!(p.as_str(), expected);
}
}
#[test]
fn no_profile_no_locked_always_passes() {
let dir = TempDir::new().unwrap();
assert!(validate_sync_preconditions(None, false, dir.path()).is_ok());
}
#[test]
fn locked_flag_without_lockfile_fails() {
let dir = TempDir::new().unwrap();
let err = validate_sync_preconditions(None, true, dir.path()).unwrap_err();
assert!(err.contains("--locked"), "error was: {err}");
assert!(err.contains("packs.lock"), "error was: {err}");
}
#[test]
fn locked_flag_with_lockfile_passes() {
let dir = TempDir::new().unwrap();
let ggen_dir = dir.path().join(".ggen");
fs::create_dir_all(&ggen_dir).unwrap();
fs::write(ggen_dir.join("packs.lock"), "{}").unwrap();
assert!(validate_sync_preconditions(None, true, dir.path()).is_ok());
}
#[test]
fn enterprise_strict_without_lockfile_fails() {
let dir = TempDir::new().unwrap();
let err =
validate_sync_preconditions(Some("enterprise-strict"), false, dir.path()).unwrap_err();
assert!(err.contains("enterprise-strict"), "error was: {err}");
assert!(err.contains("packs.lock"), "error was: {err}");
}
#[test]
fn enterprise_strict_with_lockfile_passes() {
let dir = TempDir::new().unwrap();
let ggen_dir = dir.path().join(".ggen");
fs::create_dir_all(&ggen_dir).unwrap();
fs::write(ggen_dir.join("packs.lock"), "{}").unwrap();
assert!(validate_sync_preconditions(Some("enterprise-strict"), false, dir.path()).is_ok());
}
#[test]
fn permissive_profile_passes_without_lockfile() {
let dir = TempDir::new().unwrap();
assert!(validate_sync_preconditions(Some("permissive"), false, dir.path()).is_ok());
}
#[test]
fn development_profile_passes_without_lockfile() {
let dir = TempDir::new().unwrap();
assert!(validate_sync_preconditions(Some("dev"), false, dir.path()).is_ok());
}
#[test]
fn unknown_profile_name_returns_error() {
let dir = TempDir::new().unwrap();
let err = validate_sync_preconditions(Some("nonexistent"), false, dir.path()).unwrap_err();
assert!(err.contains("Unknown profile"), "error was: {err}");
}
}