1#![allow(clippy::result_large_err)]
4
5use serde::{Deserialize, Serialize};
6use std::fmt;
7
8use crate::error::ValidationError;
9use crate::model::FieldName;
10
11#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
13#[serde(rename_all = "snake_case")]
14pub enum ItemType {
15 Solution,
16 UseCase,
17 Scenario,
18 SystemRequirement,
19 SystemArchitecture,
20 HardwareRequirement,
21 SoftwareRequirement,
22 HardwareDetailedDesign,
23 SoftwareDetailedDesign,
24}
25
26impl ItemType {
27 pub fn all() -> &'static [ItemType] {
29 &[
30 ItemType::Solution,
31 ItemType::UseCase,
32 ItemType::Scenario,
33 ItemType::SystemRequirement,
34 ItemType::SystemArchitecture,
35 ItemType::HardwareRequirement,
36 ItemType::SoftwareRequirement,
37 ItemType::HardwareDetailedDesign,
38 ItemType::SoftwareDetailedDesign,
39 ]
40 }
41
42 pub fn display_name(&self) -> &'static str {
44 match self {
45 ItemType::Solution => "Solution",
46 ItemType::UseCase => "Use Case",
47 ItemType::Scenario => "Scenario",
48 ItemType::SystemRequirement => "System Requirement",
49 ItemType::SystemArchitecture => "System Architecture",
50 ItemType::HardwareRequirement => "Hardware Requirement",
51 ItemType::SoftwareRequirement => "Software Requirement",
52 ItemType::HardwareDetailedDesign => "Hardware Detailed Design",
53 ItemType::SoftwareDetailedDesign => "Software Detailed Design",
54 }
55 }
56
57 pub fn prefix(&self) -> &'static str {
59 match self {
60 ItemType::Solution => "SOL",
61 ItemType::UseCase => "UC",
62 ItemType::Scenario => "SCEN",
63 ItemType::SystemRequirement => "SYSREQ",
64 ItemType::SystemArchitecture => "SYSARCH",
65 ItemType::HardwareRequirement => "HWREQ",
66 ItemType::SoftwareRequirement => "SWREQ",
67 ItemType::HardwareDetailedDesign => "HWDD",
68 ItemType::SoftwareDetailedDesign => "SWDD",
69 }
70 }
71
72 pub fn refines_types() -> &'static [ItemType] {
74 &[ItemType::UseCase, ItemType::Scenario]
75 }
76
77 pub fn requires_refines(&self) -> bool {
79 Self::refines_types().contains(self)
80 }
81
82 pub fn derives_from_types() -> &'static [ItemType] {
84 &[
85 ItemType::SystemRequirement,
86 ItemType::HardwareRequirement,
87 ItemType::SoftwareRequirement,
88 ]
89 }
90
91 pub fn requires_derives_from(&self) -> bool {
93 Self::derives_from_types().contains(self)
94 }
95
96 pub fn satisfies_types() -> &'static [ItemType] {
98 &[
99 ItemType::SystemArchitecture,
100 ItemType::HardwareDetailedDesign,
101 ItemType::SoftwareDetailedDesign,
102 ]
103 }
104
105 pub fn requires_satisfies(&self) -> bool {
107 Self::satisfies_types().contains(self)
108 }
109
110 pub fn specification_types() -> &'static [ItemType] {
112 &[
113 ItemType::SystemRequirement,
114 ItemType::HardwareRequirement,
115 ItemType::SoftwareRequirement,
116 ]
117 }
118
119 pub fn requires_specification(&self) -> bool {
121 Self::specification_types().contains(self)
122 }
123
124 pub fn platform_types() -> &'static [ItemType] {
126 &[ItemType::SystemArchitecture]
127 }
128
129 pub fn accepts_platform(&self) -> bool {
131 Self::platform_types().contains(self)
132 }
133
134 pub fn depends_on_types() -> &'static [ItemType] {
136 &[
137 ItemType::SystemRequirement,
138 ItemType::HardwareRequirement,
139 ItemType::SoftwareRequirement,
140 ]
141 }
142
143 pub fn supports_depends_on(&self) -> bool {
145 Self::depends_on_types().contains(self)
146 }
147
148 pub fn is_root(&self) -> bool {
150 matches!(self, ItemType::Solution)
151 }
152
153 pub fn is_leaf(&self) -> bool {
155 matches!(
156 self,
157 ItemType::HardwareDetailedDesign | ItemType::SoftwareDetailedDesign
158 )
159 }
160
161 pub fn required_parent_type(&self) -> Option<ItemType> {
164 match self {
165 ItemType::Solution => None,
166 ItemType::UseCase => Some(ItemType::Solution),
167 ItemType::Scenario => Some(ItemType::UseCase),
168 ItemType::SystemRequirement => Some(ItemType::Scenario),
169 ItemType::SystemArchitecture => Some(ItemType::SystemRequirement),
170 ItemType::HardwareRequirement => Some(ItemType::SystemArchitecture),
171 ItemType::SoftwareRequirement => Some(ItemType::SystemArchitecture),
172 ItemType::HardwareDetailedDesign => Some(ItemType::HardwareRequirement),
173 ItemType::SoftwareDetailedDesign => Some(ItemType::SoftwareRequirement),
174 }
175 }
176
177 pub fn traceability_field(&self) -> Option<FieldName> {
179 match self {
180 ItemType::Solution => None,
181 ItemType::UseCase | ItemType::Scenario => Some(FieldName::Refines),
182 ItemType::SystemRequirement
183 | ItemType::HardwareRequirement
184 | ItemType::SoftwareRequirement => Some(FieldName::DerivesFrom),
185 ItemType::SystemArchitecture
186 | ItemType::HardwareDetailedDesign
187 | ItemType::SoftwareDetailedDesign => Some(FieldName::Satisfies),
188 }
189 }
190
191 pub fn as_str(&self) -> &'static str {
193 match self {
194 ItemType::Solution => "solution",
195 ItemType::UseCase => "use_case",
196 ItemType::Scenario => "scenario",
197 ItemType::SystemRequirement => "system_requirement",
198 ItemType::SystemArchitecture => "system_architecture",
199 ItemType::HardwareRequirement => "hardware_requirement",
200 ItemType::SoftwareRequirement => "software_requirement",
201 ItemType::HardwareDetailedDesign => "hardware_detailed_design",
202 ItemType::SoftwareDetailedDesign => "software_detailed_design",
203 }
204 }
205
206 pub fn traceability_configs(&self) -> Vec<TraceabilityConfig> {
212 match self {
213 ItemType::Solution => vec![],
214 ItemType::UseCase => vec![TraceabilityConfig {
215 relationship_field: FieldName::Refines,
216 target_type: ItemType::Solution,
217 }],
218 ItemType::Scenario => vec![TraceabilityConfig {
219 relationship_field: FieldName::Refines,
220 target_type: ItemType::UseCase,
221 }],
222 ItemType::SystemRequirement => vec![
223 TraceabilityConfig {
224 relationship_field: FieldName::DerivesFrom,
225 target_type: ItemType::Scenario,
226 },
227 TraceabilityConfig {
228 relationship_field: FieldName::DependsOn,
229 target_type: ItemType::SystemRequirement,
230 },
231 ],
232 ItemType::SystemArchitecture => vec![TraceabilityConfig {
233 relationship_field: FieldName::Satisfies,
234 target_type: ItemType::SystemRequirement,
235 }],
236 ItemType::HardwareRequirement => vec![
237 TraceabilityConfig {
238 relationship_field: FieldName::DerivesFrom,
239 target_type: ItemType::SystemArchitecture,
240 },
241 TraceabilityConfig {
242 relationship_field: FieldName::DependsOn,
243 target_type: ItemType::HardwareRequirement,
244 },
245 ],
246 ItemType::SoftwareRequirement => vec![
247 TraceabilityConfig {
248 relationship_field: FieldName::DerivesFrom,
249 target_type: ItemType::SystemArchitecture,
250 },
251 TraceabilityConfig {
252 relationship_field: FieldName::DependsOn,
253 target_type: ItemType::SoftwareRequirement,
254 },
255 ],
256 ItemType::HardwareDetailedDesign => vec![TraceabilityConfig {
257 relationship_field: FieldName::Satisfies,
258 target_type: ItemType::HardwareRequirement,
259 }],
260 ItemType::SoftwareDetailedDesign => vec![TraceabilityConfig {
261 relationship_field: FieldName::Satisfies,
262 target_type: ItemType::SoftwareRequirement,
263 }],
264 }
265 }
266}
267
268#[derive(Debug, Clone, Copy, PartialEq, Eq)]
270pub struct TraceabilityConfig {
271 pub relationship_field: FieldName,
273 pub target_type: ItemType,
275}
276
277impl fmt::Display for ItemType {
278 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
279 write!(f, "{}", self.display_name())
280 }
281}
282
283#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
285#[serde(transparent)]
286pub struct ItemId(String);
287
288impl ItemId {
289 pub fn new(id: impl Into<String>) -> Result<Self, ValidationError> {
291 let id = id.into();
292 if id.is_empty() {
293 return Err(ValidationError::InvalidId {
294 id: id.clone(),
295 reason: "Item ID cannot be empty".to_string(),
296 });
297 }
298
299 if !id
301 .chars()
302 .all(|c| c.is_alphanumeric() || c == '-' || c == '_')
303 {
304 return Err(ValidationError::InvalidId {
305 id: id.clone(),
306 reason:
307 "Item ID must contain only alphanumeric characters, hyphens, and underscores"
308 .to_string(),
309 });
310 }
311
312 Ok(Self(id))
313 }
314
315 pub fn new_unchecked(id: impl Into<String>) -> Self {
320 Self(id.into())
321 }
322
323 pub fn as_str(&self) -> &str {
325 &self.0
326 }
327}
328
329impl fmt::Display for ItemId {
330 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
331 write!(f, "{}", self.0)
332 }
333}
334
335impl AsRef<str> for ItemId {
336 fn as_ref(&self) -> &str {
337 &self.0
338 }
339}
340
341#[derive(Debug, Clone, Default, Serialize, Deserialize)]
343pub struct UpstreamRefs {
344 #[serde(default, skip_serializing_if = "Vec::is_empty")]
346 pub refines: Vec<ItemId>,
347
348 #[serde(default, skip_serializing_if = "Vec::is_empty")]
350 pub derives_from: Vec<ItemId>,
351
352 #[serde(default, skip_serializing_if = "Vec::is_empty")]
354 pub satisfies: Vec<ItemId>,
355}
356
357impl UpstreamRefs {
358 pub fn all_ids(&self) -> Vec<&ItemId> {
360 let mut ids = Vec::new();
361 ids.extend(self.refines.iter());
362 ids.extend(self.derives_from.iter());
363 ids.extend(self.satisfies.iter());
364 ids
365 }
366
367 pub fn is_empty(&self) -> bool {
369 self.refines.is_empty() && self.derives_from.is_empty() && self.satisfies.is_empty()
370 }
371}
372
373#[derive(Debug, Clone, Default, Serialize, Deserialize)]
375pub struct DownstreamRefs {
376 #[serde(default, skip_serializing_if = "Vec::is_empty")]
378 pub is_refined_by: Vec<ItemId>,
379
380 #[serde(default, skip_serializing_if = "Vec::is_empty")]
382 pub derives: Vec<ItemId>,
383
384 #[serde(default, skip_serializing_if = "Vec::is_empty")]
386 pub is_satisfied_by: Vec<ItemId>,
387}
388
389impl DownstreamRefs {
390 pub fn all_ids(&self) -> Vec<&ItemId> {
392 let mut ids = Vec::new();
393 ids.extend(self.is_refined_by.iter());
394 ids.extend(self.derives.iter());
395 ids.extend(self.is_satisfied_by.iter());
396 ids
397 }
398
399 pub fn is_empty(&self) -> bool {
401 self.is_refined_by.is_empty() && self.derives.is_empty() && self.is_satisfied_by.is_empty()
402 }
403}
404
405#[derive(Debug, Clone, Default, Serialize, Deserialize)]
407pub struct ItemAttributes {
408 #[serde(default, skip_serializing_if = "Option::is_none")]
410 pub specification: Option<String>,
411
412 #[serde(default, skip_serializing_if = "Vec::is_empty")]
414 pub depends_on: Vec<ItemId>,
415
416 #[serde(default, skip_serializing_if = "Option::is_none")]
418 pub platform: Option<String>,
419
420 #[serde(default, skip_serializing_if = "Option::is_none")]
422 pub justified_by: Option<Vec<ItemId>>,
423}
424
425use crate::model::metadata::SourceLocation;
426
427#[derive(Debug, Clone, Serialize, Deserialize)]
429pub struct Item {
430 pub id: ItemId,
432
433 pub item_type: ItemType,
435
436 pub name: String,
438
439 #[serde(default, skip_serializing_if = "Option::is_none")]
441 pub description: Option<String>,
442
443 pub source: SourceLocation,
445
446 #[serde(default)]
448 pub upstream: UpstreamRefs,
449
450 #[serde(default)]
452 pub downstream: DownstreamRefs,
453
454 #[serde(default)]
456 pub attributes: ItemAttributes,
457}
458
459impl Item {
460 pub fn all_references(&self) -> Vec<&ItemId> {
462 let mut refs = Vec::new();
463 refs.extend(self.upstream.all_ids());
464 refs.extend(self.downstream.all_ids());
465 refs.extend(self.attributes.depends_on.iter());
466 if let Some(justified_by) = &self.attributes.justified_by {
467 refs.extend(justified_by.iter());
468 }
469 refs
470 }
471}
472
473#[derive(Debug, Default)]
475pub struct ItemBuilder {
476 id: Option<ItemId>,
477 item_type: Option<ItemType>,
478 name: Option<String>,
479 description: Option<String>,
480 source: Option<SourceLocation>,
481 upstream: UpstreamRefs,
482 downstream: DownstreamRefs,
483 attributes: ItemAttributes,
484}
485
486impl ItemBuilder {
487 pub fn new() -> Self {
489 Self::default()
490 }
491
492 pub fn id(mut self, id: ItemId) -> Self {
494 self.id = Some(id);
495 self
496 }
497
498 pub fn item_type(mut self, item_type: ItemType) -> Self {
500 self.item_type = Some(item_type);
501 self
502 }
503
504 pub fn name(mut self, name: impl Into<String>) -> Self {
506 self.name = Some(name.into());
507 self
508 }
509
510 pub fn description(mut self, desc: impl Into<String>) -> Self {
512 self.description = Some(desc.into());
513 self
514 }
515
516 pub fn source(mut self, source: SourceLocation) -> Self {
518 self.source = Some(source);
519 self
520 }
521
522 pub fn upstream(mut self, upstream: UpstreamRefs) -> Self {
524 self.upstream = upstream;
525 self
526 }
527
528 pub fn downstream(mut self, downstream: DownstreamRefs) -> Self {
530 self.downstream = downstream;
531 self
532 }
533
534 pub fn specification(mut self, spec: impl Into<String>) -> Self {
536 self.attributes.specification = Some(spec.into());
537 self
538 }
539
540 pub fn platform(mut self, platform: impl Into<String>) -> Self {
542 self.attributes.platform = Some(platform.into());
543 self
544 }
545
546 pub fn depends_on(mut self, id: ItemId) -> Self {
548 self.attributes.depends_on.push(id);
549 self
550 }
551
552 pub fn attributes(mut self, attrs: ItemAttributes) -> Self {
554 self.attributes = attrs;
555 self
556 }
557
558 pub fn build(self) -> Result<Item, ValidationError> {
560 let id = self.id.ok_or_else(|| ValidationError::MissingField {
561 field: "id".to_string(),
562 file: self
563 .source
564 .as_ref()
565 .map(|s| s.file_path.display().to_string())
566 .unwrap_or_default(),
567 })?;
568
569 let item_type = self
570 .item_type
571 .ok_or_else(|| ValidationError::MissingField {
572 field: "type".to_string(),
573 file: self
574 .source
575 .as_ref()
576 .map(|s| s.file_path.display().to_string())
577 .unwrap_or_default(),
578 })?;
579
580 let name = self.name.ok_or_else(|| ValidationError::MissingField {
581 field: "name".to_string(),
582 file: self
583 .source
584 .as_ref()
585 .map(|s| s.file_path.display().to_string())
586 .unwrap_or_default(),
587 })?;
588
589 let source = self.source.ok_or_else(|| ValidationError::MissingField {
590 field: "source".to_string(),
591 file: String::new(),
592 })?;
593
594 if item_type.requires_specification() && self.attributes.specification.is_none() {
596 return Err(ValidationError::MissingField {
597 field: "specification".to_string(),
598 file: source.file_path.display().to_string(),
599 });
600 }
601
602 Ok(Item {
603 id,
604 item_type,
605 name,
606 description: self.description,
607 source,
608 upstream: self.upstream,
609 downstream: self.downstream,
610 attributes: self.attributes,
611 })
612 }
613}
614
615#[cfg(test)]
616mod tests {
617 use super::*;
618 use std::path::PathBuf;
619
620 #[test]
621 fn test_item_id_valid() {
622 assert!(ItemId::new("SOL-001").is_ok());
623 assert!(ItemId::new("UC_002").is_ok());
624 assert!(ItemId::new("SYSREQ-123-A").is_ok());
625 }
626
627 #[test]
628 fn test_item_id_invalid() {
629 assert!(ItemId::new("").is_err());
630 assert!(ItemId::new("SOL 001").is_err());
631 assert!(ItemId::new("SOL.001").is_err());
632 }
633
634 #[test]
635 fn test_item_type_display() {
636 assert_eq!(ItemType::Solution.display_name(), "Solution");
637 assert_eq!(
638 ItemType::SystemRequirement.display_name(),
639 "System Requirement"
640 );
641 }
642
643 #[test]
644 fn test_item_type_requires_specification() {
645 assert!(ItemType::SystemRequirement.requires_specification());
646 assert!(ItemType::HardwareRequirement.requires_specification());
647 assert!(ItemType::SoftwareRequirement.requires_specification());
648 assert!(!ItemType::Solution.requires_specification());
649 assert!(!ItemType::Scenario.requires_specification());
650 }
651
652 #[test]
653 fn test_item_builder() {
654 let source = SourceLocation {
655 repository: PathBuf::from("/repo"),
656 file_path: PathBuf::from("docs/SOL-001.md"),
657 git_ref: None,
658 };
659
660 let item = ItemBuilder::new()
661 .id(ItemId::new_unchecked("SOL-001"))
662 .item_type(ItemType::Solution)
663 .name("Test Solution")
664 .source(source)
665 .build();
666
667 assert!(item.is_ok());
668 let item = item.unwrap();
669 assert_eq!(item.id.as_str(), "SOL-001");
670 assert_eq!(item.name, "Test Solution");
671 }
672}