meerkat_core/
runtime_bootstrap.rs1use serde::{Deserialize, Serialize};
7use std::path::{Path, PathBuf};
8use uuid::Uuid;
9
10#[derive(Debug, Clone, PartialEq, Eq)]
12pub struct RealmLocator {
13 pub state_root: PathBuf,
14 pub realm_id: String,
15}
16
17#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
19#[serde(rename_all = "snake_case", tag = "kind")]
20pub enum RealmSelection {
21 Explicit { realm_id: String },
22 Isolated,
23 WorkspaceDerived { root: PathBuf },
24}
25
26#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
28#[serde(default)]
29pub struct RealmConfig {
30 pub selection: RealmSelection,
31 pub instance_id: Option<String>,
32 pub backend_hint: Option<String>,
34 pub state_root: Option<PathBuf>,
36}
37
38impl Default for RealmConfig {
39 fn default() -> Self {
40 Self {
41 selection: RealmSelection::Isolated,
42 instance_id: None,
43 backend_hint: None,
44 state_root: None,
45 }
46 }
47}
48
49#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Default)]
51#[serde(default)]
52pub struct ContextConfig {
53 pub context_root: Option<PathBuf>,
54 pub user_config_root: Option<PathBuf>,
55}
56
57#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Default)]
59#[serde(default)]
60pub struct RuntimeBootstrap {
61 pub realm: RealmConfig,
62 pub context: ContextConfig,
63}
64
65#[derive(Debug, thiserror::Error)]
67pub enum RuntimeBootstrapError {
68 #[error("`--realm` and `--isolated` cannot be used together")]
69 ConflictingSelection,
70 #[error("invalid explicit realm id: {0}")]
71 InvalidRealmId(String),
72}
73
74pub fn default_state_root() -> PathBuf {
76 dirs::data_dir()
77 .unwrap_or_else(|| PathBuf::from("."))
78 .join("meerkat")
79 .join("realms")
80}
81
82impl RealmConfig {
83 pub fn selection_from_inputs(
85 realm: Option<String>,
86 isolated: bool,
87 default: RealmSelection,
88 ) -> Result<RealmSelection, RuntimeBootstrapError> {
89 if realm.is_some() && isolated {
90 return Err(RuntimeBootstrapError::ConflictingSelection);
91 }
92 if let Some(realm_id) = realm {
93 validate_explicit_realm_id(&realm_id)?;
94 return Ok(RealmSelection::Explicit { realm_id });
95 }
96 if isolated {
97 return Ok(RealmSelection::Isolated);
98 }
99 Ok(default)
100 }
101
102 pub fn resolve_locator(&self) -> Result<RealmLocator, RuntimeBootstrapError> {
104 let state_root = self.state_root.clone().unwrap_or_else(default_state_root);
105 let realm_id = match &self.selection {
106 RealmSelection::Explicit { realm_id } => realm_id.clone(),
107 RealmSelection::Isolated => generate_realm_id(),
108 RealmSelection::WorkspaceDerived { root } => derive_workspace_realm_id(root),
109 };
110 Ok(RealmLocator {
111 state_root,
112 realm_id,
113 })
114 }
115}
116
117pub fn validate_explicit_realm_id(realm_id: &str) -> Result<(), RuntimeBootstrapError> {
118 if realm_id.is_empty()
119 || realm_id.len() > 64
120 || realm_id.contains(':')
121 || realm_id.chars().any(char::is_whitespace)
122 {
123 return Err(RuntimeBootstrapError::InvalidRealmId(realm_id.to_string()));
124 }
125 let mut chars = realm_id.chars();
126 let first = chars
127 .next()
128 .ok_or_else(|| RuntimeBootstrapError::InvalidRealmId(realm_id.to_string()))?;
129 if !first.is_ascii_alphanumeric() {
130 return Err(RuntimeBootstrapError::InvalidRealmId(realm_id.to_string()));
131 }
132 if !chars.all(|ch| ch.is_ascii_alphanumeric() || ch == '_' || ch == '-') {
133 return Err(RuntimeBootstrapError::InvalidRealmId(realm_id.to_string()));
134 }
135 if Uuid::parse_str(realm_id).is_ok() {
137 return Err(RuntimeBootstrapError::InvalidRealmId(realm_id.to_string()));
138 }
139 Ok(())
140}
141
142pub fn generate_realm_id() -> String {
143 format!("realm-{}", Uuid::now_v7())
144}
145
146pub fn derive_workspace_realm_id(path: &Path) -> String {
147 let canonical = std::fs::canonicalize(path).unwrap_or_else(|_| path.to_path_buf());
148 let key = canonical.to_string_lossy();
149 format!("ws-{}", fnv1a64_hex(&key))
150}
151
152pub fn fnv1a64_hex(input: &str) -> String {
153 const OFFSET: u64 = 0xcbf2_9ce4_8422_2325;
154 const PRIME: u64 = 0x0100_0000_01b3;
155 let mut hash = OFFSET;
156 for b in input.as_bytes() {
157 hash ^= u64::from(*b);
158 hash = hash.wrapping_mul(PRIME);
159 }
160 format!("{hash:016x}")
161}
162
163#[cfg(test)]
164#[allow(clippy::expect_used)]
165mod tests {
166 use super::*;
167
168 #[test]
169 fn selection_conflict_is_rejected() {
170 let result = RealmConfig::selection_from_inputs(
171 Some("team".to_string()),
172 true,
173 RealmSelection::Isolated,
174 );
175 assert!(matches!(
176 result,
177 Err(RuntimeBootstrapError::ConflictingSelection)
178 ));
179 }
180
181 #[test]
182 fn explicit_realm_id_validation() {
183 assert!(validate_explicit_realm_id("team-alpha_1").is_ok());
184 assert!(validate_explicit_realm_id("bad:name").is_err());
185 assert!(validate_explicit_realm_id("").is_err());
186 assert!(validate_explicit_realm_id("550e8400-e29b-41d4-a716-446655440000").is_err());
187 }
188
189 #[test]
190 fn workspace_selection_is_deterministic() {
191 let root = PathBuf::from(".");
192 let cfg = RealmConfig {
193 selection: RealmSelection::WorkspaceDerived { root },
194 ..RealmConfig::default()
195 };
196 let a = cfg.resolve_locator().map(|locator| locator.realm_id);
197 let b = cfg.resolve_locator().map(|locator| locator.realm_id);
198 assert!(a.is_ok());
199 assert_eq!(a.ok(), b.ok());
200 }
201}