use crate::{
config::{load_global_config, load_repo_config, DependencyConfig},
constants::APICURIO_CONFIG,
identifier::Identifier,
registry::RegistryClient,
};
use anyhow::{anyhow, Result};
use convert_case::{Case, Casing};
use dialoguer::Input;
use std::{fs, path::PathBuf};
pub async fn run(identifier_str: Option<String>) -> Result<()> {
let mut identifier = if let Some(id_str) = identifier_str {
Identifier::parse(&id_str)
} else {
Identifier::parse("")
};
let repo_path = PathBuf::from(APICURIO_CONFIG);
let mut repo = load_repo_config(&repo_path)?;
let global = load_global_config()?;
let regs = repo.merge_registries(global)?;
if regs.is_empty() {
return Err(anyhow!(
"No registries configured. Please add a registry first using 'apicurio registry add'."
));
}
let registry_names: Vec<String> = regs.iter().map(|r| r.name.clone()).collect();
let registry_client = if let Some(registry_name) = &identifier.registry {
let registry_config = regs
.iter()
.find(|r| &r.name == registry_name)
.ok_or_else(|| anyhow!("Registry '{}' not found", registry_name))?;
Some(RegistryClient::new(registry_config)?)
} else if regs.len() == 1 {
Some(RegistryClient::new(®s[0])?)
} else {
None
};
identifier
.complete_interactive(
®istry_names,
&repo.dependencies,
registry_client.as_ref(),
)
.await?;
let registry_client = if let Some(client) = registry_client {
client
} else {
let registry_name = identifier.registry.as_ref().unwrap();
let registry_config = regs
.iter()
.find(|r| &r.name == registry_name)
.ok_or_else(|| anyhow!("Registry '{}' not found", registry_name))?;
RegistryClient::new(registry_config)?
};
if !identifier.is_complete() {
identifier
.complete_version_with_registry(®istry_client)
.await?;
}
if !identifier.is_complete() {
return Err(anyhow!("Failed to complete dependency identifier"));
}
let artifact_exists = identifier
.validate_artifact_exists(®istry_client)
.await?;
if !artifact_exists {
return Err(anyhow!(
"Artifact '{}/{}' does not exist in registry '{}'",
identifier.group_id.as_ref().unwrap(),
identifier.artifact_id.as_ref().unwrap(),
identifier.registry.as_ref().unwrap()
));
}
let artifact_metadata = registry_client
.get_artifact_metadata(
identifier.group_id.as_ref().unwrap(),
identifier.artifact_id.as_ref().unwrap(),
)
.await?;
let default_output_path =
generate_default_output_path(&identifier, &artifact_metadata.artifact_type);
let output_path = Input::new()
.with_prompt("Output path")
.default(default_output_path)
.interact_text()?;
let dep_name = format!(
"{}/{}",
identifier.group_id.as_ref().unwrap(),
identifier.artifact_id.as_ref().unwrap()
);
let existing_index = repo.dependencies.iter().position(|d| d.name == dep_name);
let new_dependency = DependencyConfig {
name: dep_name.clone(),
group_id: identifier.group_id.unwrap(),
artifact_id: identifier.artifact_id.unwrap(),
version: identifier.version.unwrap(),
registry: identifier.registry.unwrap(),
output_path,
};
if let Some(index) = existing_index {
repo.dependencies[index] = new_dependency;
println!("🔄 Replaced existing dependency: {dep_name}");
} else {
repo.dependencies.push(new_dependency);
println!("✅ Added dependency: {dep_name}");
}
let serialized = serde_yaml::to_string(&repo)?;
fs::write(repo_path, serialized)?;
crate::commands::pull::run().await?;
Ok(())
}
fn generate_default_output_path(identifier: &Identifier, artifact_type: &str) -> String {
let artifact_id = identifier.artifact_id.as_ref().unwrap();
match artifact_type.to_uppercase().as_str() {
"PROTOBUF" => {
let mut parts = artifact_id
.split('.')
.map(str::to_string)
.collect::<Vec<_>>();
let last = parts.pop().unwrap().to_case(Case::Snake);
let mut path = PathBuf::from("protos");
for seg in parts {
path.push(seg.to_lowercase());
}
path.push(last);
path.set_extension("proto");
path.to_string_lossy().into_owned()
}
"AVRO" => {
let mut parts = artifact_id
.split('.')
.map(str::to_string)
.collect::<Vec<_>>();
let last = parts.pop().unwrap().to_case(Case::Snake);
let mut path = PathBuf::from("schemas");
for seg in parts {
path.push(seg.to_lowercase());
}
path.push(last);
path.set_extension("avsc");
path.to_string_lossy().into_owned()
}
"JSON" => {
let mut parts = artifact_id
.split('.')
.map(str::to_string)
.collect::<Vec<_>>();
let last = parts.pop().unwrap().to_case(Case::Snake);
let mut path = PathBuf::from("schemas");
for seg in parts {
path.push(seg.to_lowercase());
}
path.push(last);
path.set_extension("json");
path.to_string_lossy().into_owned()
}
"OPENAPI" => {
let mut parts = artifact_id
.split('.')
.map(str::to_string)
.collect::<Vec<_>>();
let last = parts.pop().unwrap().to_case(Case::Snake);
let mut path = PathBuf::from("openapi");
for seg in parts {
path.push(seg.to_lowercase());
}
path.push(last);
path.set_extension("yaml");
path.to_string_lossy().into_owned()
}
_ => {
let mut parts = artifact_id
.split('.')
.map(str::to_string)
.collect::<Vec<_>>();
let last = parts.pop().unwrap().to_case(Case::Snake);
let mut path = PathBuf::from("schemas");
for seg in parts {
path.push(seg.to_lowercase());
}
path.push(last);
path.set_extension("schema");
path.to_string_lossy().into_owned()
}
}
}