runeforge 25.8.0

Blueprint to optimal stack JSON converter - Part of Rune* Ecosystem
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());
    }
}