use serde::{Deserialize, Serialize};
use std::fmt;
use crate::error::ValidationError;
use crate::model::FieldName;
#[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,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum AdrStatus {
Proposed,
Accepted,
Deprecated,
Superseded,
}
impl AdrStatus {
#[must_use]
pub const fn display_name(&self) -> &'static str {
match self {
Self::Proposed => "Proposed",
Self::Accepted => "Accepted",
Self::Deprecated => "Deprecated",
Self::Superseded => "Superseded",
}
}
#[must_use]
pub const fn as_str(&self) -> &'static str {
match self {
Self::Proposed => "proposed",
Self::Accepted => "accepted",
Self::Deprecated => "deprecated",
Self::Superseded => "superseded",
}
}
#[must_use]
pub const fn all() -> &'static [AdrStatus] {
&[
Self::Proposed,
Self::Accepted,
Self::Deprecated,
Self::Superseded,
]
}
}
impl fmt::Display for AdrStatus {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.display_name())
}
}
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 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, ValidationError> {
let id = id.into();
if id.is_empty() {
return Err(ValidationError::InvalidId {
id: id.clone(),
reason: "Item ID cannot be empty".to_string(),
});
}
if !id
.chars()
.all(|c| c.is_alphanumeric() || c == '-' || c == '_')
{
return Err(ValidationError::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
}
}
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)]
pub struct UpstreamRefs {
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub refines: Vec<ItemId>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub derives_from: Vec<ItemId>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub satisfies: Vec<ItemId>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub justifies: Vec<ItemId>,
}
impl UpstreamRefs {
pub fn all_ids(&self) -> impl Iterator<Item = &ItemId> {
self.refines
.iter()
.chain(self.derives_from.iter())
.chain(self.satisfies.iter())
.chain(self.justifies.iter())
}
#[must_use]
pub fn is_empty(&self) -> bool {
self.refines.is_empty()
&& self.derives_from.is_empty()
&& self.satisfies.is_empty()
&& self.justifies.is_empty()
}
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct DownstreamRefs {
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub is_refined_by: Vec<ItemId>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub derives: Vec<ItemId>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub is_satisfied_by: Vec<ItemId>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub justified_by: Vec<ItemId>,
}
impl DownstreamRefs {
pub fn all_ids(&self) -> impl Iterator<Item = &ItemId> {
self.is_refined_by
.iter()
.chain(self.derives.iter())
.chain(self.is_satisfied_by.iter())
.chain(self.justified_by.iter())
}
#[must_use]
pub fn is_empty(&self) -> bool {
self.is_refined_by.is_empty()
&& self.derives.is_empty()
&& self.is_satisfied_by.is_empty()
&& self.justified_by.is_empty()
}
}
#[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 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 upstream: UpstreamRefs,
#[serde(default)]
pub downstream: DownstreamRefs,
#[serde(default)]
pub attributes: ItemAttributes,
}
impl Item {
pub fn all_references(&self) -> impl Iterator<Item = &ItemId> {
let upstream_downstream = self.upstream.all_ids().chain(self.downstream.all_ids());
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()),
};
upstream_downstream.chain(peer_refs)
}
}
#[derive(Debug, Default)]
pub struct ItemBuilder {
id: Option<ItemId>,
item_type: Option<ItemType>,
name: Option<String>,
description: Option<String>,
source: Option<SourceLocation>,
upstream: UpstreamRefs,
downstream: DownstreamRefs,
specification: Option<String>,
platform: Option<String>,
depends_on: Vec<ItemId>,
status: Option<AdrStatus>,
deciders: Vec<String>,
supersedes: Vec<ItemId>,
}
impl ItemBuilder {
pub fn new() -> Self {
Self::default()
}
pub fn id(mut self, id: ItemId) -> Self {
self.id = Some(id);
self
}
pub fn item_type(mut self, item_type: ItemType) -> Self {
self.item_type = Some(item_type);
self
}
pub fn name(mut self, name: impl Into<String>) -> Self {
self.name = Some(name.into());
self
}
pub fn description(mut self, desc: impl Into<String>) -> Self {
self.description = Some(desc.into());
self
}
pub fn source(mut self, source: SourceLocation) -> Self {
self.source = Some(source);
self
}
pub fn upstream(mut self, upstream: UpstreamRefs) -> Self {
self.upstream = upstream;
self
}
pub fn downstream(mut self, downstream: DownstreamRefs) -> Self {
self.downstream = downstream;
self
}
pub fn specification(mut self, spec: impl Into<String>) -> Self {
self.specification = Some(spec.into());
self
}
pub fn platform(mut self, platform: impl Into<String>) -> Self {
self.platform = Some(platform.into());
self
}
pub fn depends_on(mut self, id: ItemId) -> Self {
self.depends_on.push(id);
self
}
pub fn status(mut self, status: AdrStatus) -> Self {
self.status = Some(status);
self
}
pub fn decider(mut self, decider: impl Into<String>) -> Self {
self.deciders.push(decider.into());
self
}
pub fn deciders(mut self, deciders: Vec<String>) -> Self {
self.deciders = deciders;
self
}
pub fn supersedes(mut self, id: ItemId) -> Self {
self.supersedes.push(id);
self
}
pub fn supersedes_all(mut self, ids: Vec<ItemId>) -> Self {
self.supersedes = ids;
self
}
pub fn attributes(mut self, attrs: ItemAttributes) -> Self {
match attrs {
ItemAttributes::Solution
| ItemAttributes::UseCase
| ItemAttributes::Scenario
| ItemAttributes::SoftwareDetailedDesign
| ItemAttributes::HardwareDetailedDesign => {}
ItemAttributes::SystemRequirement {
specification,
depends_on,
} => {
self.specification = Some(specification);
self.depends_on = depends_on;
}
ItemAttributes::SystemArchitecture { platform } => {
self.platform = platform;
}
ItemAttributes::SoftwareRequirement {
specification,
depends_on,
} => {
self.specification = Some(specification);
self.depends_on = depends_on;
}
ItemAttributes::HardwareRequirement {
specification,
depends_on,
} => {
self.specification = Some(specification);
self.depends_on = depends_on;
}
ItemAttributes::Adr {
status,
deciders,
supersedes,
} => {
self.status = Some(status);
self.deciders = deciders;
self.supersedes = supersedes;
}
}
self
}
fn require_specification(&self, file: &str) -> Result<String, ValidationError> {
self.specification
.clone()
.ok_or_else(|| ValidationError::MissingField {
field: "specification".to_string(),
file: file.to_string(),
})
}
fn build_attributes(
&self,
item_type: ItemType,
file: &str,
) -> Result<ItemAttributes, ValidationError> {
match item_type {
ItemType::Solution => Ok(ItemAttributes::Solution),
ItemType::UseCase => Ok(ItemAttributes::UseCase),
ItemType::Scenario => Ok(ItemAttributes::Scenario),
ItemType::SoftwareDetailedDesign => Ok(ItemAttributes::SoftwareDetailedDesign),
ItemType::HardwareDetailedDesign => Ok(ItemAttributes::HardwareDetailedDesign),
ItemType::SystemArchitecture => Ok(ItemAttributes::SystemArchitecture {
platform: self.platform.clone(),
}),
ItemType::SystemRequirement => Ok(ItemAttributes::SystemRequirement {
specification: self.require_specification(file)?,
depends_on: self.depends_on.clone(),
}),
ItemType::SoftwareRequirement => Ok(ItemAttributes::SoftwareRequirement {
specification: self.require_specification(file)?,
depends_on: self.depends_on.clone(),
}),
ItemType::HardwareRequirement => Ok(ItemAttributes::HardwareRequirement {
specification: self.require_specification(file)?,
depends_on: self.depends_on.clone(),
}),
ItemType::ArchitectureDecisionRecord => {
let status = self.status.ok_or_else(|| ValidationError::MissingField {
field: "status".to_string(),
file: file.to_string(),
})?;
if self.deciders.is_empty() {
return Err(ValidationError::MissingField {
field: "deciders".to_string(),
file: file.to_string(),
});
}
Ok(ItemAttributes::Adr {
status,
deciders: self.deciders.clone(),
supersedes: self.supersedes.clone(),
})
}
}
}
pub fn build(self) -> Result<Item, ValidationError> {
let id = self
.id
.clone()
.ok_or_else(|| ValidationError::MissingField {
field: "id".to_string(),
file: self
.source
.as_ref()
.map(|s| s.file_path.display().to_string())
.unwrap_or_default(),
})?;
let item_type = self
.item_type
.ok_or_else(|| ValidationError::MissingField {
field: "type".to_string(),
file: self
.source
.as_ref()
.map(|s| s.file_path.display().to_string())
.unwrap_or_default(),
})?;
let name = self
.name
.clone()
.ok_or_else(|| ValidationError::MissingField {
field: "name".to_string(),
file: self
.source
.as_ref()
.map(|s| s.file_path.display().to_string())
.unwrap_or_default(),
})?;
let source = self
.source
.clone()
.ok_or_else(|| ValidationError::MissingField {
field: "source".to_string(),
file: String::new(),
})?;
let file_path = source.file_path.display().to_string();
let attributes = self.build_attributes(item_type, &file_path)?;
Ok(Item {
id,
item_type,
name,
description: self.description,
source,
upstream: self.upstream,
downstream: self.downstream,
attributes,
})
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::path::PathBuf;
#[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_item_builder() {
let source = SourceLocation {
repository: PathBuf::from("/repo"),
file_path: PathBuf::from("docs/SOL-001.md"),
git_ref: None,
};
let item = ItemBuilder::new()
.id(ItemId::new_unchecked("SOL-001"))
.item_type(ItemType::Solution)
.name("Test Solution")
.source(source)
.build();
assert!(item.is_ok());
let item = item.unwrap();
assert_eq!(item.id.as_str(), "SOL-001");
assert_eq!(item.name, "Test Solution");
}
}