use crate::contract_drift::{DriftBudget, DriftBudgetConfig};
use crate::multi_tenant::MultiTenantWorkspaceRegistry;
use crate::openapi::OpenApiSpec;
use crate::reality_continuum::config::{
ContinuumConfig, ContinuumRule, MergeStrategy, TransitionMode,
};
use crate::scenarios::types::{ScenarioDefinition, ScenarioStep};
use crate::voice::command_parser::{EndpointRequirement, ModelRequirement, ParsedCommand};
use crate::voice::command_parser::{
ParsedDriftBudget, ParsedRealityContinuum, ParsedWorkspaceCreation,
};
use crate::voice::spec_generator::VoiceSpecGenerator;
use crate::Result;
use crate::Workspace;
use mockforge_data::{Domain, PersonaProfile};
use std::collections::HashMap;
#[derive(Debug, Clone)]
pub struct BuiltWorkspace {
pub workspace_id: String,
pub name: String,
pub openapi_spec: Option<OpenApiSpec>,
pub personas: Vec<PersonaProfile>,
pub scenarios: Vec<ScenarioDefinition>,
pub reality_continuum: Option<ContinuumConfig>,
pub drift_budget: Option<DriftBudgetConfig>,
pub creation_log: Vec<String>,
}
pub struct WorkspaceBuilder {
creation_log: Vec<String>,
}
impl WorkspaceBuilder {
pub fn new() -> Self {
Self {
creation_log: Vec::new(),
}
}
pub async fn build_workspace(
&mut self,
registry: &mut MultiTenantWorkspaceRegistry,
parsed: &ParsedWorkspaceCreation,
) -> Result<BuiltWorkspace> {
self.creation_log.clear();
self.validate_requirements(parsed)?;
let workspace_id = Self::sanitize_workspace_id(&parsed.workspace_name);
if registry.get_workspace(&workspace_id).is_ok() {
let suggestions = self.suggest_workspace_names(&workspace_id, registry)?;
return Err(crate::Error::validation(format!(
"Workspace '{}' already exists. Suggested alternatives: {}",
workspace_id,
suggestions.join(", ")
)));
}
self.log(&format!("Creating workspace: {}", workspace_id));
let mut workspace = Workspace::new(parsed.workspace_name.clone());
workspace.description = Some(parsed.workspace_description.clone());
registry.register_workspace(workspace_id.clone(), workspace)?;
self.log(&format!("✓ Workspace '{}' created", workspace_id));
let openapi_spec = self.generate_openapi_spec(parsed).await?;
let personas = self.create_personas(parsed)?;
self.log(&format!("✓ Created {} personas", personas.len()));
let scenarios = self.create_scenarios(parsed, &personas)?;
self.log(&format!("✓ Created {} scenarios", scenarios.len()));
let reality_continuum = if let Some(ref parsed_rc) = parsed.reality_continuum {
Some(self.apply_reality_config(parsed_rc)?)
} else {
None
};
if reality_continuum.is_some() {
self.log("✓ Reality continuum configured");
}
let drift_budget = if let Some(ref parsed_db) = parsed.drift_budget {
Some(self.apply_drift_budget(parsed_db)?)
} else {
None
};
if drift_budget.is_some() {
self.log("✓ Drift budget configured");
}
Ok(BuiltWorkspace {
workspace_id,
name: parsed.workspace_name.clone(),
openapi_spec,
personas,
scenarios,
reality_continuum,
drift_budget,
creation_log: self.creation_log.clone(),
})
}
async fn generate_openapi_spec(
&mut self,
parsed: &ParsedWorkspaceCreation,
) -> Result<Option<OpenApiSpec>> {
if parsed.entities.is_empty() {
return Ok(None);
}
self.log("Generating OpenAPI specification...");
let mut endpoints = Vec::new();
let mut models = Vec::new();
for entity in &parsed.entities {
for endpoint in &entity.endpoints {
endpoints.push(EndpointRequirement {
path: endpoint.path.clone(),
method: endpoint.method.clone(),
description: endpoint.description.clone(),
request_body: None,
response: None,
});
}
if !entity.fields.is_empty() {
models.push(ModelRequirement {
name: entity.name.clone(),
fields: entity.fields.clone(),
});
}
}
let parsed_command = ParsedCommand {
api_type: "workspace".to_string(),
title: parsed.workspace_name.clone(),
description: parsed.workspace_description.clone(),
endpoints,
models,
relationships: vec![],
sample_counts: HashMap::new(),
flows: vec![],
};
let spec_generator = VoiceSpecGenerator::new();
let spec = spec_generator.generate_spec(&parsed_command).await?;
self.log(&format!(
"✓ Generated OpenAPI spec with {} endpoints",
spec.all_paths_and_operations().len()
));
Ok(Some(spec))
}
fn create_personas(&mut self, parsed: &ParsedWorkspaceCreation) -> Result<Vec<PersonaProfile>> {
let mut personas = Vec::new();
let domain = Self::infer_domain(&parsed.workspace_description);
for persona_req in &parsed.personas {
let persona_id = format!("persona:{}", Self::sanitize_id(&persona_req.name));
let mut persona =
PersonaProfile::with_traits(persona_id.clone(), domain, persona_req.traits.clone());
persona.backstory = Some(persona_req.description.clone());
let mut relationships = HashMap::new();
for rel in &persona_req.relationships {
let rel_key = format!("{}:{}", rel.r#type, rel.target_entity);
relationships
.entry(rel_key)
.or_insert_with(Vec::new)
.push(format!("entity:{}", Self::sanitize_id(&rel.target_entity)));
}
persona.relationships = relationships;
personas.push(persona);
}
Ok(personas)
}
fn create_scenarios(
&mut self,
parsed: &ParsedWorkspaceCreation,
_personas: &[PersonaProfile],
) -> Result<Vec<ScenarioDefinition>> {
let mut scenarios = Vec::new();
for scenario_req in &parsed.scenarios {
let scenario_id = Self::sanitize_id(&scenario_req.name);
let mut scenario =
ScenarioDefinition::new(scenario_id.clone(), scenario_req.name.clone());
scenario.description = Some(scenario_req.description.clone());
scenario.tags = vec![scenario_req.r#type.clone()];
for (idx, step_req) in scenario_req.steps.iter().enumerate() {
let (method, path) = Self::parse_endpoint(&step_req.endpoint)?;
let mut step = ScenarioStep::new(
format!("step-{}", idx + 1),
step_req.description.clone(),
method,
path,
)
.expect_status(if scenario_req.r#type == "failure" {
400
} else {
200
});
if scenario_req.r#type == "slow_path" {
step.delay_ms = Some(2000); }
scenario = scenario.add_step(step);
}
scenarios.push(scenario);
}
Ok(scenarios)
}
fn apply_reality_config(&mut self, parsed: &ParsedRealityContinuum) -> Result<ContinuumConfig> {
let mut config = ContinuumConfig::default();
if parsed.enabled {
config = config.enable();
}
config = config.with_default_ratio(parsed.default_ratio);
let transition_mode = match parsed.transition_mode.as_str() {
"time_based" => TransitionMode::TimeBased,
"scheduled" => TransitionMode::Scheduled,
_ => TransitionMode::Manual,
};
config = config.with_transition_mode(transition_mode);
let merge_strategy = match parsed.merge_strategy.as_str() {
"weighted" => MergeStrategy::Weighted,
"body_blend" => MergeStrategy::BodyBlend,
_ => MergeStrategy::FieldLevel,
};
config = config.with_merge_strategy(merge_strategy);
for rule in &parsed.route_rules {
let continuum_rule = ContinuumRule::new(rule.pattern.clone(), rule.ratio);
config = config.add_route(continuum_rule);
}
Ok(config)
}
fn apply_drift_budget(&mut self, parsed: &ParsedDriftBudget) -> Result<DriftBudgetConfig> {
let mut config = DriftBudgetConfig::default();
if !parsed.enabled {
config.enabled = false;
}
let default_budget = DriftBudget {
max_breaking_changes: parsed.max_breaking_changes,
max_non_breaking_changes: parsed.max_non_breaking_changes,
max_field_churn_percent: parsed.max_field_churn_percent,
time_window_days: parsed.time_window_days,
severity_threshold: crate::ai_contract_diff::MismatchSeverity::High,
enabled: parsed.enabled,
};
config.default_budget = Some(default_budget);
for (service_name, service_budget) in &parsed.per_service_budgets {
let budget = DriftBudget {
max_breaking_changes: service_budget.max_breaking_changes,
max_non_breaking_changes: service_budget.max_non_breaking_changes,
max_field_churn_percent: None,
time_window_days: None,
severity_threshold: crate::ai_contract_diff::MismatchSeverity::High,
enabled: parsed.enabled,
};
config.per_service_budgets.insert(service_name.clone(), budget);
}
Ok(config)
}
fn sanitize_workspace_id(name: &str) -> String {
name.to_lowercase()
.chars()
.map(|c| {
if c.is_alphanumeric() || c == '-' || c == '_' {
c
} else {
'-'
}
})
.collect::<String>()
.trim_matches('-')
.to_string()
}
fn sanitize_id(name: &str) -> String {
name.to_lowercase()
.chars()
.map(|c| {
if c.is_alphanumeric() || c == '-' || c == '_' {
c
} else {
'-'
}
})
.collect::<String>()
.trim_matches('-')
.to_string()
}
fn infer_domain(description: &str) -> Domain {
let desc_lower = description.to_lowercase();
if desc_lower.contains("e-commerce")
|| desc_lower.contains("ecommerce")
|| desc_lower.contains("shop")
{
Domain::Ecommerce
} else if desc_lower.contains("bank")
|| desc_lower.contains("finance")
|| desc_lower.contains("payment")
{
Domain::Finance
} else if desc_lower.contains("health") || desc_lower.contains("medical") {
Domain::Healthcare
} else if desc_lower.contains("iot") || desc_lower.contains("device") {
Domain::Iot
} else {
Domain::General
}
}
fn parse_endpoint(endpoint: &str) -> Result<(String, String)> {
let parts: Vec<&str> = endpoint.trim().splitn(2, ' ').collect();
if parts.len() != 2 {
return Err(crate::Error::config(format!(
"Invalid endpoint format: {}. Expected 'METHOD /path'",
endpoint
)));
}
let method = parts[0].to_uppercase();
let path = parts[1].to_string();
Ok((method, path))
}
fn validate_requirements(&self, parsed: &ParsedWorkspaceCreation) -> Result<()> {
for entity in &parsed.entities {
if entity.endpoints.len() < 2 {
return Err(crate::Error::validation(format!(
"Entity '{}' must have at least 2 endpoints. Found {}.",
entity.name,
entity.endpoints.len()
)));
}
}
if parsed.personas.len() < 2 {
return Err(crate::Error::validation(format!(
"Workspace must have at least 2 personas. Found {}.",
parsed.personas.len()
)));
}
for persona in &parsed.personas {
if persona.relationships.is_empty() {
return Err(crate::Error::validation(format!(
"Persona '{}' must have at least one relationship.",
persona.name
)));
}
}
if parsed.scenarios.len() < 2 {
return Err(crate::Error::validation(format!(
"Workspace must have at least 2 behavioral scenarios. Found {}.",
parsed.scenarios.len()
)));
}
let scenario_types: Vec<&str> =
parsed.scenarios.iter().map(|s| s.r#type.as_str()).collect();
let has_happy = scenario_types.contains(&"happy_path");
let has_failure = scenario_types.contains(&"failure");
let has_slow = scenario_types.contains(&"slow_path");
if !has_happy && !has_failure && !has_slow {
return Err(crate::Error::validation(
"Workspace must have at least one scenario of type: happy_path, failure, or slow_path".to_string(),
));
}
Ok(())
}
fn suggest_workspace_names(
&self,
base_name: &str,
registry: &MultiTenantWorkspaceRegistry,
) -> Result<Vec<String>> {
let mut suggestions = Vec::new();
let mut counter = 1;
while suggestions.len() < 3 && counter < 10 {
let candidate = format!("{}-{}", base_name, counter);
if registry.get_workspace(&candidate).is_err() {
suggestions.push(candidate);
}
counter += 1;
}
if suggestions.len() < 3 {
let timestamp = chrono::Utc::now().format("%Y%m%d");
let candidate = format!("{}-{}", base_name, timestamp);
if registry.get_workspace(&candidate).is_err() {
suggestions.push(candidate);
}
}
Ok(suggestions)
}
fn log(&mut self, message: &str) {
self.creation_log.push(message.to_string());
}
}
impl Default for WorkspaceBuilder {
fn default() -> Self {
Self::new()
}
}