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 is_root(&self) -> bool {
136 matches!(self, ItemType::Solution)
137 }
138
139 pub fn is_leaf(&self) -> bool {
141 matches!(
142 self,
143 ItemType::HardwareDetailedDesign | ItemType::SoftwareDetailedDesign
144 )
145 }
146
147 pub fn required_parent_type(&self) -> Option<ItemType> {
150 match self {
151 ItemType::Solution => None,
152 ItemType::UseCase => Some(ItemType::Solution),
153 ItemType::Scenario => Some(ItemType::UseCase),
154 ItemType::SystemRequirement => Some(ItemType::Scenario),
155 ItemType::SystemArchitecture => Some(ItemType::SystemRequirement),
156 ItemType::HardwareRequirement => Some(ItemType::SystemArchitecture),
157 ItemType::SoftwareRequirement => Some(ItemType::SystemArchitecture),
158 ItemType::HardwareDetailedDesign => Some(ItemType::HardwareRequirement),
159 ItemType::SoftwareDetailedDesign => Some(ItemType::SoftwareRequirement),
160 }
161 }
162
163 pub fn traceability_field(&self) -> Option<FieldName> {
165 match self {
166 ItemType::Solution => None,
167 ItemType::UseCase | ItemType::Scenario => Some(FieldName::Refines),
168 ItemType::SystemRequirement
169 | ItemType::HardwareRequirement
170 | ItemType::SoftwareRequirement => Some(FieldName::DerivesFrom),
171 ItemType::SystemArchitecture
172 | ItemType::HardwareDetailedDesign
173 | ItemType::SoftwareDetailedDesign => Some(FieldName::Satisfies),
174 }
175 }
176
177 pub fn as_str(&self) -> &'static str {
179 match self {
180 ItemType::Solution => "solution",
181 ItemType::UseCase => "use_case",
182 ItemType::Scenario => "scenario",
183 ItemType::SystemRequirement => "system_requirement",
184 ItemType::SystemArchitecture => "system_architecture",
185 ItemType::HardwareRequirement => "hardware_requirement",
186 ItemType::SoftwareRequirement => "software_requirement",
187 ItemType::HardwareDetailedDesign => "hardware_detailed_design",
188 ItemType::SoftwareDetailedDesign => "software_detailed_design",
189 }
190 }
191
192 pub fn traceability_config(&self) -> Option<TraceabilityConfig> {
196 match self {
197 ItemType::Solution => None,
198 ItemType::UseCase => Some(TraceabilityConfig {
199 relationship_field: FieldName::Refines,
200 parent_type: ItemType::Solution,
201 }),
202 ItemType::Scenario => Some(TraceabilityConfig {
203 relationship_field: FieldName::Refines,
204 parent_type: ItemType::UseCase,
205 }),
206 ItemType::SystemRequirement => Some(TraceabilityConfig {
207 relationship_field: FieldName::DerivesFrom,
208 parent_type: ItemType::Scenario,
209 }),
210 ItemType::SystemArchitecture => Some(TraceabilityConfig {
211 relationship_field: FieldName::Satisfies,
212 parent_type: ItemType::SystemRequirement,
213 }),
214 ItemType::HardwareRequirement | ItemType::SoftwareRequirement => {
215 Some(TraceabilityConfig {
216 relationship_field: FieldName::DerivesFrom,
217 parent_type: ItemType::SystemArchitecture,
218 })
219 }
220 ItemType::HardwareDetailedDesign => Some(TraceabilityConfig {
221 relationship_field: FieldName::Satisfies,
222 parent_type: ItemType::HardwareRequirement,
223 }),
224 ItemType::SoftwareDetailedDesign => Some(TraceabilityConfig {
225 relationship_field: FieldName::Satisfies,
226 parent_type: ItemType::SoftwareRequirement,
227 }),
228 }
229 }
230}
231
232#[derive(Debug, Clone, Copy, PartialEq, Eq)]
234pub struct TraceabilityConfig {
235 pub relationship_field: FieldName,
237 pub parent_type: ItemType,
239}
240
241impl fmt::Display for ItemType {
242 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
243 write!(f, "{}", self.display_name())
244 }
245}
246
247#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
249#[serde(transparent)]
250pub struct ItemId(String);
251
252impl ItemId {
253 pub fn new(id: impl Into<String>) -> Result<Self, ValidationError> {
255 let id = id.into();
256 if id.is_empty() {
257 return Err(ValidationError::InvalidId {
258 id: id.clone(),
259 reason: "Item ID cannot be empty".to_string(),
260 });
261 }
262
263 if !id
265 .chars()
266 .all(|c| c.is_alphanumeric() || c == '-' || c == '_')
267 {
268 return Err(ValidationError::InvalidId {
269 id: id.clone(),
270 reason:
271 "Item ID must contain only alphanumeric characters, hyphens, and underscores"
272 .to_string(),
273 });
274 }
275
276 Ok(Self(id))
277 }
278
279 pub fn new_unchecked(id: impl Into<String>) -> Self {
284 Self(id.into())
285 }
286
287 pub fn as_str(&self) -> &str {
289 &self.0
290 }
291}
292
293impl fmt::Display for ItemId {
294 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
295 write!(f, "{}", self.0)
296 }
297}
298
299impl AsRef<str> for ItemId {
300 fn as_ref(&self) -> &str {
301 &self.0
302 }
303}
304
305#[derive(Debug, Clone, Default, Serialize, Deserialize)]
307pub struct UpstreamRefs {
308 #[serde(default, skip_serializing_if = "Vec::is_empty")]
310 pub refines: Vec<ItemId>,
311
312 #[serde(default, skip_serializing_if = "Vec::is_empty")]
314 pub derives_from: Vec<ItemId>,
315
316 #[serde(default, skip_serializing_if = "Vec::is_empty")]
318 pub satisfies: Vec<ItemId>,
319}
320
321impl UpstreamRefs {
322 pub fn all_ids(&self) -> Vec<&ItemId> {
324 let mut ids = Vec::new();
325 ids.extend(self.refines.iter());
326 ids.extend(self.derives_from.iter());
327 ids.extend(self.satisfies.iter());
328 ids
329 }
330
331 pub fn is_empty(&self) -> bool {
333 self.refines.is_empty() && self.derives_from.is_empty() && self.satisfies.is_empty()
334 }
335}
336
337#[derive(Debug, Clone, Default, Serialize, Deserialize)]
339pub struct DownstreamRefs {
340 #[serde(default, skip_serializing_if = "Vec::is_empty")]
342 pub is_refined_by: Vec<ItemId>,
343
344 #[serde(default, skip_serializing_if = "Vec::is_empty")]
346 pub derives: Vec<ItemId>,
347
348 #[serde(default, skip_serializing_if = "Vec::is_empty")]
350 pub is_satisfied_by: Vec<ItemId>,
351}
352
353impl DownstreamRefs {
354 pub fn all_ids(&self) -> Vec<&ItemId> {
356 let mut ids = Vec::new();
357 ids.extend(self.is_refined_by.iter());
358 ids.extend(self.derives.iter());
359 ids.extend(self.is_satisfied_by.iter());
360 ids
361 }
362
363 pub fn is_empty(&self) -> bool {
365 self.is_refined_by.is_empty() && self.derives.is_empty() && self.is_satisfied_by.is_empty()
366 }
367}
368
369#[derive(Debug, Clone, Default, Serialize, Deserialize)]
371pub struct ItemAttributes {
372 #[serde(default, skip_serializing_if = "Option::is_none")]
374 pub specification: Option<String>,
375
376 #[serde(default, skip_serializing_if = "Vec::is_empty")]
378 pub depends_on: Vec<ItemId>,
379
380 #[serde(default, skip_serializing_if = "Option::is_none")]
382 pub platform: Option<String>,
383
384 #[serde(default, skip_serializing_if = "Option::is_none")]
386 pub justified_by: Option<Vec<ItemId>>,
387}
388
389use crate::model::metadata::SourceLocation;
390
391#[derive(Debug, Clone, Serialize, Deserialize)]
393pub struct Item {
394 pub id: ItemId,
396
397 pub item_type: ItemType,
399
400 pub name: String,
402
403 #[serde(default, skip_serializing_if = "Option::is_none")]
405 pub description: Option<String>,
406
407 pub source: SourceLocation,
409
410 #[serde(default)]
412 pub upstream: UpstreamRefs,
413
414 #[serde(default)]
416 pub downstream: DownstreamRefs,
417
418 #[serde(default)]
420 pub attributes: ItemAttributes,
421}
422
423impl Item {
424 pub fn all_references(&self) -> Vec<&ItemId> {
426 let mut refs = Vec::new();
427 refs.extend(self.upstream.all_ids());
428 refs.extend(self.downstream.all_ids());
429 refs.extend(self.attributes.depends_on.iter());
430 if let Some(justified_by) = &self.attributes.justified_by {
431 refs.extend(justified_by.iter());
432 }
433 refs
434 }
435}
436
437#[derive(Debug, Default)]
439pub struct ItemBuilder {
440 id: Option<ItemId>,
441 item_type: Option<ItemType>,
442 name: Option<String>,
443 description: Option<String>,
444 source: Option<SourceLocation>,
445 upstream: UpstreamRefs,
446 downstream: DownstreamRefs,
447 attributes: ItemAttributes,
448}
449
450impl ItemBuilder {
451 pub fn new() -> Self {
453 Self::default()
454 }
455
456 pub fn id(mut self, id: ItemId) -> Self {
458 self.id = Some(id);
459 self
460 }
461
462 pub fn item_type(mut self, item_type: ItemType) -> Self {
464 self.item_type = Some(item_type);
465 self
466 }
467
468 pub fn name(mut self, name: impl Into<String>) -> Self {
470 self.name = Some(name.into());
471 self
472 }
473
474 pub fn description(mut self, desc: impl Into<String>) -> Self {
476 self.description = Some(desc.into());
477 self
478 }
479
480 pub fn source(mut self, source: SourceLocation) -> Self {
482 self.source = Some(source);
483 self
484 }
485
486 pub fn upstream(mut self, upstream: UpstreamRefs) -> Self {
488 self.upstream = upstream;
489 self
490 }
491
492 pub fn downstream(mut self, downstream: DownstreamRefs) -> Self {
494 self.downstream = downstream;
495 self
496 }
497
498 pub fn specification(mut self, spec: impl Into<String>) -> Self {
500 self.attributes.specification = Some(spec.into());
501 self
502 }
503
504 pub fn platform(mut self, platform: impl Into<String>) -> Self {
506 self.attributes.platform = Some(platform.into());
507 self
508 }
509
510 pub fn depends_on(mut self, id: ItemId) -> Self {
512 self.attributes.depends_on.push(id);
513 self
514 }
515
516 pub fn attributes(mut self, attrs: ItemAttributes) -> Self {
518 self.attributes = attrs;
519 self
520 }
521
522 pub fn build(self) -> Result<Item, ValidationError> {
524 let id = self.id.ok_or_else(|| ValidationError::MissingField {
525 field: "id".to_string(),
526 file: self
527 .source
528 .as_ref()
529 .map(|s| s.file_path.display().to_string())
530 .unwrap_or_default(),
531 })?;
532
533 let item_type = self
534 .item_type
535 .ok_or_else(|| ValidationError::MissingField {
536 field: "type".to_string(),
537 file: self
538 .source
539 .as_ref()
540 .map(|s| s.file_path.display().to_string())
541 .unwrap_or_default(),
542 })?;
543
544 let name = self.name.ok_or_else(|| ValidationError::MissingField {
545 field: "name".to_string(),
546 file: self
547 .source
548 .as_ref()
549 .map(|s| s.file_path.display().to_string())
550 .unwrap_or_default(),
551 })?;
552
553 let source = self.source.ok_or_else(|| ValidationError::MissingField {
554 field: "source".to_string(),
555 file: String::new(),
556 })?;
557
558 if item_type.requires_specification() && self.attributes.specification.is_none() {
560 return Err(ValidationError::MissingField {
561 field: "specification".to_string(),
562 file: source.file_path.display().to_string(),
563 });
564 }
565
566 Ok(Item {
567 id,
568 item_type,
569 name,
570 description: self.description,
571 source,
572 upstream: self.upstream,
573 downstream: self.downstream,
574 attributes: self.attributes,
575 })
576 }
577}
578
579#[cfg(test)]
580mod tests {
581 use super::*;
582 use std::path::PathBuf;
583
584 #[test]
585 fn test_item_id_valid() {
586 assert!(ItemId::new("SOL-001").is_ok());
587 assert!(ItemId::new("UC_002").is_ok());
588 assert!(ItemId::new("SYSREQ-123-A").is_ok());
589 }
590
591 #[test]
592 fn test_item_id_invalid() {
593 assert!(ItemId::new("").is_err());
594 assert!(ItemId::new("SOL 001").is_err());
595 assert!(ItemId::new("SOL.001").is_err());
596 }
597
598 #[test]
599 fn test_item_type_display() {
600 assert_eq!(ItemType::Solution.display_name(), "Solution");
601 assert_eq!(
602 ItemType::SystemRequirement.display_name(),
603 "System Requirement"
604 );
605 }
606
607 #[test]
608 fn test_item_type_requires_specification() {
609 assert!(ItemType::SystemRequirement.requires_specification());
610 assert!(ItemType::HardwareRequirement.requires_specification());
611 assert!(ItemType::SoftwareRequirement.requires_specification());
612 assert!(!ItemType::Solution.requires_specification());
613 assert!(!ItemType::Scenario.requires_specification());
614 }
615
616 #[test]
617 fn test_item_builder() {
618 let source = SourceLocation {
619 repository: PathBuf::from("/repo"),
620 file_path: PathBuf::from("docs/SOL-001.md"),
621 git_ref: None,
622 };
623
624 let item = ItemBuilder::new()
625 .id(ItemId::new_unchecked("SOL-001"))
626 .item_type(ItemType::Solution)
627 .name("Test Solution")
628 .source(source)
629 .build();
630
631 assert!(item.is_ok());
632 let item = item.unwrap();
633 assert_eq!(item.id.as_str(), "SOL-001");
634 assert_eq!(item.name, "Test Solution");
635 }
636}