1use serde::{Deserialize, Serialize};
4use std::fmt;
5
6use crate::error::SaraError;
7use crate::model::FieldName;
8use crate::model::relationship::{Relationship, RelationshipType};
9
10use super::adr::AdrStatus;
11
12#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
14#[serde(rename_all = "snake_case")]
15pub enum ItemType {
16 Solution,
17 UseCase,
18 Scenario,
19 SystemRequirement,
20 SystemArchitecture,
21 HardwareRequirement,
22 SoftwareRequirement,
23 HardwareDetailedDesign,
24 SoftwareDetailedDesign,
25 ArchitectureDecisionRecord,
26}
27
28impl ItemType {
29 #[must_use]
31 pub const fn all() -> &'static [ItemType] {
32 &[
33 Self::Solution,
34 Self::UseCase,
35 Self::Scenario,
36 Self::SystemRequirement,
37 Self::SystemArchitecture,
38 Self::HardwareRequirement,
39 Self::SoftwareRequirement,
40 Self::HardwareDetailedDesign,
41 Self::SoftwareDetailedDesign,
42 Self::ArchitectureDecisionRecord,
43 ]
44 }
45
46 #[must_use]
48 pub const fn display_name(&self) -> &'static str {
49 match self {
50 Self::Solution => "Solution",
51 Self::UseCase => "Use Case",
52 Self::Scenario => "Scenario",
53 Self::SystemRequirement => "System Requirement",
54 Self::SystemArchitecture => "System Architecture",
55 Self::HardwareRequirement => "Hardware Requirement",
56 Self::SoftwareRequirement => "Software Requirement",
57 Self::HardwareDetailedDesign => "Hardware Detailed Design",
58 Self::SoftwareDetailedDesign => "Software Detailed Design",
59 Self::ArchitectureDecisionRecord => "Architecture Decision Record",
60 }
61 }
62
63 #[must_use]
65 pub const fn prefix(&self) -> &'static str {
66 match self {
67 Self::Solution => "SOL",
68 Self::UseCase => "UC",
69 Self::Scenario => "SCEN",
70 Self::SystemRequirement => "SYSREQ",
71 Self::SystemArchitecture => "SYSARCH",
72 Self::HardwareRequirement => "HWREQ",
73 Self::SoftwareRequirement => "SWREQ",
74 Self::HardwareDetailedDesign => "HWDD",
75 Self::SoftwareDetailedDesign => "SWDD",
76 Self::ArchitectureDecisionRecord => "ADR",
77 }
78 }
79
80 #[must_use]
84 pub fn generate_id(&self, sequence: Option<u32>) -> String {
85 let num = sequence.unwrap_or(1);
86 format!("{}-{:03}", self.prefix(), num)
87 }
88
89 #[must_use]
94 pub fn suggest_next_id(&self, graph: Option<&crate::graph::KnowledgeGraph>) -> String {
95 let Some(graph) = graph else {
96 return self.generate_id(None);
97 };
98
99 let prefix = self.prefix();
100 let max_num = graph
101 .items()
102 .filter(|item| item.item_type == *self)
103 .filter_map(|item| {
104 item.id
105 .as_str()
106 .strip_prefix(prefix)
107 .and_then(|suffix| suffix.trim_start_matches('-').parse::<u32>().ok())
108 })
109 .max()
110 .unwrap_or(0);
111
112 format!("{}-{:03}", prefix, max_num + 1)
113 }
114
115 #[must_use]
117 pub const fn requires_refines(&self) -> bool {
118 matches!(self, Self::UseCase | Self::Scenario)
119 }
120
121 #[must_use]
123 pub const fn requires_derives_from(&self) -> bool {
124 matches!(
125 self,
126 Self::SystemRequirement | Self::HardwareRequirement | Self::SoftwareRequirement
127 )
128 }
129
130 #[must_use]
132 pub const fn requires_satisfies(&self) -> bool {
133 matches!(
134 self,
135 Self::SystemArchitecture | Self::HardwareDetailedDesign | Self::SoftwareDetailedDesign
136 )
137 }
138
139 #[must_use]
141 pub const fn requires_specification(&self) -> bool {
142 matches!(
143 self,
144 Self::SystemRequirement | Self::HardwareRequirement | Self::SoftwareRequirement
145 )
146 }
147
148 #[must_use]
150 pub const fn accepts_platform(&self) -> bool {
151 matches!(self, Self::SystemArchitecture)
152 }
153
154 #[must_use]
156 pub const fn supports_depends_on(&self) -> bool {
157 matches!(
158 self,
159 Self::SystemRequirement | Self::HardwareRequirement | Self::SoftwareRequirement
160 )
161 }
162
163 #[must_use]
165 pub const fn is_root(&self) -> bool {
166 matches!(self, Self::Solution)
167 }
168
169 #[must_use]
171 pub const fn requires_deciders(&self) -> bool {
172 matches!(self, Self::ArchitectureDecisionRecord)
173 }
174
175 #[must_use]
177 pub const fn supports_status(&self) -> bool {
178 matches!(self, Self::ArchitectureDecisionRecord)
179 }
180
181 #[must_use]
183 pub const fn supports_supersedes(&self) -> bool {
184 matches!(self, Self::ArchitectureDecisionRecord)
185 }
186
187 #[must_use]
191 pub const fn required_parent_type(&self) -> Option<ItemType> {
192 match self {
193 Self::Solution => None,
194 Self::UseCase => Some(Self::Solution),
195 Self::Scenario => Some(Self::UseCase),
196 Self::SystemRequirement => Some(Self::Scenario),
197 Self::SystemArchitecture => Some(Self::SystemRequirement),
198 Self::HardwareRequirement => Some(Self::SystemArchitecture),
199 Self::SoftwareRequirement => Some(Self::SystemArchitecture),
200 Self::HardwareDetailedDesign => Some(Self::HardwareRequirement),
201 Self::SoftwareDetailedDesign => Some(Self::SoftwareRequirement),
202 Self::ArchitectureDecisionRecord => None,
203 }
204 }
205
206 #[must_use]
208 pub const fn traceability_field(&self) -> Option<FieldName> {
209 match self {
210 Self::Solution => None,
211 Self::UseCase | Self::Scenario => Some(FieldName::Refines),
212 Self::SystemRequirement | Self::HardwareRequirement | Self::SoftwareRequirement => {
213 Some(FieldName::DerivesFrom)
214 }
215 Self::SystemArchitecture
216 | Self::HardwareDetailedDesign
217 | Self::SoftwareDetailedDesign => Some(FieldName::Satisfies),
218 Self::ArchitectureDecisionRecord => Some(FieldName::Justifies),
219 }
220 }
221
222 #[must_use]
224 pub const fn as_str(&self) -> &'static str {
225 match self {
226 Self::Solution => "solution",
227 Self::UseCase => "use_case",
228 Self::Scenario => "scenario",
229 Self::SystemRequirement => "system_requirement",
230 Self::SystemArchitecture => "system_architecture",
231 Self::HardwareRequirement => "hardware_requirement",
232 Self::SoftwareRequirement => "software_requirement",
233 Self::HardwareDetailedDesign => "hardware_detailed_design",
234 Self::SoftwareDetailedDesign => "software_detailed_design",
235 Self::ArchitectureDecisionRecord => "architecture_decision_record",
236 }
237 }
238
239 #[must_use]
245 pub fn traceability_configs(&self) -> Vec<TraceabilityConfig> {
246 match self {
247 ItemType::Solution => vec![],
248 ItemType::UseCase => vec![TraceabilityConfig {
249 relationship_field: FieldName::Refines,
250 target_type: ItemType::Solution,
251 }],
252 ItemType::Scenario => vec![TraceabilityConfig {
253 relationship_field: FieldName::Refines,
254 target_type: ItemType::UseCase,
255 }],
256 ItemType::SystemRequirement => vec![
257 TraceabilityConfig {
258 relationship_field: FieldName::DerivesFrom,
259 target_type: ItemType::Scenario,
260 },
261 TraceabilityConfig {
262 relationship_field: FieldName::DependsOn,
263 target_type: ItemType::SystemRequirement,
264 },
265 ],
266 ItemType::SystemArchitecture => vec![TraceabilityConfig {
267 relationship_field: FieldName::Satisfies,
268 target_type: ItemType::SystemRequirement,
269 }],
270 ItemType::HardwareRequirement => vec![
271 TraceabilityConfig {
272 relationship_field: FieldName::DerivesFrom,
273 target_type: ItemType::SystemArchitecture,
274 },
275 TraceabilityConfig {
276 relationship_field: FieldName::DependsOn,
277 target_type: ItemType::HardwareRequirement,
278 },
279 ],
280 ItemType::SoftwareRequirement => vec![
281 TraceabilityConfig {
282 relationship_field: FieldName::DerivesFrom,
283 target_type: ItemType::SystemArchitecture,
284 },
285 TraceabilityConfig {
286 relationship_field: FieldName::DependsOn,
287 target_type: ItemType::SoftwareRequirement,
288 },
289 ],
290 ItemType::HardwareDetailedDesign => vec![TraceabilityConfig {
291 relationship_field: FieldName::Satisfies,
292 target_type: ItemType::HardwareRequirement,
293 }],
294 ItemType::SoftwareDetailedDesign => vec![TraceabilityConfig {
295 relationship_field: FieldName::Satisfies,
296 target_type: ItemType::SoftwareRequirement,
297 }],
298 ItemType::ArchitectureDecisionRecord => vec![
299 TraceabilityConfig {
300 relationship_field: FieldName::Justifies,
301 target_type: ItemType::SystemArchitecture,
302 },
303 TraceabilityConfig {
304 relationship_field: FieldName::Justifies,
305 target_type: ItemType::SoftwareDetailedDesign,
306 },
307 TraceabilityConfig {
308 relationship_field: FieldName::Justifies,
309 target_type: ItemType::HardwareDetailedDesign,
310 },
311 ],
312 }
313 }
314}
315
316#[derive(Debug, Clone, Copy, PartialEq, Eq)]
318pub struct TraceabilityConfig {
319 pub relationship_field: FieldName,
321 pub target_type: ItemType,
323}
324
325impl fmt::Display for ItemType {
326 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
327 write!(f, "{}", self.display_name())
328 }
329}
330
331#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
333#[serde(transparent)]
334pub struct ItemId(String);
335
336impl ItemId {
337 pub fn new(id: impl Into<String>) -> Result<Self, SaraError> {
339 let id = id.into();
340 if id.is_empty() {
341 return Err(SaraError::InvalidId {
342 id: id.clone(),
343 reason: "Item ID cannot be empty".to_string(),
344 });
345 }
346
347 if !id
349 .chars()
350 .all(|c| c.is_alphanumeric() || c == '-' || c == '_')
351 {
352 return Err(SaraError::InvalidId {
353 id: id.clone(),
354 reason:
355 "Item ID must contain only alphanumeric characters, hyphens, and underscores"
356 .to_string(),
357 });
358 }
359
360 Ok(Self(id))
361 }
362
363 pub fn new_unchecked(id: impl Into<String>) -> Self {
368 Self(id.into())
369 }
370
371 #[must_use]
373 pub fn as_str(&self) -> &str {
374 &self.0
375 }
376}
377
378impl fmt::Display for ItemId {
379 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
380 write!(f, "{}", self.0)
381 }
382}
383
384impl AsRef<str> for ItemId {
385 fn as_ref(&self) -> &str {
386 &self.0
387 }
388}
389
390#[derive(Debug, Clone, Default, Serialize, Deserialize)]
392#[serde(tag = "_attr_type")]
393pub enum ItemAttributes {
394 #[serde(rename = "solution")]
396 #[default]
397 Solution,
398
399 #[serde(rename = "use_case")]
401 UseCase,
402
403 #[serde(rename = "scenario")]
405 Scenario,
406
407 #[serde(rename = "system_requirement")]
409 SystemRequirement {
410 specification: String,
412 #[serde(default, skip_serializing_if = "Vec::is_empty")]
414 depends_on: Vec<ItemId>,
415 },
416
417 #[serde(rename = "system_architecture")]
419 SystemArchitecture {
420 #[serde(default, skip_serializing_if = "Option::is_none")]
422 platform: Option<String>,
423 },
424
425 #[serde(rename = "software_requirement")]
427 SoftwareRequirement {
428 specification: String,
430 #[serde(default, skip_serializing_if = "Vec::is_empty")]
432 depends_on: Vec<ItemId>,
433 },
434
435 #[serde(rename = "hardware_requirement")]
437 HardwareRequirement {
438 specification: String,
440 #[serde(default, skip_serializing_if = "Vec::is_empty")]
442 depends_on: Vec<ItemId>,
443 },
444
445 #[serde(rename = "software_detailed_design")]
447 SoftwareDetailedDesign,
448
449 #[serde(rename = "hardware_detailed_design")]
451 HardwareDetailedDesign,
452
453 #[serde(rename = "architecture_decision_record")]
455 Adr {
456 status: AdrStatus,
458 deciders: Vec<String>,
460 #[serde(default, skip_serializing_if = "Vec::is_empty")]
462 supersedes: Vec<ItemId>,
463 },
464}
465
466impl ItemAttributes {
467 #[must_use]
469 pub fn for_type(item_type: ItemType) -> Self {
470 match item_type {
471 ItemType::Solution => ItemAttributes::Solution,
472 ItemType::UseCase => ItemAttributes::UseCase,
473 ItemType::Scenario => ItemAttributes::Scenario,
474 ItemType::SystemRequirement => ItemAttributes::SystemRequirement {
475 specification: String::new(),
476 depends_on: Vec::new(),
477 },
478 ItemType::SystemArchitecture => ItemAttributes::SystemArchitecture { platform: None },
479 ItemType::SoftwareRequirement => ItemAttributes::SoftwareRequirement {
480 specification: String::new(),
481 depends_on: Vec::new(),
482 },
483 ItemType::HardwareRequirement => ItemAttributes::HardwareRequirement {
484 specification: String::new(),
485 depends_on: Vec::new(),
486 },
487 ItemType::SoftwareDetailedDesign => ItemAttributes::SoftwareDetailedDesign,
488 ItemType::HardwareDetailedDesign => ItemAttributes::HardwareDetailedDesign,
489 ItemType::ArchitectureDecisionRecord => ItemAttributes::Adr {
490 status: AdrStatus::Proposed,
491 deciders: Vec::new(),
492 supersedes: Vec::new(),
493 },
494 }
495 }
496
497 #[must_use]
499 pub fn specification(&self) -> Option<&String> {
500 match self {
501 Self::SystemRequirement { specification, .. }
502 | Self::SoftwareRequirement { specification, .. }
503 | Self::HardwareRequirement { specification, .. } => Some(specification),
504 _ => None,
505 }
506 }
507
508 #[must_use]
510 pub fn depends_on(&self) -> &[ItemId] {
511 match self {
512 Self::SystemRequirement { depends_on, .. }
513 | Self::SoftwareRequirement { depends_on, .. }
514 | Self::HardwareRequirement { depends_on, .. } => depends_on,
515 _ => &[],
516 }
517 }
518
519 #[must_use]
521 pub fn platform(&self) -> Option<&String> {
522 match self {
523 Self::SystemArchitecture { platform, .. } => platform.as_ref(),
524 _ => None,
525 }
526 }
527
528 #[must_use]
530 pub fn status(&self) -> Option<AdrStatus> {
531 match self {
532 Self::Adr { status, .. } => Some(*status),
533 _ => None,
534 }
535 }
536
537 #[must_use]
539 pub fn deciders(&self) -> &[String] {
540 match self {
541 Self::Adr { deciders, .. } => deciders,
542 _ => &[],
543 }
544 }
545
546 #[must_use]
548 pub fn supersedes(&self) -> &[ItemId] {
549 match self {
550 Self::Adr { supersedes, .. } => supersedes,
551 _ => &[],
552 }
553 }
554}
555
556use crate::model::metadata::SourceLocation;
557
558#[derive(Debug, Clone, Serialize, Deserialize)]
560pub struct Item {
561 pub id: ItemId,
563
564 pub item_type: ItemType,
566
567 pub name: String,
569
570 #[serde(default, skip_serializing_if = "Option::is_none")]
572 pub description: Option<String>,
573
574 pub source: SourceLocation,
576
577 #[serde(default)]
579 pub relationships: Vec<Relationship>,
580
581 #[serde(default)]
583 pub attributes: ItemAttributes,
584}
585
586impl Item {
587 pub fn relationship_ids(&self, rel_type: RelationshipType) -> impl Iterator<Item = &ItemId> {
589 self.relationships
590 .iter()
591 .filter(move |r| r.relationship_type == rel_type)
592 .map(|r| &r.to)
593 }
594
595 #[must_use]
597 pub fn has_relationship_type(&self, rel_type: RelationshipType) -> bool {
598 self.relationships
599 .iter()
600 .any(|r| r.relationship_type == rel_type)
601 }
602
603 #[must_use]
605 pub fn has_upstream(&self) -> bool {
606 self.relationships
607 .iter()
608 .any(|r| r.relationship_type.is_upstream())
609 }
610
611 pub fn all_references(&self) -> impl Iterator<Item = &ItemId> {
613 let relationship_refs = self.relationships.iter().map(|r| &r.to);
614
615 let peer_refs: Box<dyn Iterator<Item = &ItemId>> = match &self.attributes {
617 ItemAttributes::SystemRequirement { depends_on, .. }
618 | ItemAttributes::SoftwareRequirement { depends_on, .. }
619 | ItemAttributes::HardwareRequirement { depends_on, .. } => Box::new(depends_on.iter()),
620 ItemAttributes::Adr { supersedes, .. } => Box::new(supersedes.iter()),
621 _ => Box::new(std::iter::empty()),
622 };
623
624 relationship_refs.chain(peer_refs)
625 }
626}
627
628#[cfg(test)]
629mod tests {
630 use super::*;
631
632 #[test]
633 fn test_item_id_valid() {
634 assert!(ItemId::new("SOL-001").is_ok());
635 assert!(ItemId::new("UC_002").is_ok());
636 assert!(ItemId::new("SYSREQ-123-A").is_ok());
637 }
638
639 #[test]
640 fn test_item_id_invalid() {
641 assert!(ItemId::new("").is_err());
642 assert!(ItemId::new("SOL 001").is_err());
643 assert!(ItemId::new("SOL.001").is_err());
644 }
645
646 #[test]
647 fn test_item_type_display() {
648 assert_eq!(ItemType::Solution.display_name(), "Solution");
649 assert_eq!(
650 ItemType::SystemRequirement.display_name(),
651 "System Requirement"
652 );
653 }
654
655 #[test]
656 fn test_item_type_requires_specification() {
657 assert!(ItemType::SystemRequirement.requires_specification());
658 assert!(ItemType::HardwareRequirement.requires_specification());
659 assert!(ItemType::SoftwareRequirement.requires_specification());
660 assert!(!ItemType::Solution.requires_specification());
661 assert!(!ItemType::Scenario.requires_specification());
662 }
663
664 #[test]
665 fn test_generate_id() {
666 assert_eq!(ItemType::Solution.generate_id(Some(1)), "SOL-001");
667 assert_eq!(ItemType::UseCase.generate_id(Some(42)), "UC-042");
668 assert_eq!(ItemType::SystemRequirement.generate_id(None), "SYSREQ-001");
669 }
670}