use std::error::Error;
use std::path::PathBuf;
use std::process::ExitCode;
use clap::{Args, Subcommand};
use sara_core::service::{InitError, InitOptions, InitResult, InitService, TypeConfig};
use sara_core::config::{Config, OutputConfig};
use super::interactive::{
InteractiveSession, PrefilledFields, PromptError, handle_interactive_result,
run_interactive_session,
};
use crate::output::{print_error, print_success, print_warning};
#[derive(Args, Debug, Clone)]
pub struct CommonOptions {
pub file: PathBuf,
#[arg(long)]
pub id: Option<String>,
#[arg(long)]
pub name: Option<String>,
#[arg(short = 'd', long)]
pub description: Option<String>,
#[arg(long)]
pub force: bool,
}
#[derive(Args, Debug)]
#[command(verbatim_doc_comment)]
pub struct InitArgs {
#[command(subcommand)]
pub command: Option<InitSubcommand>,
}
#[derive(Subcommand, Debug)]
pub enum InitSubcommand {
#[command(name = "architecture-decision-record", visible_alias = "adr")]
Adr(AdrArgs),
#[command(name = "solution", visible_alias = "sol")]
Solution(SolutionArgs),
#[command(name = "use-case", visible_alias = "uc")]
UseCase(UseCaseArgs),
#[command(name = "scenario", visible_alias = "scen")]
Scenario(ScenarioArgs),
#[command(name = "system-requirement", visible_alias = "sysreq")]
SystemRequirement(SystemRequirementArgs),
#[command(name = "system-architecture", visible_alias = "sysarch")]
SystemArchitecture(SystemArchitectureArgs),
#[command(name = "software-requirement", visible_alias = "swreq")]
SoftwareRequirement(SoftwareRequirementArgs),
#[command(name = "hardware-requirement", visible_alias = "hwreq")]
HardwareRequirement(HardwareRequirementArgs),
#[command(name = "software-detailed-design", visible_alias = "swdd")]
SoftwareDetailedDesign(SoftwareDetailedDesignArgs),
#[command(name = "hardware-detailed-design", visible_alias = "hwdd")]
HardwareDetailedDesign(HardwareDetailedDesignArgs),
}
#[derive(Args, Debug)]
pub struct AdrArgs {
#[command(flatten)]
pub common: CommonOptions,
#[arg(long, short = 's')]
pub status: Option<String>,
#[arg(long, num_args = 1..)]
pub deciders: Vec<String>,
#[arg(long, short = 'j', num_args = 1..)]
pub justifies: Vec<String>,
#[arg(long, num_args = 1..)]
pub supersedes: Vec<String>,
}
#[derive(Args, Debug)]
pub struct SolutionArgs {
#[command(flatten)]
pub common: CommonOptions,
}
#[derive(Args, Debug)]
pub struct UseCaseArgs {
#[command(flatten)]
pub common: CommonOptions,
#[arg(long, num_args = 1..)]
pub refines: Vec<String>,
}
#[derive(Args, Debug)]
pub struct ScenarioArgs {
#[command(flatten)]
pub common: CommonOptions,
#[arg(long, num_args = 1..)]
pub refines: Vec<String>,
}
#[derive(Args, Debug)]
pub struct SystemRequirementArgs {
#[command(flatten)]
pub common: CommonOptions,
#[arg(long)]
pub specification: Option<String>,
#[arg(long, num_args = 1..)]
pub derives_from: Vec<String>,
#[arg(long, num_args = 1..)]
pub depends_on: Vec<String>,
}
#[derive(Args, Debug)]
pub struct SystemArchitectureArgs {
#[command(flatten)]
pub common: CommonOptions,
#[arg(long)]
pub platform: Option<String>,
#[arg(long, num_args = 1..)]
pub satisfies: Vec<String>,
}
#[derive(Args, Debug)]
pub struct SoftwareRequirementArgs {
#[command(flatten)]
pub common: CommonOptions,
#[arg(long)]
pub specification: Option<String>,
#[arg(long, num_args = 1..)]
pub derives_from: Vec<String>,
#[arg(long, num_args = 1..)]
pub depends_on: Vec<String>,
}
#[derive(Args, Debug)]
pub struct HardwareRequirementArgs {
#[command(flatten)]
pub common: CommonOptions,
#[arg(long)]
pub specification: Option<String>,
#[arg(long, num_args = 1..)]
pub derives_from: Vec<String>,
#[arg(long, num_args = 1..)]
pub depends_on: Vec<String>,
}
#[derive(Args, Debug)]
pub struct SoftwareDetailedDesignArgs {
#[command(flatten)]
pub common: CommonOptions,
#[arg(long, num_args = 1..)]
pub satisfies: Vec<String>,
}
#[derive(Args, Debug)]
pub struct HardwareDetailedDesignArgs {
#[command(flatten)]
pub common: CommonOptions,
#[arg(long, num_args = 1..)]
pub satisfies: Vec<String>,
}
const EXIT_FRONTMATTER_EXISTS: u8 = 2;
const EXIT_INVALID_OPTION: u8 = 3;
pub fn run(args: &InitArgs, config: &Config) -> Result<ExitCode, Box<dyn Error>> {
match &args.command {
None => run_interactive(config),
Some(subcommand) => run_subcommand(subcommand, config),
}
}
fn run_interactive(config: &Config) -> Result<ExitCode, Box<dyn Error>> {
let prefilled = PrefilledFields {
file: None,
item_type: None,
id: None,
name: None,
description: None,
refines: Vec::new(),
derives_from: Vec::new(),
satisfies: Vec::new(),
depends_on: Vec::new(),
specification: None,
platform: None,
deciders: Vec::new(),
justifies: Vec::new(),
};
let mut session = InteractiveSession {
graph: None,
prefilled,
repositories: &config.repositories.paths,
output: &config.output,
};
let result = run_interactive_session(&mut session);
match handle_interactive_result(result, &config.output) {
Ok(Some(input)) => {
let type_config = build_type_config_from_interactive(&input);
let opts = InitOptions::new(input.file, type_config)
.with_id(input.id)
.with_name(input.name)
.maybe_description(input.description)
.with_force(false);
run_with_options(opts, config)
}
Ok(None) => Ok(ExitCode::from(130)),
Err(PromptError::NonInteractiveTerminal) => Ok(ExitCode::from(1)),
Err(PromptError::MissingParent(_)) => Ok(ExitCode::from(1)),
Err(_) => Ok(ExitCode::from(1)),
}
}
fn build_type_config_from_interactive(input: &super::interactive::InteractiveInput) -> TypeConfig {
use sara_core::model::ItemType;
use super::interactive::TypeSpecificInput;
match (&input.item_type, &input.type_specific) {
(ItemType::Solution, _) => TypeConfig::Solution,
(ItemType::UseCase, _) => TypeConfig::UseCase {
refines: input.traceability.refines.clone(),
},
(ItemType::Scenario, _) => TypeConfig::Scenario {
refines: input.traceability.refines.clone(),
},
(ItemType::SystemRequirement, TypeSpecificInput::Requirement { specification }) => {
TypeConfig::SystemRequirement {
specification: specification.clone(),
derives_from: input.traceability.derives_from.clone(),
depends_on: input.traceability.depends_on.clone(),
}
}
(ItemType::SystemArchitecture, TypeSpecificInput::SystemArchitecture { platform }) => {
TypeConfig::SystemArchitecture {
platform: platform.clone(),
satisfies: input.traceability.satisfies.clone(),
}
}
(ItemType::SoftwareRequirement, TypeSpecificInput::Requirement { specification }) => {
TypeConfig::SoftwareRequirement {
specification: specification.clone(),
derives_from: input.traceability.derives_from.clone(),
depends_on: input.traceability.depends_on.clone(),
}
}
(ItemType::HardwareRequirement, TypeSpecificInput::Requirement { specification }) => {
TypeConfig::HardwareRequirement {
specification: specification.clone(),
derives_from: input.traceability.derives_from.clone(),
depends_on: input.traceability.depends_on.clone(),
}
}
(ItemType::SoftwareDetailedDesign, _) => TypeConfig::SoftwareDetailedDesign {
satisfies: input.traceability.satisfies.clone(),
},
(ItemType::HardwareDetailedDesign, _) => TypeConfig::HardwareDetailedDesign {
satisfies: input.traceability.satisfies.clone(),
},
(ItemType::ArchitectureDecisionRecord, TypeSpecificInput::Adr { deciders }) => {
TypeConfig::Adr {
status: None,
deciders: deciders.clone(),
justifies: input.traceability.justifies.clone(),
supersedes: Vec::new(),
superseded_by: None,
}
}
(ItemType::SystemRequirement, _) => TypeConfig::SystemRequirement {
specification: None,
derives_from: input.traceability.derives_from.clone(),
depends_on: input.traceability.depends_on.clone(),
},
(ItemType::SoftwareRequirement, _) => TypeConfig::SoftwareRequirement {
specification: None,
derives_from: input.traceability.derives_from.clone(),
depends_on: input.traceability.depends_on.clone(),
},
(ItemType::HardwareRequirement, _) => TypeConfig::HardwareRequirement {
specification: None,
derives_from: input.traceability.derives_from.clone(),
depends_on: input.traceability.depends_on.clone(),
},
(ItemType::SystemArchitecture, _) => TypeConfig::SystemArchitecture {
platform: None,
satisfies: input.traceability.satisfies.clone(),
},
(ItemType::ArchitectureDecisionRecord, _) => TypeConfig::Adr {
status: None,
deciders: Vec::new(),
justifies: input.traceability.justifies.clone(),
supersedes: Vec::new(),
superseded_by: None,
},
}
}
fn run_subcommand(
subcommand: &InitSubcommand,
config: &Config,
) -> Result<ExitCode, Box<dyn Error>> {
let opts = match subcommand {
InitSubcommand::Adr(args) => {
let type_config = TypeConfig::Adr {
status: args.status.clone(),
deciders: args.deciders.clone(),
justifies: args.justifies.clone(),
supersedes: args.supersedes.clone(),
superseded_by: None,
};
InitOptions::new(args.common.file.clone(), type_config)
.maybe_id(args.common.id.clone())
.maybe_name(args.common.name.clone())
.maybe_description(args.common.description.clone())
.with_force(args.common.force)
}
InitSubcommand::Solution(args) => {
InitOptions::new(args.common.file.clone(), TypeConfig::Solution)
.maybe_id(args.common.id.clone())
.maybe_name(args.common.name.clone())
.maybe_description(args.common.description.clone())
.with_force(args.common.force)
}
InitSubcommand::UseCase(args) => {
let type_config = TypeConfig::UseCase {
refines: args.refines.clone(),
};
InitOptions::new(args.common.file.clone(), type_config)
.maybe_id(args.common.id.clone())
.maybe_name(args.common.name.clone())
.maybe_description(args.common.description.clone())
.with_force(args.common.force)
}
InitSubcommand::Scenario(args) => {
let type_config = TypeConfig::Scenario {
refines: args.refines.clone(),
};
InitOptions::new(args.common.file.clone(), type_config)
.maybe_id(args.common.id.clone())
.maybe_name(args.common.name.clone())
.maybe_description(args.common.description.clone())
.with_force(args.common.force)
}
InitSubcommand::SystemRequirement(args) => {
let type_config = TypeConfig::SystemRequirement {
specification: args.specification.clone(),
derives_from: args.derives_from.clone(),
depends_on: args.depends_on.clone(),
};
InitOptions::new(args.common.file.clone(), type_config)
.maybe_id(args.common.id.clone())
.maybe_name(args.common.name.clone())
.maybe_description(args.common.description.clone())
.with_force(args.common.force)
}
InitSubcommand::SystemArchitecture(args) => {
let type_config = TypeConfig::SystemArchitecture {
platform: args.platform.clone(),
satisfies: args.satisfies.clone(),
};
InitOptions::new(args.common.file.clone(), type_config)
.maybe_id(args.common.id.clone())
.maybe_name(args.common.name.clone())
.maybe_description(args.common.description.clone())
.with_force(args.common.force)
}
InitSubcommand::SoftwareRequirement(args) => {
let type_config = TypeConfig::SoftwareRequirement {
specification: args.specification.clone(),
derives_from: args.derives_from.clone(),
depends_on: args.depends_on.clone(),
};
InitOptions::new(args.common.file.clone(), type_config)
.maybe_id(args.common.id.clone())
.maybe_name(args.common.name.clone())
.maybe_description(args.common.description.clone())
.with_force(args.common.force)
}
InitSubcommand::HardwareRequirement(args) => {
let type_config = TypeConfig::HardwareRequirement {
specification: args.specification.clone(),
derives_from: args.derives_from.clone(),
depends_on: args.depends_on.clone(),
};
InitOptions::new(args.common.file.clone(), type_config)
.maybe_id(args.common.id.clone())
.maybe_name(args.common.name.clone())
.maybe_description(args.common.description.clone())
.with_force(args.common.force)
}
InitSubcommand::SoftwareDetailedDesign(args) => {
let type_config = TypeConfig::SoftwareDetailedDesign {
satisfies: args.satisfies.clone(),
};
InitOptions::new(args.common.file.clone(), type_config)
.maybe_id(args.common.id.clone())
.maybe_name(args.common.name.clone())
.maybe_description(args.common.description.clone())
.with_force(args.common.force)
}
InitSubcommand::HardwareDetailedDesign(args) => {
let type_config = TypeConfig::HardwareDetailedDesign {
satisfies: args.satisfies.clone(),
};
InitOptions::new(args.common.file.clone(), type_config)
.maybe_id(args.common.id.clone())
.maybe_name(args.common.name.clone())
.maybe_description(args.common.description.clone())
.with_force(args.common.force)
}
};
run_with_options(opts, config)
}
fn run_with_options(opts: InitOptions, config: &Config) -> Result<ExitCode, Box<dyn Error>> {
let output = &config.output;
let service = InitService::new();
match service.init(&opts) {
Ok(result) => {
print_result(output, &result);
Ok(ExitCode::SUCCESS)
}
Err(InitError::FrontmatterExists(path)) => {
print_error(
output,
&format!(
"File {} already has frontmatter. Use --force to overwrite.",
path.display()
),
);
Ok(ExitCode::from(EXIT_FRONTMATTER_EXISTS))
}
Err(InitError::InvalidOption(msg)) => {
print_error(output, &msg);
Ok(ExitCode::from(EXIT_INVALID_OPTION))
}
Err(InitError::Io(e)) => {
print_error(output, &format!("IO error: {}", e));
Ok(ExitCode::FAILURE)
}
}
}
fn print_result(config: &OutputConfig, result: &InitResult) {
if result.updated_existing {
if result.replaced_frontmatter {
print_success(
config,
&format!("Replaced frontmatter in {}", result.file.display()),
);
} else {
print_success(
config,
&format!("Added frontmatter to {}", result.file.display()),
);
}
} else {
print_success(
config,
&format!(
"Created {} with {} template",
result.file.display(),
result.item_type.display_name()
),
);
}
print_item_info(config, result);
if result.needs_specification {
print_warning(config, "Don't forget to update the specification field!");
}
}
fn print_item_info(_config: &OutputConfig, result: &InitResult) {
let output = format!(
"\n ID: {}\n Name: {}\n Type: {}",
result.id,
result.name,
result.item_type.display_name()
);
println!("{}", output);
}
#[cfg(test)]
mod tests {
use clap::CommandFactory;
use super::*;
impl CommandFactory for InitArgs {
fn command() -> clap::Command {
<Self as clap::Args>::augment_args(clap::Command::new("init"))
}
fn command_for_update() -> clap::Command {
<Self as clap::Args>::augment_args_for_update(clap::Command::new("init"))
}
}
#[test]
fn test_init_subcommands_exist() {
let cmd = InitArgs::command();
let subcommands: Vec<&clap::Command> = cmd.get_subcommands().collect();
assert!(!subcommands.is_empty(), "Init should have subcommands");
let names: Vec<&str> = subcommands.iter().map(|s| s.get_name()).collect();
assert!(names.contains(&"architecture-decision-record"));
assert!(names.contains(&"solution"));
assert!(names.contains(&"use-case"));
assert!(names.contains(&"scenario"));
assert!(names.contains(&"system-requirement"));
assert!(names.contains(&"system-architecture"));
assert!(names.contains(&"software-requirement"));
assert!(names.contains(&"hardware-requirement"));
assert!(names.contains(&"software-detailed-design"));
assert!(names.contains(&"hardware-detailed-design"));
}
#[test]
fn test_aliases_exist() {
let cmd = InitArgs::command();
for sub in cmd.get_subcommands() {
let aliases: Vec<&str> = sub.get_visible_aliases().collect();
match sub.get_name() {
"architecture-decision-record" => assert!(aliases.contains(&"adr")),
"solution" => assert!(aliases.contains(&"sol")),
"use-case" => assert!(aliases.contains(&"uc")),
"scenario" => assert!(aliases.contains(&"scen")),
"system-requirement" => assert!(aliases.contains(&"sysreq")),
"system-architecture" => assert!(aliases.contains(&"sysarch")),
"software-requirement" => assert!(aliases.contains(&"swreq")),
"hardware-requirement" => assert!(aliases.contains(&"hwreq")),
"software-detailed-design" => assert!(aliases.contains(&"swdd")),
"hardware-detailed-design" => assert!(aliases.contains(&"hwdd")),
_ => {}
}
}
}
}