use anyhow::{anyhow, Result};
use jsonschema::{JSONSchema, ValidationError};
use serde::{Deserialize, Serialize};
use serde_json::Value;
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct Blueprint {
pub project_name: String,
pub goals: Vec<String>,
pub constraints: Option<Constraints>,
pub traffic_profile: TrafficProfile,
pub prefs: Option<Preferences>,
pub single_language_mode: Option<String>,
}
#[derive(Debug, Clone, Default, Deserialize, Serialize)]
pub struct Constraints {
pub monthly_cost_usd_max: Option<f64>,
pub coldstart_ms_max: Option<f64>,
pub persistence: Option<String>,
pub region_allow: Vec<String>,
pub compliance: Vec<String>,
}
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct TrafficProfile {
pub rps_peak: f64,
#[serde(default)]
pub global: bool,
#[serde(default)]
pub latency_sensitive: bool,
}
#[derive(Debug, Clone, Default, Deserialize, Serialize)]
pub struct Preferences {
pub frontend: Option<Vec<String>>,
pub backend: Option<Vec<String>>,
pub database: Option<Vec<String>>,
pub ai: Option<Vec<String>>,
}
#[derive(Debug, Deserialize, Serialize)]
pub struct Stack {
pub decisions: Vec<Decision>,
pub stack: StackComponent,
pub estimated: Estimated,
pub slo: SLO,
pub risk: Vec<Risk>,
}
#[derive(Debug, Deserialize, Serialize)]
pub struct Decision {
pub topic: String,
pub choice: String,
pub reasons: Vec<String>,
pub alternatives: Vec<String>,
}
#[derive(Debug, Deserialize, Serialize)]
pub struct StackComponent {
pub language: String,
pub frontend: Option<String>,
pub backend: String,
pub database: Option<String>,
pub cache: Option<String>,
pub queue: Option<String>,
pub auth: Option<String>,
pub payments: Option<String>,
pub ai: Option<Vec<String>>,
pub infra: String,
pub ci_cd: Option<String>,
}
#[derive(Debug, Deserialize, Serialize)]
pub struct Estimated {
pub monthly_cost_usd: f64,
}
#[derive(Debug, Deserialize, Serialize)]
pub struct SLO {
pub availability_pct: f64,
pub p95_ms: P95MS,
}
#[derive(Debug, Deserialize, Serialize)]
pub struct P95MS {
pub read: f64,
pub write: f64,
}
#[derive(Debug, Deserialize, Serialize)]
pub struct Risk {
pub id: String,
pub desc: String,
pub mitigation: String,
}
pub fn validate_blueprint_schema(blueprint: &Blueprint) -> Result<()> {
let schema_content = include_str!("../schemas/blueprint.schema.json");
let schema: Value = serde_json::from_str(schema_content)?;
let compiled = JSONSchema::compile(&schema)
.map_err(|e| anyhow!("Failed to compile blueprint schema: {}", e))?;
let blueprint_json = serde_json::to_value(blueprint)?;
if let Err(errors) = compiled.validate(&blueprint_json) {
let error_messages: Vec<String> = errors
.collect::<Vec<ValidationError>>()
.iter()
.map(|e| format!("Validation error at {}: {}", e.instance_path, e))
.collect();
return Err(anyhow!("Blueprint validation failed: {}", error_messages.join(", ")));
}
Ok(())
}
pub fn validate_stack_schema(stack: &Stack) -> Result<()> {
let schema_content = include_str!("../schemas/stack.schema.json");
let schema: Value = serde_json::from_str(schema_content)?;
let compiled = JSONSchema::compile(&schema)
.map_err(|e| anyhow!("Failed to compile stack schema: {}", e))?;
let stack_json = serde_json::to_value(stack)?;
if let Err(errors) = compiled.validate(&stack_json) {
let error_messages: Vec<String> = errors
.collect::<Vec<ValidationError>>()
.iter()
.map(|e| format!("Validation error at {}: {}", e.instance_path, e))
.collect();
return Err(anyhow!("Stack validation failed: {}", error_messages.join(", ")));
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_blueprint_deserialization() {
let blueprint_json = r#"
{
"project_name": "test-app",
"goals": ["high-performance", "low-cost"],
"traffic_profile": {
"rps_peak": 1000.0,
"global": true,
"latency_sensitive": false
}
}
"#;
let blueprint: Result<Blueprint, _> = serde_json::from_str(blueprint_json);
assert!(blueprint.is_ok());
let bp = blueprint.unwrap();
assert_eq!(bp.project_name, "test-app");
assert_eq!(bp.goals.len(), 2);
assert_eq!(bp.traffic_profile.rps_peak, 1000.0);
}
#[test]
fn test_stack_serialization() {
let stack = Stack {
decisions: vec![
Decision {
topic: "backend".to_string(),
choice: "Actix Web 4.11".to_string(),
reasons: vec!["高性能".to_string()],
alternatives: vec!["Axum".to_string()],
}
],
stack: StackComponent {
language: "Rust".to_string(),
frontend: Some("SvelteKit".to_string()),
backend: "Actix Web 4.11".to_string(),
database: Some("PlanetScale".to_string()),
cache: Some("Cloudflare KV".to_string()),
queue: Some("NATS".to_string()),
auth: Some("Passkeys".to_string()),
payments: Some("Stripe".to_string()),
ai: Some(vec!["OpenAI".to_string()]),
infra: "Cloudflare Workers".to_string(),
ci_cd: Some("GitHub Actions".to_string()),
},
estimated: Estimated {
monthly_cost_usd: 100.0,
},
slo: SLO {
availability_pct: 99.95,
p95_ms: P95MS {
read: 800.0,
write: 1200.0,
},
},
risk: vec![],
};
let json = serde_json::to_string_pretty(&stack);
assert!(json.is_ok());
}
}