use serde::{Deserialize, Serialize};
use std::fmt;
use crate::error::SaraError;
use crate::model::FieldName;
use crate::model::relationship::{Relationship, RelationshipType};
use super::adr::AdrStatus;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum ItemType {
Solution,
UseCase,
Scenario,
SystemRequirement,
SystemArchitecture,
HardwareRequirement,
SoftwareRequirement,
HardwareDetailedDesign,
SoftwareDetailedDesign,
ArchitectureDecisionRecord,
}
impl ItemType {
#[must_use]
pub const fn all() -> &'static [ItemType] {
&[
Self::Solution,
Self::UseCase,
Self::Scenario,
Self::SystemRequirement,
Self::SystemArchitecture,
Self::HardwareRequirement,
Self::SoftwareRequirement,
Self::HardwareDetailedDesign,
Self::SoftwareDetailedDesign,
Self::ArchitectureDecisionRecord,
]
}
#[must_use]
pub const fn display_name(&self) -> &'static str {
match self {
Self::Solution => "Solution",
Self::UseCase => "Use Case",
Self::Scenario => "Scenario",
Self::SystemRequirement => "System Requirement",
Self::SystemArchitecture => "System Architecture",
Self::HardwareRequirement => "Hardware Requirement",
Self::SoftwareRequirement => "Software Requirement",
Self::HardwareDetailedDesign => "Hardware Detailed Design",
Self::SoftwareDetailedDesign => "Software Detailed Design",
Self::ArchitectureDecisionRecord => "Architecture Decision Record",
}
}
#[must_use]
pub const fn prefix(&self) -> &'static str {
match self {
Self::Solution => "SOL",
Self::UseCase => "UC",
Self::Scenario => "SCEN",
Self::SystemRequirement => "SYSREQ",
Self::SystemArchitecture => "SYSARCH",
Self::HardwareRequirement => "HWREQ",
Self::SoftwareRequirement => "SWREQ",
Self::HardwareDetailedDesign => "HWDD",
Self::SoftwareDetailedDesign => "SWDD",
Self::ArchitectureDecisionRecord => "ADR",
}
}
#[must_use]
pub fn generate_id(&self, sequence: Option<u32>) -> String {
let num = sequence.unwrap_or(1);
format!("{}-{:03}", self.prefix(), num)
}
#[must_use]
pub fn suggest_next_id(&self, graph: Option<&crate::graph::KnowledgeGraph>) -> String {
let Some(graph) = graph else {
return self.generate_id(None);
};
let prefix = self.prefix();
let max_num = graph
.items()
.filter(|item| item.item_type == *self)
.filter_map(|item| {
item.id
.as_str()
.strip_prefix(prefix)
.and_then(|suffix| suffix.trim_start_matches('-').parse::<u32>().ok())
})
.max()
.unwrap_or(0);
format!("{}-{:03}", prefix, max_num + 1)
}
#[must_use]
pub const fn refines_types() -> &'static [ItemType] {
&[Self::UseCase, Self::Scenario]
}
#[must_use]
pub fn requires_refines(&self) -> bool {
Self::refines_types().contains(self)
}
#[must_use]
pub const fn derives_from_types() -> &'static [ItemType] {
&[
Self::SystemRequirement,
Self::HardwareRequirement,
Self::SoftwareRequirement,
]
}
#[must_use]
pub fn requires_derives_from(&self) -> bool {
Self::derives_from_types().contains(self)
}
#[must_use]
pub const fn satisfies_types() -> &'static [ItemType] {
&[
Self::SystemArchitecture,
Self::HardwareDetailedDesign,
Self::SoftwareDetailedDesign,
]
}
#[must_use]
pub fn requires_satisfies(&self) -> bool {
Self::satisfies_types().contains(self)
}
#[must_use]
pub const fn specification_types() -> &'static [ItemType] {
&[
Self::SystemRequirement,
Self::HardwareRequirement,
Self::SoftwareRequirement,
]
}
#[must_use]
pub fn requires_specification(&self) -> bool {
Self::specification_types().contains(self)
}
#[must_use]
pub const fn platform_types() -> &'static [ItemType] {
&[Self::SystemArchitecture]
}
#[must_use]
pub fn accepts_platform(&self) -> bool {
Self::platform_types().contains(self)
}
#[must_use]
pub const fn depends_on_types() -> &'static [ItemType] {
&[
Self::SystemRequirement,
Self::HardwareRequirement,
Self::SoftwareRequirement,
]
}
#[must_use]
pub fn supports_depends_on(&self) -> bool {
Self::depends_on_types().contains(self)
}
#[must_use]
pub const fn is_root(&self) -> bool {
matches!(self, Self::Solution)
}
#[must_use]
pub const fn requires_deciders(&self) -> bool {
matches!(self, Self::ArchitectureDecisionRecord)
}
#[must_use]
pub const fn supports_status(&self) -> bool {
matches!(self, Self::ArchitectureDecisionRecord)
}
#[must_use]
pub const fn supports_supersedes(&self) -> bool {
matches!(self, Self::ArchitectureDecisionRecord)
}
#[must_use]
pub const fn required_parent_type(&self) -> Option<ItemType> {
match self {
Self::Solution => None,
Self::UseCase => Some(Self::Solution),
Self::Scenario => Some(Self::UseCase),
Self::SystemRequirement => Some(Self::Scenario),
Self::SystemArchitecture => Some(Self::SystemRequirement),
Self::HardwareRequirement => Some(Self::SystemArchitecture),
Self::SoftwareRequirement => Some(Self::SystemArchitecture),
Self::HardwareDetailedDesign => Some(Self::HardwareRequirement),
Self::SoftwareDetailedDesign => Some(Self::SoftwareRequirement),
Self::ArchitectureDecisionRecord => None,
}
}
#[must_use]
pub const fn traceability_field(&self) -> Option<FieldName> {
match self {
Self::Solution => None,
Self::UseCase | Self::Scenario => Some(FieldName::Refines),
Self::SystemRequirement | Self::HardwareRequirement | Self::SoftwareRequirement => {
Some(FieldName::DerivesFrom)
}
Self::SystemArchitecture
| Self::HardwareDetailedDesign
| Self::SoftwareDetailedDesign => Some(FieldName::Satisfies),
Self::ArchitectureDecisionRecord => Some(FieldName::Justifies),
}
}
#[must_use]
pub const fn as_str(&self) -> &'static str {
match self {
Self::Solution => "solution",
Self::UseCase => "use_case",
Self::Scenario => "scenario",
Self::SystemRequirement => "system_requirement",
Self::SystemArchitecture => "system_architecture",
Self::HardwareRequirement => "hardware_requirement",
Self::SoftwareRequirement => "software_requirement",
Self::HardwareDetailedDesign => "hardware_detailed_design",
Self::SoftwareDetailedDesign => "software_detailed_design",
Self::ArchitectureDecisionRecord => "architecture_decision_record",
}
}
#[must_use]
pub fn traceability_configs(&self) -> Vec<TraceabilityConfig> {
match self {
ItemType::Solution => vec![],
ItemType::UseCase => vec![TraceabilityConfig {
relationship_field: FieldName::Refines,
target_type: ItemType::Solution,
}],
ItemType::Scenario => vec![TraceabilityConfig {
relationship_field: FieldName::Refines,
target_type: ItemType::UseCase,
}],
ItemType::SystemRequirement => vec![
TraceabilityConfig {
relationship_field: FieldName::DerivesFrom,
target_type: ItemType::Scenario,
},
TraceabilityConfig {
relationship_field: FieldName::DependsOn,
target_type: ItemType::SystemRequirement,
},
],
ItemType::SystemArchitecture => vec![TraceabilityConfig {
relationship_field: FieldName::Satisfies,
target_type: ItemType::SystemRequirement,
}],
ItemType::HardwareRequirement => vec![
TraceabilityConfig {
relationship_field: FieldName::DerivesFrom,
target_type: ItemType::SystemArchitecture,
},
TraceabilityConfig {
relationship_field: FieldName::DependsOn,
target_type: ItemType::HardwareRequirement,
},
],
ItemType::SoftwareRequirement => vec![
TraceabilityConfig {
relationship_field: FieldName::DerivesFrom,
target_type: ItemType::SystemArchitecture,
},
TraceabilityConfig {
relationship_field: FieldName::DependsOn,
target_type: ItemType::SoftwareRequirement,
},
],
ItemType::HardwareDetailedDesign => vec![TraceabilityConfig {
relationship_field: FieldName::Satisfies,
target_type: ItemType::HardwareRequirement,
}],
ItemType::SoftwareDetailedDesign => vec![TraceabilityConfig {
relationship_field: FieldName::Satisfies,
target_type: ItemType::SoftwareRequirement,
}],
ItemType::ArchitectureDecisionRecord => vec![
TraceabilityConfig {
relationship_field: FieldName::Justifies,
target_type: ItemType::SystemArchitecture,
},
TraceabilityConfig {
relationship_field: FieldName::Justifies,
target_type: ItemType::SoftwareDetailedDesign,
},
TraceabilityConfig {
relationship_field: FieldName::Justifies,
target_type: ItemType::HardwareDetailedDesign,
},
],
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct TraceabilityConfig {
pub relationship_field: FieldName,
pub target_type: ItemType,
}
impl fmt::Display for ItemType {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.display_name())
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(transparent)]
pub struct ItemId(String);
impl ItemId {
pub fn new(id: impl Into<String>) -> Result<Self, SaraError> {
let id = id.into();
if id.is_empty() {
return Err(SaraError::InvalidId {
id: id.clone(),
reason: "Item ID cannot be empty".to_string(),
});
}
if !id
.chars()
.all(|c| c.is_alphanumeric() || c == '-' || c == '_')
{
return Err(SaraError::InvalidId {
id: id.clone(),
reason:
"Item ID must contain only alphanumeric characters, hyphens, and underscores"
.to_string(),
});
}
Ok(Self(id))
}
pub fn new_unchecked(id: impl Into<String>) -> Self {
Self(id.into())
}
#[must_use]
pub fn as_str(&self) -> &str {
&self.0
}
#[must_use]
pub fn slice_to_strs(ids: &[ItemId]) -> Vec<&str> {
ids.iter().map(|id| id.as_str()).collect()
}
}
impl fmt::Display for ItemId {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.0)
}
}
impl AsRef<str> for ItemId {
fn as_ref(&self) -> &str {
&self.0
}
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
#[serde(tag = "_attr_type")]
pub enum ItemAttributes {
#[serde(rename = "solution")]
#[default]
Solution,
#[serde(rename = "use_case")]
UseCase,
#[serde(rename = "scenario")]
Scenario,
#[serde(rename = "system_requirement")]
SystemRequirement {
specification: String,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
depends_on: Vec<ItemId>,
},
#[serde(rename = "system_architecture")]
SystemArchitecture {
#[serde(default, skip_serializing_if = "Option::is_none")]
platform: Option<String>,
},
#[serde(rename = "software_requirement")]
SoftwareRequirement {
specification: String,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
depends_on: Vec<ItemId>,
},
#[serde(rename = "hardware_requirement")]
HardwareRequirement {
specification: String,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
depends_on: Vec<ItemId>,
},
#[serde(rename = "software_detailed_design")]
SoftwareDetailedDesign,
#[serde(rename = "hardware_detailed_design")]
HardwareDetailedDesign,
#[serde(rename = "architecture_decision_record")]
Adr {
status: AdrStatus,
deciders: Vec<String>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
supersedes: Vec<ItemId>,
},
}
impl ItemAttributes {
#[must_use]
pub fn for_type(item_type: ItemType) -> Self {
match item_type {
ItemType::Solution => ItemAttributes::Solution,
ItemType::UseCase => ItemAttributes::UseCase,
ItemType::Scenario => ItemAttributes::Scenario,
ItemType::SystemRequirement => ItemAttributes::SystemRequirement {
specification: String::new(),
depends_on: Vec::new(),
},
ItemType::SystemArchitecture => ItemAttributes::SystemArchitecture { platform: None },
ItemType::SoftwareRequirement => ItemAttributes::SoftwareRequirement {
specification: String::new(),
depends_on: Vec::new(),
},
ItemType::HardwareRequirement => ItemAttributes::HardwareRequirement {
specification: String::new(),
depends_on: Vec::new(),
},
ItemType::SoftwareDetailedDesign => ItemAttributes::SoftwareDetailedDesign,
ItemType::HardwareDetailedDesign => ItemAttributes::HardwareDetailedDesign,
ItemType::ArchitectureDecisionRecord => ItemAttributes::Adr {
status: AdrStatus::Proposed,
deciders: Vec::new(),
supersedes: Vec::new(),
},
}
}
#[must_use]
pub fn specification(&self) -> Option<&String> {
match self {
Self::SystemRequirement { specification, .. }
| Self::SoftwareRequirement { specification, .. }
| Self::HardwareRequirement { specification, .. } => Some(specification),
_ => None,
}
}
#[must_use]
pub fn depends_on(&self) -> &[ItemId] {
match self {
Self::SystemRequirement { depends_on, .. }
| Self::SoftwareRequirement { depends_on, .. }
| Self::HardwareRequirement { depends_on, .. } => depends_on,
_ => &[],
}
}
#[must_use]
pub fn depends_on_as_option(&self) -> Option<&[ItemId]> {
match self {
Self::SystemRequirement { depends_on, .. }
| Self::SoftwareRequirement { depends_on, .. }
| Self::HardwareRequirement { depends_on, .. } => Some(depends_on),
_ => None,
}
}
#[must_use]
pub fn platform(&self) -> Option<&String> {
match self {
Self::SystemArchitecture { platform, .. } => platform.as_ref(),
_ => None,
}
}
#[must_use]
pub fn status(&self) -> Option<AdrStatus> {
match self {
Self::Adr { status, .. } => Some(*status),
_ => None,
}
}
#[must_use]
pub fn deciders(&self) -> &[String] {
match self {
Self::Adr { deciders, .. } => deciders,
_ => &[],
}
}
#[must_use]
pub fn supersedes(&self) -> &[ItemId] {
match self {
Self::Adr { supersedes, .. } => supersedes,
_ => &[],
}
}
}
use crate::model::metadata::SourceLocation;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Item {
pub id: ItemId,
pub item_type: ItemType,
pub name: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub description: Option<String>,
pub source: SourceLocation,
#[serde(default)]
pub relationships: Vec<Relationship>,
#[serde(default)]
pub attributes: ItemAttributes,
}
impl Item {
pub fn relationship_ids(&self, rel_type: RelationshipType) -> impl Iterator<Item = &ItemId> {
self.relationships
.iter()
.filter(move |r| r.relationship_type == rel_type)
.map(|r| &r.to)
}
#[must_use]
pub fn has_relationship_type(&self, rel_type: RelationshipType) -> bool {
self.relationships
.iter()
.any(|r| r.relationship_type == rel_type)
}
#[must_use]
pub fn has_upstream(&self) -> bool {
self.relationships
.iter()
.any(|r| r.relationship_type.is_upstream())
}
pub fn all_references(&self) -> impl Iterator<Item = &ItemId> {
let relationship_refs = self.relationships.iter().map(|r| &r.to);
let peer_refs: Box<dyn Iterator<Item = &ItemId>> = match &self.attributes {
ItemAttributes::SystemRequirement { depends_on, .. }
| ItemAttributes::SoftwareRequirement { depends_on, .. }
| ItemAttributes::HardwareRequirement { depends_on, .. } => Box::new(depends_on.iter()),
ItemAttributes::Adr { supersedes, .. } => Box::new(supersedes.iter()),
_ => Box::new(std::iter::empty()),
};
relationship_refs.chain(peer_refs)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_item_id_valid() {
assert!(ItemId::new("SOL-001").is_ok());
assert!(ItemId::new("UC_002").is_ok());
assert!(ItemId::new("SYSREQ-123-A").is_ok());
}
#[test]
fn test_item_id_invalid() {
assert!(ItemId::new("").is_err());
assert!(ItemId::new("SOL 001").is_err());
assert!(ItemId::new("SOL.001").is_err());
}
#[test]
fn test_item_type_display() {
assert_eq!(ItemType::Solution.display_name(), "Solution");
assert_eq!(
ItemType::SystemRequirement.display_name(),
"System Requirement"
);
}
#[test]
fn test_item_type_requires_specification() {
assert!(ItemType::SystemRequirement.requires_specification());
assert!(ItemType::HardwareRequirement.requires_specification());
assert!(ItemType::SoftwareRequirement.requires_specification());
assert!(!ItemType::Solution.requires_specification());
assert!(!ItemType::Scenario.requires_specification());
}
#[test]
fn test_generate_id() {
assert_eq!(ItemType::Solution.generate_id(Some(1)), "SOL-001");
assert_eq!(ItemType::UseCase.generate_id(Some(42)), "UC-042");
assert_eq!(ItemType::SystemRequirement.generate_id(None), "SYSREQ-001");
}
}