use clap::Subcommand;
use serde::{Deserialize, Serialize};
use std::fs;
use std::path::{Path, PathBuf};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct BlueprintMetadata {
pub manifest_version: String,
pub name: String,
pub version: String,
pub title: String,
pub description: String,
pub author: String,
#[serde(default)]
pub author_email: Option<String>,
pub category: String,
#[serde(default)]
pub tags: Vec<String>,
#[serde(default)]
pub setup: Option<BlueprintSetup>,
#[serde(default)]
pub compatibility: Option<BlueprintCompatibility>,
#[serde(default)]
pub files: Vec<String>,
#[serde(default)]
pub readme: Option<String>,
#[serde(default)]
pub contracts: Vec<ContractInfo>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct BlueprintSetup {
#[serde(default)]
pub personas: Vec<PersonaInfo>,
#[serde(default)]
pub reality: Option<RealityInfo>,
#[serde(default)]
pub flows: Vec<FlowInfo>,
#[serde(default)]
pub scenarios: Vec<ScenarioInfo>,
#[serde(default)]
pub playground: Option<PlaygroundInfo>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PersonaInfo {
pub id: String,
pub name: String,
#[serde(default)]
pub description: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RealityInfo {
pub level: String,
#[serde(default)]
pub description: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct FlowInfo {
pub id: String,
pub name: String,
#[serde(default)]
pub description: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ScenarioInfo {
pub id: String,
pub name: String,
pub r#type: String, #[serde(default)]
pub description: Option<String>,
pub file: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ContractInfo {
pub file: String,
#[serde(default)]
pub description: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PlaygroundInfo {
#[serde(default = "default_true")]
pub enabled: bool,
pub collection_file: String,
}
fn default_true() -> bool {
true
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct BlueprintCompatibility {
#[serde(default = "default_min_version")]
pub min_version: String,
#[serde(default)]
pub max_version: Option<String>,
#[serde(default)]
pub required_features: Vec<String>,
#[serde(default)]
pub protocols: Vec<String>,
}
fn default_min_version() -> String {
"0.3.0".to_string()
}
#[derive(Subcommand)]
pub enum BlueprintCommands {
List {
#[arg(short, long)]
detailed: bool,
#[arg(short, long)]
category: Option<String>,
},
Create {
name: String,
#[arg(short, long)]
blueprint: String,
#[arg(short, long)]
output: Option<PathBuf>,
#[arg(long)]
force: bool,
},
Info {
blueprint_id: String,
},
}
fn get_blueprints_dir() -> PathBuf {
PathBuf::from("blueprints")
}
pub fn list_blueprints(detailed: bool, category: Option<String>) -> anyhow::Result<()> {
let blueprints_dir = get_blueprints_dir();
if !blueprints_dir.exists() {
println!("No blueprints directory found at: {}", blueprints_dir.display());
return Ok(());
}
let mut blueprints = Vec::new();
for entry in fs::read_dir(&blueprints_dir)? {
let entry = entry?;
let path = entry.path();
if path.is_dir() {
let blueprint_yaml = path.join("blueprint.yaml");
if blueprint_yaml.exists() {
if let Ok(metadata) = load_blueprint_metadata(&blueprint_yaml) {
if let Some(ref cat) = category {
if metadata.category != *cat {
continue;
}
}
blueprints.push((path, metadata));
}
}
}
}
if blueprints.is_empty() {
println!("No blueprints found.");
return Ok(());
}
blueprints.sort_by(|a, b| a.1.name.cmp(&b.1.name));
println!("Available Blueprints:\n");
for (path, metadata) in blueprints {
if detailed {
println!("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
println!("Name: {}", metadata.name);
println!("Title: {}", metadata.title);
println!("Version: {}", metadata.version);
println!("Category: {}", metadata.category);
println!("Description: {}", metadata.description.lines().next().unwrap_or(""));
if !metadata.tags.is_empty() {
println!("Tags: {}", metadata.tags.join(", "));
}
println!("Path: {}", path.display());
println!();
} else {
println!(" • {} ({}) - {}", metadata.name, metadata.category, metadata.title);
}
}
Ok(())
}
pub fn create_from_blueprint(
name: String,
blueprint_id: String,
output: Option<PathBuf>,
force: bool,
) -> anyhow::Result<()> {
let blueprints_dir = get_blueprints_dir();
let blueprint_path = blueprints_dir.join(&blueprint_id);
if !blueprint_path.exists() {
anyhow::bail!("Blueprint '{}' not found at: {}", blueprint_id, blueprint_path.display());
}
let blueprint_yaml = blueprint_path.join("blueprint.yaml");
if !blueprint_yaml.exists() {
anyhow::bail!("Blueprint metadata not found: {}", blueprint_yaml.display());
}
let metadata = load_blueprint_metadata(&blueprint_yaml)?;
let output_dir = output.unwrap_or_else(|| PathBuf::from(&name));
if output_dir.exists() && !force {
anyhow::bail!(
"Directory '{}' already exists. Use --force to overwrite.",
output_dir.display()
);
}
if output_dir.exists() && force {
fs::remove_dir_all(&output_dir)?;
}
fs::create_dir_all(&output_dir)?;
println!("Creating project '{}' from blueprint '{}'...", name, blueprint_id);
copy_blueprint_files(&blueprint_path, &output_dir, &metadata)?;
generate_project_files(&output_dir, &name, &metadata)?;
println!("✅ Project created successfully!");
println!("\nNext steps:");
println!(" 1. cd {}", output_dir.display());
println!(" 2. Review mockforge.yaml configuration");
println!(" 3. Run: mockforge serve");
Ok(())
}
pub fn show_blueprint_info(blueprint_id: String) -> anyhow::Result<()> {
let blueprints_dir = get_blueprints_dir();
let blueprint_path = blueprints_dir.join(&blueprint_id);
let blueprint_yaml = blueprint_path.join("blueprint.yaml");
if !blueprint_yaml.exists() {
anyhow::bail!("Blueprint '{}' not found", blueprint_id);
}
let metadata = load_blueprint_metadata(&blueprint_yaml)?;
println!("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
println!("Blueprint: {}", metadata.name);
println!("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
println!("\nTitle: {}", metadata.title);
println!("Version: {}", metadata.version);
println!("Category: {}", metadata.category);
println!("Author: {}", metadata.author);
if let Some(email) = &metadata.author_email {
println!("Email: {}", email);
}
println!("\nDescription:");
for line in metadata.description.lines() {
println!(" {}", line);
}
if !metadata.tags.is_empty() {
println!("\nTags: {}", metadata.tags.join(", "));
}
if let Some(setup) = &metadata.setup {
if !setup.personas.is_empty() {
println!("\nPersonas:");
for persona in &setup.personas {
println!(" • {} - {}", persona.id, persona.name);
if let Some(desc) = &persona.description {
println!(" {}", desc);
}
}
}
if let Some(reality) = &setup.reality {
println!("\nReality Level: {}", reality.level);
if let Some(desc) = &reality.description {
println!(" {}", desc);
}
}
if !setup.flows.is_empty() {
println!("\nSample Flows:");
for flow in &setup.flows {
println!(" • {} - {}", flow.id, flow.name);
if let Some(desc) = &flow.description {
println!(" {}", desc);
}
}
}
if !setup.scenarios.is_empty() {
println!("\nScenarios:");
for scenario in &setup.scenarios {
println!(" • {} ({}) - {}", scenario.id, scenario.r#type, scenario.name);
if let Some(desc) = &scenario.description {
println!(" {}", desc);
}
}
}
}
if !metadata.contracts.is_empty() {
println!("\nContract Schemas:");
for contract in &metadata.contracts {
println!(" • {}", contract.file);
if let Some(desc) = &contract.description {
println!(" {}", desc);
}
}
}
println!("\nPath: {}", blueprint_path.display());
Ok(())
}
fn load_blueprint_metadata(path: &Path) -> anyhow::Result<BlueprintMetadata> {
let content = fs::read_to_string(path)?;
let metadata: BlueprintMetadata = serde_yaml::from_str(&content)?;
Ok(metadata)
}
fn copy_blueprint_files(
blueprint_path: &Path,
output_dir: &Path,
metadata: &BlueprintMetadata,
) -> anyhow::Result<()> {
let config_src = blueprint_path.join("config.yaml");
if config_src.exists() {
let config_dst = output_dir.join("mockforge.yaml");
fs::copy(&config_src, &config_dst)?;
println!(" ✓ Created mockforge.yaml");
}
let personas_src = blueprint_path.join("personas");
if personas_src.exists() {
let personas_dst = output_dir.join("personas");
copy_directory(&personas_src, &personas_dst)?;
println!(" ✓ Copied personas/");
}
let flows_src = blueprint_path.join("flows");
if flows_src.exists() {
let flows_dst = output_dir.join("flows");
copy_directory(&flows_src, &flows_dst)?;
println!(" ✓ Copied flows/");
}
let scenarios_src = blueprint_path.join("scenarios");
if scenarios_src.exists() {
let scenarios_dst = output_dir.join("scenarios");
copy_directory(&scenarios_src, &scenarios_dst)?;
println!(" ✓ Copied scenarios/");
}
let contracts_src = blueprint_path.join("contracts");
if contracts_src.exists() {
let contracts_dst = output_dir.join("contracts");
copy_directory(&contracts_src, &contracts_dst)?;
println!(" ✓ Copied contracts/");
}
let playground_src = blueprint_path.join("playground");
if playground_src.exists() {
let playground_dst = output_dir.join("playground");
copy_directory(&playground_src, &playground_dst)?;
println!(" ✓ Copied playground/");
}
for file in &metadata.files {
if file == "blueprint.yaml" || file == "config.yaml" {
continue; }
let src = blueprint_path.join(file);
if src.exists() {
if src.is_file() {
let dst = output_dir.join(file);
if let Some(parent) = dst.parent() {
fs::create_dir_all(parent)?;
}
fs::copy(&src, &dst)?;
} else if src.is_dir() {
let dst = output_dir.join(file);
copy_directory(&src, &dst)?;
}
}
}
Ok(())
}
fn copy_directory(src: &Path, dst: &Path) -> anyhow::Result<()> {
fs::create_dir_all(dst)?;
for entry in fs::read_dir(src)? {
let entry = entry?;
let path = entry.path();
let Some(file_name) = path.file_name() else {
continue; };
let dst_path = dst.join(file_name);
if path.is_dir() {
copy_directory(&path, &dst_path)?;
} else {
fs::copy(&path, &dst_path)?;
}
}
Ok(())
}
fn generate_project_files(
output_dir: &Path,
name: &str,
metadata: &BlueprintMetadata,
) -> anyhow::Result<()> {
let readme_path = output_dir.join("README.md");
if !readme_path.exists() {
let readme_content = generate_readme(name, metadata);
fs::write(&readme_path, readme_content)?;
println!(" ✓ Created README.md");
}
Ok(())
}
fn generate_readme(name: &str, metadata: &BlueprintMetadata) -> String {
format!(
r#"# {}
{}
This project was created from the **{}** blueprint.
## Quick Start
```bash
# Start the mock server
mockforge serve
# Or with a specific config
mockforge serve --config mockforge.yaml
```
## What's Included
{}
## Documentation
For more information, visit: https://docs.mockforge.dev
"#,
name,
metadata.description,
metadata.title,
if let Some(setup) = &metadata.setup {
let mut sections = Vec::new();
if !setup.personas.is_empty() {
sections.push(format!(
"### Personas\n\nThis blueprint includes {} predefined personas.",
setup.personas.len()
));
}
if !setup.flows.is_empty() {
sections.push(format!(
"### Sample Flows\n\n{} sample flows demonstrating common workflows.",
setup.flows.len()
));
}
if setup.playground.as_ref().map(|p| p.enabled).unwrap_or(false) {
sections.push(
"### Playground Collection\n\nA playground collection for testing endpoints."
.to_string(),
);
}
sections.join("\n\n")
} else {
"This blueprint provides a complete setup for your use case.".to_string()
}
)
}
#[cfg(test)]
mod tests {
use super::*;
fn create_minimal_metadata() -> BlueprintMetadata {
BlueprintMetadata {
manifest_version: "1.0".to_string(),
name: "test-blueprint".to_string(),
version: "1.0.0".to_string(),
title: "Test Blueprint".to_string(),
description: "A test blueprint for testing".to_string(),
author: "Test Author".to_string(),
author_email: None,
category: "testing".to_string(),
tags: vec![],
setup: None,
compatibility: None,
files: vec![],
readme: None,
contracts: vec![],
}
}
fn create_full_metadata() -> BlueprintMetadata {
BlueprintMetadata {
manifest_version: "1.0".to_string(),
name: "full-blueprint".to_string(),
version: "2.0.0".to_string(),
title: "Full Blueprint".to_string(),
description: "A fully configured blueprint".to_string(),
author: "Full Author".to_string(),
author_email: Some("author@example.com".to_string()),
category: "e-commerce".to_string(),
tags: vec!["api".to_string(), "mock".to_string()],
setup: Some(BlueprintSetup {
personas: vec![PersonaInfo {
id: "admin".to_string(),
name: "Admin User".to_string(),
description: Some("Administrator persona".to_string()),
}],
reality: Some(RealityInfo {
level: "standard".to_string(),
description: Some("Standard reality level".to_string()),
}),
flows: vec![FlowInfo {
id: "checkout".to_string(),
name: "Checkout Flow".to_string(),
description: Some("Complete checkout process".to_string()),
}],
scenarios: vec![ScenarioInfo {
id: "happy".to_string(),
name: "Happy Path".to_string(),
r#type: "happy_path".to_string(),
description: Some("Normal flow".to_string()),
file: "happy.yaml".to_string(),
}],
playground: Some(PlaygroundInfo {
enabled: true,
collection_file: "playground.json".to_string(),
}),
}),
compatibility: Some(BlueprintCompatibility {
min_version: "0.3.0".to_string(),
max_version: Some("1.0.0".to_string()),
required_features: vec!["vbr".to_string()],
protocols: vec!["http".to_string(), "grpc".to_string()],
}),
files: vec!["config.yaml".to_string()],
readme: Some("README.md".to_string()),
contracts: vec![ContractInfo {
file: "schema.json".to_string(),
description: Some("Main API schema".to_string()),
}],
}
}
#[test]
fn test_default_true() {
assert!(default_true());
}
#[test]
fn test_default_min_version() {
assert_eq!(default_min_version(), "0.3.0");
}
#[test]
fn test_get_blueprints_dir() {
let dir = get_blueprints_dir();
assert_eq!(dir, PathBuf::from("blueprints"));
}
#[test]
fn test_blueprint_metadata_serialization() {
let metadata = create_minimal_metadata();
let json = serde_json::to_string(&metadata).unwrap();
assert!(json.contains("\"name\":\"test-blueprint\""));
assert!(json.contains("\"version\":\"1.0.0\""));
assert!(json.contains("\"category\":\"testing\""));
}
#[test]
fn test_blueprint_metadata_deserialization() {
let yaml = r#"
manifest_version: "1.0"
name: "test-blueprint"
version: "1.0.0"
title: "Test Blueprint"
description: "A test blueprint"
author: "Test Author"
category: "testing"
"#;
let metadata: BlueprintMetadata = serde_yaml::from_str(yaml).unwrap();
assert_eq!(metadata.name, "test-blueprint");
assert_eq!(metadata.version, "1.0.0");
assert_eq!(metadata.category, "testing");
assert!(metadata.tags.is_empty()); }
#[test]
fn test_blueprint_metadata_full_deserialization() {
let yaml = r#"
manifest_version: "1.0"
name: "full-blueprint"
version: "2.0.0"
title: "Full Blueprint"
description: "Full description"
author: "Author"
author_email: "author@example.com"
category: "e-commerce"
tags:
- api
- mock
setup:
personas:
- id: admin
name: Admin User
description: Administrator
reality:
level: standard
flows:
- id: checkout
name: Checkout Flow
scenarios:
- id: happy
name: Happy Path
type: happy_path
file: happy.yaml
playground:
enabled: true
collection_file: playground.json
compatibility:
min_version: "0.3.0"
max_version: "1.0.0"
required_features:
- vbr
protocols:
- http
files:
- config.yaml
contracts:
- file: schema.json
description: API Schema
"#;
let metadata: BlueprintMetadata = serde_yaml::from_str(yaml).unwrap();
assert_eq!(metadata.name, "full-blueprint");
assert_eq!(metadata.author_email, Some("author@example.com".to_string()));
assert_eq!(metadata.tags.len(), 2);
let setup = metadata.setup.unwrap();
assert_eq!(setup.personas.len(), 1);
assert_eq!(setup.personas[0].id, "admin");
assert!(setup.reality.is_some());
assert_eq!(setup.flows.len(), 1);
assert_eq!(setup.scenarios.len(), 1);
assert!(setup.playground.is_some());
let compat = metadata.compatibility.unwrap();
assert_eq!(compat.min_version, "0.3.0");
assert_eq!(compat.max_version, Some("1.0.0".to_string()));
}
#[test]
fn test_persona_info_serialization() {
let persona = PersonaInfo {
id: "user".to_string(),
name: "Regular User".to_string(),
description: Some("A regular user persona".to_string()),
};
let json = serde_json::to_string(&persona).unwrap();
assert!(json.contains("\"id\":\"user\""));
assert!(json.contains("\"name\":\"Regular User\""));
assert!(json.contains("\"description\":\"A regular user persona\""));
}
#[test]
fn test_persona_info_clone() {
let persona = PersonaInfo {
id: "user".to_string(),
name: "Regular User".to_string(),
description: None,
};
let cloned = persona.clone();
assert_eq!(persona.id, cloned.id);
assert_eq!(persona.name, cloned.name);
}
#[test]
fn test_reality_info() {
let reality = RealityInfo {
level: "high".to_string(),
description: Some("High fidelity mode".to_string()),
};
let json = serde_json::to_string(&reality).unwrap();
let parsed: RealityInfo = serde_json::from_str(&json).unwrap();
assert_eq!(parsed.level, "high");
assert_eq!(parsed.description, Some("High fidelity mode".to_string()));
}
#[test]
fn test_flow_info() {
let flow = FlowInfo {
id: "login".to_string(),
name: "Login Flow".to_string(),
description: None,
};
let json = serde_json::to_string(&flow).unwrap();
assert!(json.contains("\"id\":\"login\""));
assert!(json.contains("\"name\":\"Login Flow\""));
}
#[test]
fn test_scenario_info() {
let scenario = ScenarioInfo {
id: "error".to_string(),
name: "Error Scenario".to_string(),
r#type: "known_failure".to_string(),
description: Some("Tests error handling".to_string()),
file: "error_scenario.yaml".to_string(),
};
let json = serde_json::to_string(&scenario).unwrap();
assert!(json.contains("\"id\":\"error\""));
assert!(json.contains("\"type\":\"known_failure\""));
assert!(json.contains("\"file\":\"error_scenario.yaml\""));
}
#[test]
fn test_contract_info() {
let contract = ContractInfo {
file: "openapi.yaml".to_string(),
description: Some("OpenAPI specification".to_string()),
};
let json = serde_json::to_string(&contract).unwrap();
let parsed: ContractInfo = serde_json::from_str(&json).unwrap();
assert_eq!(parsed.file, "openapi.yaml");
assert_eq!(parsed.description, Some("OpenAPI specification".to_string()));
}
#[test]
fn test_playground_info_with_default() {
let yaml = r#"collection_file: "test.json""#;
let playground: PlaygroundInfo = serde_yaml::from_str(yaml).unwrap();
assert!(playground.enabled); assert_eq!(playground.collection_file, "test.json");
}
#[test]
fn test_playground_info_explicit_disabled() {
let yaml = r#"
enabled: false
collection_file: "test.json"
"#;
let playground: PlaygroundInfo = serde_yaml::from_str(yaml).unwrap();
assert!(!playground.enabled);
}
#[test]
fn test_blueprint_compatibility_defaults() {
let yaml = r#"{}"#;
let compat: BlueprintCompatibility = serde_yaml::from_str(yaml).unwrap();
assert_eq!(compat.min_version, "0.3.0"); assert!(compat.max_version.is_none());
assert!(compat.required_features.is_empty());
assert!(compat.protocols.is_empty());
}
#[test]
fn test_blueprint_setup_default() {
let yaml = r#"{}"#;
let setup: BlueprintSetup = serde_yaml::from_str(yaml).unwrap();
assert!(setup.personas.is_empty());
assert!(setup.reality.is_none());
assert!(setup.flows.is_empty());
assert!(setup.scenarios.is_empty());
assert!(setup.playground.is_none());
}
#[test]
fn test_generate_readme_minimal() {
let metadata = create_minimal_metadata();
let readme = generate_readme("MyProject", &metadata);
assert!(readme.contains("# MyProject"));
assert!(readme.contains("A test blueprint for testing"));
assert!(readme.contains("**Test Blueprint** blueprint"));
assert!(readme.contains("mockforge serve"));
}
#[test]
fn test_generate_readme_with_setup() {
let metadata = create_full_metadata();
let readme = generate_readme("FullProject", &metadata);
assert!(readme.contains("# FullProject"));
assert!(readme.contains("### Personas"));
assert!(readme.contains("1 predefined personas"));
assert!(readme.contains("### Sample Flows"));
assert!(readme.contains("### Playground Collection"));
}
#[test]
fn test_generate_readme_without_playground() {
let mut metadata = create_full_metadata();
if let Some(ref mut setup) = metadata.setup {
setup.playground = None;
}
let readme = generate_readme("TestProject", &metadata);
assert!(readme.contains("### Personas"));
assert!(!readme.contains("### Playground Collection"));
}
#[test]
fn test_blueprint_metadata_debug() {
let metadata = create_minimal_metadata();
let debug_str = format!("{:?}", metadata);
assert!(debug_str.contains("BlueprintMetadata"));
assert!(debug_str.contains("test-blueprint"));
}
#[test]
fn test_blueprint_commands_enum_variants() {
let _list = BlueprintCommands::List {
detailed: true,
category: Some("testing".to_string()),
};
let _create = BlueprintCommands::Create {
name: "test".to_string(),
blueprint: "test-blueprint".to_string(),
output: Some(PathBuf::from("output")),
force: true,
};
let _info = BlueprintCommands::Info {
blueprint_id: "test".to_string(),
};
}
#[test]
fn test_metadata_round_trip_yaml() {
let original = create_full_metadata();
let yaml = serde_yaml::to_string(&original).unwrap();
let parsed: BlueprintMetadata = serde_yaml::from_str(&yaml).unwrap();
assert_eq!(original.name, parsed.name);
assert_eq!(original.version, parsed.version);
assert_eq!(original.author_email, parsed.author_email);
assert_eq!(original.tags, parsed.tags);
}
#[test]
fn test_metadata_round_trip_json() {
let original = create_minimal_metadata();
let json = serde_json::to_string(&original).unwrap();
let parsed: BlueprintMetadata = serde_json::from_str(&json).unwrap();
assert_eq!(original.name, parsed.name);
assert_eq!(original.version, parsed.version);
assert_eq!(original.category, parsed.category);
}
#[test]
fn test_all_structs_implement_clone() {
let metadata = create_full_metadata();
let cloned = metadata.clone();
assert_eq!(metadata.name, cloned.name);
let setup = metadata.setup.clone().unwrap();
let setup_cloned = setup.clone();
assert_eq!(setup.personas.len(), setup_cloned.personas.len());
if let Some(compat) = metadata.compatibility.clone() {
let compat_cloned = compat.clone();
assert_eq!(compat.min_version, compat_cloned.min_version);
}
}
#[test]
fn test_scenario_types() {
let types = ["happy_path", "known_failure", "slow_path", "edge_case"];
for scenario_type in types {
let scenario = ScenarioInfo {
id: "test".to_string(),
name: "Test Scenario".to_string(),
r#type: scenario_type.to_string(),
description: None,
file: "test.yaml".to_string(),
};
let json = serde_json::to_string(&scenario).unwrap();
assert!(json.contains(&format!("\"type\":\"{}\"", scenario_type)));
}
}
#[test]
fn test_metadata_with_empty_optional_arrays() {
let yaml = r#"
manifest_version: "1.0"
name: "minimal"
version: "1.0.0"
title: "Minimal"
description: "Minimal blueprint"
author: "Author"
category: "test"
tags: []
files: []
contracts: []
"#;
let metadata: BlueprintMetadata = serde_yaml::from_str(yaml).unwrap();
assert!(metadata.tags.is_empty());
assert!(metadata.files.is_empty());
assert!(metadata.contracts.is_empty());
}
}