use crate::cli::{FlattenCommand, FlattenFormat};
use crate::core::api::ApiError;
use crate::core::external_refs::flatten_spec;
use crate::core::parser::{parse_openapi_v3, OpenAPIDocument, ReferenceOr, SpecResolver};
use crate::utils::security::{validate_file_path, validate_output_path};
use anyhow::{Context, Result};
use colored::*;
use serde_json::Value;
use std::fs;
use std::path::Path;
pub async fn flatten_command(cmd: FlattenCommand) -> Result<()> {
println!("🔧 {} Flatten", "MicroRapid".bright_cyan());
println!(
"📄 Loading spec from: {}",
cmd.spec.display().to_string().cyan()
);
validate_file_path(&cmd.spec).context("Invalid input spec path")?;
let content = fs::read_to_string(&cmd.spec)
.map_err(|e| ApiError::ValidationError(format!("Cannot read spec file: {}", e)))?;
let raw_value: serde_yaml::Value =
serde_yaml::from_str(&content).context("Failed to parse spec as YAML")?;
let mut json_value: Value =
serde_json::to_value(&raw_value).context("Failed to convert YAML to JSON")?;
if cmd.resolve_external {
println!("🔍 Resolving external references...");
let base_path = cmd.spec.parent().unwrap_or(Path::new("."));
flatten_spec(&mut json_value, base_path, cmd.allow_insecure).await?;
if !cmd.include_unused {
if let Some(obj) = json_value.as_object_mut() {
obj.remove("components");
obj.remove("definitions"); }
}
} else {
let is_openapi = json_value.get("openapi").is_some();
let components = if is_openapi {
let _openapi_result = parse_openapi_v3(&content)?;
let openapi: OpenAPIDocument = serde_json::from_value(json_value.clone())
.or_else(|_| serde_yaml::from_str(&content))
.context("Failed to parse as OpenAPI document")?;
openapi.components
} else {
None
};
let mut resolver = SpecResolver::new(components);
flatten_value(&mut json_value, &mut resolver, &mut Vec::new())?;
if !cmd.include_unused {
if let Some(obj) = json_value.as_object_mut() {
obj.remove("components");
}
}
}
let output_content = match cmd.format {
FlattenFormat::Yaml => {
serde_yaml::to_string(&json_value).context("Failed to serialize to YAML")?
}
FlattenFormat::Json => {
serde_json::to_string_pretty(&json_value).context("Failed to serialize to JSON")?
}
};
if let Some(output_path) = cmd.output {
validate_output_path(&output_path).context("Invalid output path")?;
fs::write(&output_path, output_content)?;
println!(
"✅ Flattened spec written to: {}",
output_path.display().to_string().green()
);
} else {
println!("{}", output_content);
}
Ok(())
}
fn flatten_value(
value: &mut Value,
resolver: &mut SpecResolver,
path: &mut Vec<String>,
) -> Result<()> {
match value {
Value::Object(map) => {
if let Some(Value::String(ref_str)) = map.get("$ref") {
let ref_str = ref_str.clone();
let resolved = resolve_reference(&ref_str, resolver)?;
*value = resolved;
flatten_value(value, resolver, path)?;
} else {
for (key, val) in map.iter_mut() {
path.push(key.clone());
flatten_value(val, resolver, path)?;
path.pop();
}
}
}
Value::Array(arr) => {
for (i, val) in arr.iter_mut().enumerate() {
path.push(format!("[{}]", i));
flatten_value(val, resolver, path)?;
path.pop();
}
}
_ => {
}
}
Ok(())
}
fn resolve_reference(reference: &str, resolver: &mut SpecResolver) -> Result<Value> {
if let Some(path) = reference.strip_prefix("#/components/") {
let parts: Vec<&str> = path.split('/').collect();
if parts.len() != 2 {
return Err(ApiError::ValidationError(format!(
"Invalid reference format: {}",
reference
))
.into());
}
let component_type = parts[0];
let _component_name = parts[1];
match component_type {
"parameters" => {
let param_ref = ReferenceOr::Reference {
reference: reference.to_string(),
};
let param = resolver.resolve_parameter(¶m_ref)?;
serde_json::to_value(param).context("Failed to serialize parameter")
}
"schemas" => {
let schema_ref = ReferenceOr::Reference {
reference: reference.to_string(),
};
let schema = resolver.resolve_schema(&schema_ref)?;
serde_json::to_value(schema).context("Failed to serialize schema")
}
"responses" => {
let response_ref = ReferenceOr::Reference {
reference: reference.to_string(),
};
let response = resolver.resolve_response(&response_ref)?;
serde_json::to_value(response).context("Failed to serialize response")
}
"requestBodies" => {
let rb_ref = ReferenceOr::Reference {
reference: reference.to_string(),
};
let request_body = resolver.resolve_request_body(&rb_ref)?;
serde_json::to_value(request_body).context("Failed to serialize request body")
}
_ => Err(ApiError::ValidationError(format!(
"Unsupported component type: {}",
component_type
))
.into()),
}
} else {
Err(ApiError::ValidationError(
"Only local references (#/components/...) are currently supported".to_string(),
)
.into())
}
}