mockforge_core/pillars.rs
1//! Pillar metadata system for compile-time pillar tagging
2//!
3//! This module provides types and utilities for tagging modules, functions, and features
4//! with MockForge pillars. Pillars help organize code, enable pillar-based queries for
5//! test coverage and production usage, and guide users to relevant features.
6//!
7//! ## The Five Pillars
8//!
9//! - **[Reality]** – Everything that makes mocks feel like a real, evolving backend
10//! - **[Contracts]** – Schema, drift, validation, and safety nets
11//! - **[DevX]** – SDKs, generators, playgrounds, ergonomics
12//! - **[Cloud]** – Registry, orgs, governance, monetization, marketplace
13//! - **[AI]** – LLM/voice flows, AI diff/assist, generative behaviors
14//!
15//! ## Usage
16//!
17//! Tag modules in their documentation comments:
18//!
19//! ```rust
20//! //! Pillars: [Reality][AI]
21//! //!
22//! //! This module implements Smart Personas with relationship graphs
23//! //! and AI-powered data generation.
24//! ```
25//!
26//! Or use the `Pillar` enum programmatically:
27//!
28//! ```rust
29//! use mockforge_core::pillars::{Pillar, PillarMetadata};
30//!
31//! let metadata = PillarMetadata::new()
32//! .with_pillar(Pillar::Reality)
33//! .with_pillar(Pillar::Ai);
34//! ```
35
36use serde::{Deserialize, Serialize};
37use std::collections::HashSet;
38use std::fmt;
39
40/// MockForge pillar identifier
41///
42/// Every feature in MockForge maps to one or more pillars. This structure helps:
43/// - Communicate value clearly in changelogs, docs, and marketing
44/// - Prioritize development based on pillar investment
45/// - Maintain consistency across features and releases
46/// - Guide users to the right features for their needs
47#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Hash)]
48#[serde(rename_all = "lowercase")]
49pub enum Pillar {
50 /// Reality pillar - everything that makes mocks feel like a real, evolving backend
51 ///
52 /// Key capabilities:
53 /// - Realistic data generation with relationships and constraints
54 /// - Stateful behavior and persistence
55 /// - Network condition simulation (latency, packet loss, failures)
56 /// - Time-based mutations and temporal simulation
57 /// - Progressive data evolution and drift
58 /// - Multi-protocol support
59 Reality,
60 /// Contracts pillar - schema, drift, validation, and safety nets
61 ///
62 /// Key capabilities:
63 /// - OpenAPI/GraphQL schema validation
64 /// - Request/response validation with detailed error reporting
65 /// - Contract drift detection and monitoring
66 /// - Automatic API sync and change detection
67 /// - Schema-driven mock generation
68 Contracts,
69 /// DevX pillar - SDKs, generators, playgrounds, ergonomics
70 ///
71 /// Key capabilities:
72 /// - Multi-language SDKs (Rust, Node.js, Python, Go, Java, .NET)
73 /// - Client code generation (React, Vue, Angular, Svelte)
74 /// - Interactive playgrounds and admin UI
75 /// - CLI tooling and configuration management
76 /// - Comprehensive documentation and examples
77 /// - Plugin system for extensibility
78 DevX,
79 /// Cloud pillar - registry, orgs, governance, monetization, marketplace
80 ///
81 /// Key capabilities:
82 /// - Organization and user management
83 /// - Scenario marketplace and sharing
84 /// - Registry server for mock distribution
85 /// - Cloud workspaces and synchronization
86 /// - Governance and access controls
87 /// - Monetization infrastructure
88 Cloud,
89 /// AI pillar - LLM/voice flows, AI diff/assist, generative behaviors
90 ///
91 /// Key capabilities:
92 /// - LLM-powered mock generation
93 /// - AI-driven data synthesis
94 /// - Voice interface for mock creation
95 /// - Intelligent contract analysis
96 /// - Generative data behaviors
97 /// - Natural language to mock conversion
98 Ai,
99}
100
101impl Pillar {
102 /// Convert pillar to lowercase string representation
103 ///
104 /// # Examples
105 ///
106 /// ```
107 /// use mockforge_core::pillars::Pillar;
108 ///
109 /// assert_eq!(Pillar::Reality.as_str(), "reality");
110 /// assert_eq!(Pillar::Contracts.as_str(), "contracts");
111 /// assert_eq!(Pillar::DevX.as_str(), "devx");
112 /// assert_eq!(Pillar::Cloud.as_str(), "cloud");
113 /// assert_eq!(Pillar::Ai.as_str(), "ai");
114 /// ```
115 pub fn as_str(&self) -> &'static str {
116 match self {
117 Pillar::Reality => "reality",
118 Pillar::Contracts => "contracts",
119 Pillar::DevX => "devx",
120 Pillar::Cloud => "cloud",
121 Pillar::Ai => "ai",
122 }
123 }
124
125 /// Parse pillar from string (case-insensitive)
126 ///
127 /// # Examples
128 ///
129 /// ```
130 /// use mockforge_core::pillars::Pillar;
131 ///
132 /// assert_eq!(Pillar::from_str("reality"), Some(Pillar::Reality));
133 /// assert_eq!(Pillar::from_str("REALITY"), Some(Pillar::Reality));
134 /// assert_eq!(Pillar::from_str("Contracts"), Some(Pillar::Contracts));
135 /// assert_eq!(Pillar::from_str("devx"), Some(Pillar::DevX));
136 /// assert_eq!(Pillar::from_str("invalid"), None);
137 /// ```
138 #[allow(clippy::should_implement_trait)]
139 pub fn from_str(s: &str) -> Option<Self> {
140 match s.to_lowercase().as_str() {
141 "reality" => Some(Pillar::Reality),
142 "contracts" => Some(Pillar::Contracts),
143 "devx" => Some(Pillar::DevX),
144 "cloud" => Some(Pillar::Cloud),
145 "ai" => Some(Pillar::Ai),
146 _ => None,
147 }
148 }
149
150 /// Get display name for the pillar (with brackets for changelog format)
151 ///
152 /// # Examples
153 ///
154 /// ```
155 /// use mockforge_core::pillars::Pillar;
156 ///
157 /// assert_eq!(Pillar::Reality.display_name(), "[Reality]");
158 /// assert_eq!(Pillar::Contracts.display_name(), "[Contracts]");
159 /// ```
160 pub fn display_name(&self) -> String {
161 match self {
162 Pillar::Reality => "[Reality]".to_string(),
163 Pillar::Contracts => "[Contracts]".to_string(),
164 Pillar::DevX => "[DevX]".to_string(),
165 Pillar::Cloud => "[Cloud]".to_string(),
166 Pillar::Ai => "[AI]".to_string(),
167 }
168 }
169
170 /// Get all pillars
171 ///
172 /// # Examples
173 ///
174 /// ```
175 /// use mockforge_core::pillars::Pillar;
176 ///
177 /// let all = Pillar::all();
178 /// assert_eq!(all.len(), 5);
179 /// assert!(all.contains(&Pillar::Reality));
180 /// assert!(all.contains(&Pillar::Contracts));
181 /// assert!(all.contains(&Pillar::DevX));
182 /// assert!(all.contains(&Pillar::Cloud));
183 /// assert!(all.contains(&Pillar::Ai));
184 /// ```
185 pub fn all() -> Vec<Pillar> {
186 vec![
187 Pillar::Reality,
188 Pillar::Contracts,
189 Pillar::DevX,
190 Pillar::Cloud,
191 Pillar::Ai,
192 ]
193 }
194}
195
196impl fmt::Display for Pillar {
197 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
198 write!(f, "{}", self.as_str())
199 }
200}
201
202/// Pillar metadata for tagging modules, functions, or features
203///
204/// This type allows associating one or more pillars with code entities.
205/// It's used for compile-time tagging and can be extracted from documentation
206/// comments or set programmatically.
207#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
208pub struct PillarMetadata {
209 /// Set of pillars associated with this entity
210 pillars: HashSet<Pillar>,
211}
212
213impl PillarMetadata {
214 /// Create new empty pillar metadata
215 pub fn new() -> Self {
216 Self {
217 pillars: HashSet::new(),
218 }
219 }
220
221 /// Create pillar metadata with a single pillar
222 pub fn with_pillar(mut self, pillar: Pillar) -> Self {
223 self.pillars.insert(pillar);
224 self
225 }
226
227 /// Create pillar metadata with multiple pillars
228 pub fn with_pillars(mut self, pillars: &[Pillar]) -> Self {
229 for pillar in pillars {
230 self.pillars.insert(*pillar);
231 }
232 self
233 }
234
235 /// Add a pillar to existing metadata
236 pub fn add_pillar(&mut self, pillar: Pillar) {
237 self.pillars.insert(pillar);
238 }
239
240 /// Check if metadata contains a specific pillar
241 pub fn has_pillar(&self, pillar: Pillar) -> bool {
242 self.pillars.contains(&pillar)
243 }
244
245 /// Get all pillars in this metadata
246 pub fn pillars(&self) -> Vec<Pillar> {
247 let mut result: Vec<Pillar> = self.pillars.iter().copied().collect();
248 // Sort for consistent output
249 result.sort_by_key(|p| p.as_str());
250 result
251 }
252
253 /// Check if metadata is empty
254 pub fn is_empty(&self) -> bool {
255 self.pillars.is_empty()
256 }
257
258 /// Format pillars as changelog-style tags (e.g., "[Reality][AI]")
259 pub fn to_changelog_tags(&self) -> String {
260 let mut pillars: Vec<Pillar> = self.pillars.iter().copied().collect();
261 // Sort for consistent output
262 pillars.sort_by_key(|p| p.as_str());
263 pillars.iter().map(|p| p.display_name()).collect::<Vec<_>>().join("")
264 }
265
266 /// Parse pillar metadata from documentation comment
267 ///
268 /// Looks for patterns like:
269 /// - `Pillars: [Reality][AI]`
270 /// - `Pillar: [Reality]`
271 /// - `//! Pillars: [Reality][AI]`
272 ///
273 /// # Examples
274 ///
275 /// ```
276 /// use mockforge_core::pillars::{Pillar, PillarMetadata};
277 ///
278 /// let doc = "//! Pillars: [Reality][AI]\n//! This module does something";
279 /// let metadata = PillarMetadata::from_doc_comment(doc).unwrap();
280 /// assert!(metadata.has_pillar(Pillar::Reality));
281 /// assert!(metadata.has_pillar(Pillar::Ai));
282 /// ```
283 pub fn from_doc_comment(doc: &str) -> Option<Self> {
284 // Look for "Pillars:" or "Pillar:" followed by bracket notation
285 let re = regex::Regex::new(r"(?i)(?:pillars?):\s*(\[[^\]]+\])+").ok()?;
286 let caps = re.captures(doc)?;
287 let full_match = caps.get(0)?;
288
289 // Extract all [Pillar] tags
290 let tag_re = regex::Regex::new(r"\[([^\]]+)\]").ok()?;
291 let mut metadata = Self::new();
292
293 for cap in tag_re.captures_iter(full_match.as_str()) {
294 if let Some(pillar_name) = cap.get(1) {
295 if let Some(pillar) = Pillar::from_str(pillar_name.as_str()) {
296 metadata.add_pillar(pillar);
297 }
298 }
299 }
300
301 if metadata.is_empty() {
302 None
303 } else {
304 Some(metadata)
305 }
306 }
307}
308
309impl Default for PillarMetadata {
310 fn default() -> Self {
311 Self::new()
312 }
313}
314
315/// Parse pillar tags from a list of scenario tags
316///
317/// Extracts pillar tags in the format `[PillarName]` from a list of tags.
318/// Pillar tags are recognized in formats like:
319/// - `[Cloud]`
320/// - `[Contracts]`
321/// - `[Reality]`
322/// - `[Cloud][Contracts][Reality]` (multiple pillars in one tag)
323///
324/// # Examples
325///
326/// ```
327/// use mockforge_core::pillars::{Pillar, parse_pillar_tags_from_scenario_tags};
328///
329/// let tags = vec!["[Cloud]".to_string(), "auth".to_string(), "[Contracts][Reality]".to_string()];
330/// let pillars = parse_pillar_tags_from_scenario_tags(&tags);
331/// assert!(pillars.contains(&Pillar::Cloud));
332/// assert!(pillars.contains(&Pillar::Contracts));
333/// assert!(pillars.contains(&Pillar::Reality));
334/// assert_eq!(pillars.len(), 3);
335/// ```
336pub fn parse_pillar_tags_from_scenario_tags(tags: &[String]) -> Vec<Pillar> {
337 let mut pillars = Vec::new();
338 let tag_re = regex::Regex::new(r"\[([^\]]+)\]").ok();
339
340 if tag_re.is_none() {
341 return pillars;
342 }
343
344 let tag_re = tag_re.unwrap();
345
346 for tag in tags {
347 // Extract all [PillarName] patterns from the tag
348 for cap in tag_re.captures_iter(tag) {
349 if let Some(pillar_name) = cap.get(1) {
350 if let Some(pillar) = Pillar::from_str(pillar_name.as_str()) {
351 if !pillars.contains(&pillar) {
352 pillars.push(pillar);
353 }
354 }
355 }
356 }
357 }
358
359 pillars
360}
361
362/// Check if a tag string contains pillar tags
363///
364/// Returns true if the tag contains at least one valid pillar tag in bracket notation.
365///
366/// # Examples
367///
368/// ```
369/// use mockforge_core::pillars::has_pillar_tags;
370///
371/// assert!(has_pillar_tags("[Cloud]"));
372/// assert!(has_pillar_tags("[Contracts][Reality]"));
373/// assert!(has_pillar_tags("auth-[Cloud]-test"));
374/// assert!(!has_pillar_tags("auth"));
375/// assert!(!has_pillar_tags("[Invalid]"));
376/// ```
377pub fn has_pillar_tags(tag: &str) -> bool {
378 let tag_re = regex::Regex::new(r"\[([^\]]+)\]").ok();
379
380 if let Some(tag_re) = tag_re {
381 for cap in tag_re.captures_iter(tag) {
382 if let Some(pillar_name) = cap.get(1) {
383 if Pillar::from_str(pillar_name.as_str()).is_some() {
384 return true;
385 }
386 }
387 }
388 }
389
390 false
391}
392
393/// Extract pillar metadata from scenario tags
394///
395/// Parses all pillar tags from a list of scenario tags and returns them as PillarMetadata.
396///
397/// # Examples
398///
399/// ```
400/// use mockforge_core::pillars::{Pillar, pillar_metadata_from_scenario_tags};
401///
402/// let tags = vec!["[Cloud]".to_string(), "[Contracts]".to_string(), "auth".to_string()];
403/// let metadata = pillar_metadata_from_scenario_tags(&tags);
404/// assert!(metadata.has_pillar(Pillar::Cloud));
405/// assert!(metadata.has_pillar(Pillar::Contracts));
406/// assert!(!metadata.has_pillar(Pillar::Reality));
407/// ```
408pub fn pillar_metadata_from_scenario_tags(tags: &[String]) -> PillarMetadata {
409 let pillars = parse_pillar_tags_from_scenario_tags(tags);
410 PillarMetadata::from(pillars)
411}
412
413impl From<Vec<Pillar>> for PillarMetadata {
414 fn from(pillars: Vec<Pillar>) -> Self {
415 Self {
416 pillars: pillars.into_iter().collect(),
417 }
418 }
419}
420
421impl From<&[Pillar]> for PillarMetadata {
422 fn from(pillars: &[Pillar]) -> Self {
423 Self {
424 pillars: pillars.iter().copied().collect(),
425 }
426 }
427}
428
429#[cfg(test)]
430mod tests {
431 use super::*;
432
433 #[test]
434 fn test_pillar_as_str() {
435 assert_eq!(Pillar::Reality.as_str(), "reality");
436 assert_eq!(Pillar::Contracts.as_str(), "contracts");
437 assert_eq!(Pillar::DevX.as_str(), "devx");
438 assert_eq!(Pillar::Cloud.as_str(), "cloud");
439 assert_eq!(Pillar::Ai.as_str(), "ai");
440 }
441
442 #[test]
443 fn test_pillar_from_str() {
444 assert_eq!(Pillar::from_str("reality"), Some(Pillar::Reality));
445 assert_eq!(Pillar::from_str("REALITY"), Some(Pillar::Reality));
446 assert_eq!(Pillar::from_str("Contracts"), Some(Pillar::Contracts));
447 assert_eq!(Pillar::from_str("devx"), Some(Pillar::DevX));
448 assert_eq!(Pillar::from_str("invalid"), None);
449 }
450
451 #[test]
452 fn test_pillar_display_name() {
453 assert_eq!(Pillar::Reality.display_name(), "[Reality]");
454 assert_eq!(Pillar::Contracts.display_name(), "[Contracts]");
455 assert_eq!(Pillar::DevX.display_name(), "[DevX]");
456 assert_eq!(Pillar::Cloud.display_name(), "[Cloud]");
457 assert_eq!(Pillar::Ai.display_name(), "[AI]");
458 }
459
460 #[test]
461 fn test_pillar_metadata() {
462 let mut metadata = PillarMetadata::new();
463 assert!(metadata.is_empty());
464
465 metadata.add_pillar(Pillar::Reality);
466 assert!(!metadata.is_empty());
467 assert!(metadata.has_pillar(Pillar::Reality));
468 assert!(!metadata.has_pillar(Pillar::Contracts));
469
470 metadata.add_pillar(Pillar::Ai);
471 assert!(metadata.has_pillar(Pillar::Reality));
472 assert!(metadata.has_pillar(Pillar::Ai));
473 assert_eq!(metadata.pillars().len(), 2);
474 }
475
476 #[test]
477 fn test_pillar_metadata_builder() {
478 let metadata = PillarMetadata::new().with_pillar(Pillar::Reality).with_pillar(Pillar::Ai);
479
480 assert!(metadata.has_pillar(Pillar::Reality));
481 assert!(metadata.has_pillar(Pillar::Ai));
482 assert_eq!(metadata.pillars().len(), 2);
483 }
484
485 #[test]
486 fn test_pillar_metadata_changelog_tags() {
487 let metadata = PillarMetadata::from(vec![Pillar::Reality, Pillar::Ai]);
488 let tags = metadata.to_changelog_tags();
489 // Should be sorted alphabetically: [AI][Reality]
490 assert!(tags.contains("[AI]"));
491 assert!(tags.contains("[Reality]"));
492 }
493
494 #[test]
495 fn test_pillar_metadata_from_doc_comment() {
496 let doc = "//! Pillars: [Reality][AI]\n//! This module does something";
497 let metadata = PillarMetadata::from_doc_comment(doc).unwrap();
498 assert!(metadata.has_pillar(Pillar::Reality));
499 assert!(metadata.has_pillar(Pillar::Ai));
500
501 let doc2 = "/// Pillar: [Contracts]";
502 let metadata2 = PillarMetadata::from_doc_comment(doc2).unwrap();
503 assert!(metadata2.has_pillar(Pillar::Contracts));
504
505 let doc3 = "//! No pillars here";
506 assert!(PillarMetadata::from_doc_comment(doc3).is_none());
507 }
508
509 #[test]
510 fn test_parse_pillar_tags_from_scenario_tags() {
511 use super::{parse_pillar_tags_from_scenario_tags, Pillar};
512
513 let tags = vec![
514 "[Cloud]".to_string(),
515 "auth".to_string(),
516 "[Contracts][Reality]".to_string(),
517 ];
518 let pillars = parse_pillar_tags_from_scenario_tags(&tags);
519 assert!(pillars.contains(&Pillar::Cloud));
520 assert!(pillars.contains(&Pillar::Contracts));
521 assert!(pillars.contains(&Pillar::Reality));
522 assert_eq!(pillars.len(), 3);
523
524 let tags2 = vec!["normal".to_string(), "test".to_string()];
525 let pillars2 = parse_pillar_tags_from_scenario_tags(&tags2);
526 assert!(pillars2.is_empty());
527
528 let tags3 = vec!["[Cloud][Contracts][Reality]".to_string()];
529 let pillars3 = parse_pillar_tags_from_scenario_tags(&tags3);
530 assert_eq!(pillars3.len(), 3);
531 assert!(pillars3.contains(&Pillar::Cloud));
532 assert!(pillars3.contains(&Pillar::Contracts));
533 assert!(pillars3.contains(&Pillar::Reality));
534 }
535
536 #[test]
537 fn test_has_pillar_tags() {
538 use super::has_pillar_tags;
539
540 assert!(has_pillar_tags("[Cloud]"));
541 assert!(has_pillar_tags("[Contracts][Reality]"));
542 assert!(has_pillar_tags("auth-[Cloud]-test"));
543 assert!(!has_pillar_tags("auth"));
544 assert!(!has_pillar_tags("[Invalid]"));
545 assert!(!has_pillar_tags(""));
546 }
547
548 #[test]
549 fn test_pillar_metadata_from_scenario_tags() {
550 use super::{pillar_metadata_from_scenario_tags, Pillar};
551
552 let tags = vec![
553 "[Cloud]".to_string(),
554 "[Contracts]".to_string(),
555 "auth".to_string(),
556 ];
557 let metadata = pillar_metadata_from_scenario_tags(&tags);
558 assert!(metadata.has_pillar(Pillar::Cloud));
559 assert!(metadata.has_pillar(Pillar::Contracts));
560 assert!(!metadata.has_pillar(Pillar::Reality));
561
562 let tags2 = vec!["[Cloud][Contracts][Reality]".to_string()];
563 let metadata2 = pillar_metadata_from_scenario_tags(&tags2);
564 assert!(metadata2.has_pillar(Pillar::Cloud));
565 assert!(metadata2.has_pillar(Pillar::Contracts));
566 assert!(metadata2.has_pillar(Pillar::Reality));
567 }
568}