1#![allow(clippy::result_large_err)]
4
5use serde::{Deserialize, Serialize};
6use std::fmt;
7
8use crate::error::ValidationError;
9
10#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
12#[serde(rename_all = "snake_case")]
13pub enum ItemType {
14 Solution,
15 UseCase,
16 Scenario,
17 SystemRequirement,
18 SystemArchitecture,
19 HardwareRequirement,
20 SoftwareRequirement,
21 HardwareDetailedDesign,
22 SoftwareDetailedDesign,
23}
24
25impl ItemType {
26 pub fn all() -> &'static [ItemType] {
28 &[
29 ItemType::Solution,
30 ItemType::UseCase,
31 ItemType::Scenario,
32 ItemType::SystemRequirement,
33 ItemType::SystemArchitecture,
34 ItemType::HardwareRequirement,
35 ItemType::SoftwareRequirement,
36 ItemType::HardwareDetailedDesign,
37 ItemType::SoftwareDetailedDesign,
38 ]
39 }
40
41 pub fn display_name(&self) -> &'static str {
43 match self {
44 ItemType::Solution => "Solution",
45 ItemType::UseCase => "Use Case",
46 ItemType::Scenario => "Scenario",
47 ItemType::SystemRequirement => "System Requirement",
48 ItemType::SystemArchitecture => "System Architecture",
49 ItemType::HardwareRequirement => "Hardware Requirement",
50 ItemType::SoftwareRequirement => "Software Requirement",
51 ItemType::HardwareDetailedDesign => "Hardware Detailed Design",
52 ItemType::SoftwareDetailedDesign => "Software Detailed Design",
53 }
54 }
55
56 pub fn prefix(&self) -> &'static str {
58 match self {
59 ItemType::Solution => "SOL",
60 ItemType::UseCase => "UC",
61 ItemType::Scenario => "SCEN",
62 ItemType::SystemRequirement => "SYSREQ",
63 ItemType::SystemArchitecture => "SYSARCH",
64 ItemType::HardwareRequirement => "HWREQ",
65 ItemType::SoftwareRequirement => "SWREQ",
66 ItemType::HardwareDetailedDesign => "HWDD",
67 ItemType::SoftwareDetailedDesign => "SWDD",
68 }
69 }
70
71 pub fn requires_specification(&self) -> bool {
73 matches!(
74 self,
75 ItemType::SystemRequirement
76 | ItemType::HardwareRequirement
77 | ItemType::SoftwareRequirement
78 )
79 }
80
81 pub fn is_root(&self) -> bool {
83 matches!(self, ItemType::Solution)
84 }
85
86 pub fn is_leaf(&self) -> bool {
88 matches!(
89 self,
90 ItemType::HardwareDetailedDesign | ItemType::SoftwareDetailedDesign
91 )
92 }
93
94 pub fn required_parent_type(&self) -> Option<ItemType> {
97 match self {
98 ItemType::Solution => None,
99 ItemType::UseCase => Some(ItemType::Solution),
100 ItemType::Scenario => Some(ItemType::UseCase),
101 ItemType::SystemRequirement => Some(ItemType::Scenario),
102 ItemType::SystemArchitecture => Some(ItemType::SystemRequirement),
103 ItemType::HardwareRequirement => Some(ItemType::SystemArchitecture),
104 ItemType::SoftwareRequirement => Some(ItemType::SystemArchitecture),
105 ItemType::HardwareDetailedDesign => Some(ItemType::HardwareRequirement),
106 ItemType::SoftwareDetailedDesign => Some(ItemType::SoftwareRequirement),
107 }
108 }
109
110 pub fn traceability_field(&self) -> Option<&'static str> {
112 match self {
113 ItemType::Solution => None,
114 ItemType::UseCase | ItemType::Scenario => Some("refines"),
115 ItemType::SystemRequirement
116 | ItemType::HardwareRequirement
117 | ItemType::SoftwareRequirement => Some("derives_from"),
118 ItemType::SystemArchitecture
119 | ItemType::HardwareDetailedDesign
120 | ItemType::SoftwareDetailedDesign => Some("satisfies"),
121 }
122 }
123
124 pub fn yaml_value(&self) -> &'static str {
126 match self {
127 ItemType::Solution => "solution",
128 ItemType::UseCase => "use_case",
129 ItemType::Scenario => "scenario",
130 ItemType::SystemRequirement => "system_requirement",
131 ItemType::SystemArchitecture => "system_architecture",
132 ItemType::HardwareRequirement => "hardware_requirement",
133 ItemType::SoftwareRequirement => "software_requirement",
134 ItemType::HardwareDetailedDesign => "hardware_detailed_design",
135 ItemType::SoftwareDetailedDesign => "software_detailed_design",
136 }
137 }
138
139 pub fn traceability_config(&self) -> Option<TraceabilityConfig> {
143 match self {
144 ItemType::Solution => None,
145 ItemType::UseCase => Some(TraceabilityConfig {
146 relationship_field: "refines",
147 parent_type: ItemType::Solution,
148 }),
149 ItemType::Scenario => Some(TraceabilityConfig {
150 relationship_field: "refines",
151 parent_type: ItemType::UseCase,
152 }),
153 ItemType::SystemRequirement => Some(TraceabilityConfig {
154 relationship_field: "derives_from",
155 parent_type: ItemType::Scenario,
156 }),
157 ItemType::SystemArchitecture => Some(TraceabilityConfig {
158 relationship_field: "satisfies",
159 parent_type: ItemType::SystemRequirement,
160 }),
161 ItemType::HardwareRequirement | ItemType::SoftwareRequirement => {
162 Some(TraceabilityConfig {
163 relationship_field: "derives_from",
164 parent_type: ItemType::SystemArchitecture,
165 })
166 }
167 ItemType::HardwareDetailedDesign => Some(TraceabilityConfig {
168 relationship_field: "satisfies",
169 parent_type: ItemType::HardwareRequirement,
170 }),
171 ItemType::SoftwareDetailedDesign => Some(TraceabilityConfig {
172 relationship_field: "satisfies",
173 parent_type: ItemType::SoftwareRequirement,
174 }),
175 }
176 }
177}
178
179#[derive(Debug, Clone, Copy, PartialEq, Eq)]
181pub struct TraceabilityConfig {
182 pub relationship_field: &'static str,
184 pub parent_type: ItemType,
186}
187
188impl fmt::Display for ItemType {
189 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
190 write!(f, "{}", self.display_name())
191 }
192}
193
194#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
196#[serde(transparent)]
197pub struct ItemId(String);
198
199impl ItemId {
200 pub fn new(id: impl Into<String>) -> Result<Self, ValidationError> {
202 let id = id.into();
203 if id.is_empty() {
204 return Err(ValidationError::InvalidId {
205 id: id.clone(),
206 reason: "Item ID cannot be empty".to_string(),
207 });
208 }
209
210 if !id
212 .chars()
213 .all(|c| c.is_alphanumeric() || c == '-' || c == '_')
214 {
215 return Err(ValidationError::InvalidId {
216 id: id.clone(),
217 reason:
218 "Item ID must contain only alphanumeric characters, hyphens, and underscores"
219 .to_string(),
220 });
221 }
222
223 Ok(Self(id))
224 }
225
226 pub fn new_unchecked(id: impl Into<String>) -> Self {
231 Self(id.into())
232 }
233
234 pub fn as_str(&self) -> &str {
236 &self.0
237 }
238}
239
240impl fmt::Display for ItemId {
241 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
242 write!(f, "{}", self.0)
243 }
244}
245
246impl AsRef<str> for ItemId {
247 fn as_ref(&self) -> &str {
248 &self.0
249 }
250}
251
252#[derive(Debug, Clone, Default, Serialize, Deserialize)]
254pub struct UpstreamRefs {
255 #[serde(default, skip_serializing_if = "Vec::is_empty")]
257 pub refines: Vec<ItemId>,
258
259 #[serde(default, skip_serializing_if = "Vec::is_empty")]
261 pub derives_from: Vec<ItemId>,
262
263 #[serde(default, skip_serializing_if = "Vec::is_empty")]
265 pub satisfies: Vec<ItemId>,
266}
267
268impl UpstreamRefs {
269 pub fn all_ids(&self) -> Vec<&ItemId> {
271 let mut ids = Vec::new();
272 ids.extend(self.refines.iter());
273 ids.extend(self.derives_from.iter());
274 ids.extend(self.satisfies.iter());
275 ids
276 }
277
278 pub fn is_empty(&self) -> bool {
280 self.refines.is_empty() && self.derives_from.is_empty() && self.satisfies.is_empty()
281 }
282}
283
284#[derive(Debug, Clone, Default, Serialize, Deserialize)]
286pub struct DownstreamRefs {
287 #[serde(default, skip_serializing_if = "Vec::is_empty")]
289 pub is_refined_by: Vec<ItemId>,
290
291 #[serde(default, skip_serializing_if = "Vec::is_empty")]
293 pub derives: Vec<ItemId>,
294
295 #[serde(default, skip_serializing_if = "Vec::is_empty")]
297 pub is_satisfied_by: Vec<ItemId>,
298}
299
300impl DownstreamRefs {
301 pub fn all_ids(&self) -> Vec<&ItemId> {
303 let mut ids = Vec::new();
304 ids.extend(self.is_refined_by.iter());
305 ids.extend(self.derives.iter());
306 ids.extend(self.is_satisfied_by.iter());
307 ids
308 }
309
310 pub fn is_empty(&self) -> bool {
312 self.is_refined_by.is_empty() && self.derives.is_empty() && self.is_satisfied_by.is_empty()
313 }
314}
315
316#[derive(Debug, Clone, Default, Serialize, Deserialize)]
318pub struct ItemAttributes {
319 #[serde(default, skip_serializing_if = "Option::is_none")]
321 pub specification: Option<String>,
322
323 #[serde(default, skip_serializing_if = "Vec::is_empty")]
325 pub depends_on: Vec<ItemId>,
326
327 #[serde(default, skip_serializing_if = "Option::is_none")]
329 pub platform: Option<String>,
330
331 #[serde(default, skip_serializing_if = "Option::is_none")]
333 pub justified_by: Option<Vec<ItemId>>,
334}
335
336use crate::model::metadata::SourceLocation;
337
338#[derive(Debug, Clone, Serialize, Deserialize)]
340pub struct Item {
341 pub id: ItemId,
343
344 pub item_type: ItemType,
346
347 pub name: String,
349
350 #[serde(default, skip_serializing_if = "Option::is_none")]
352 pub description: Option<String>,
353
354 pub source: SourceLocation,
356
357 #[serde(default)]
359 pub upstream: UpstreamRefs,
360
361 #[serde(default)]
363 pub downstream: DownstreamRefs,
364
365 #[serde(default)]
367 pub attributes: ItemAttributes,
368}
369
370impl Item {
371 pub fn all_references(&self) -> Vec<&ItemId> {
373 let mut refs = Vec::new();
374 refs.extend(self.upstream.all_ids());
375 refs.extend(self.downstream.all_ids());
376 refs.extend(self.attributes.depends_on.iter());
377 if let Some(justified_by) = &self.attributes.justified_by {
378 refs.extend(justified_by.iter());
379 }
380 refs
381 }
382}
383
384#[derive(Debug, Default)]
386pub struct ItemBuilder {
387 id: Option<ItemId>,
388 item_type: Option<ItemType>,
389 name: Option<String>,
390 description: Option<String>,
391 source: Option<SourceLocation>,
392 upstream: UpstreamRefs,
393 downstream: DownstreamRefs,
394 attributes: ItemAttributes,
395}
396
397impl ItemBuilder {
398 pub fn new() -> Self {
400 Self::default()
401 }
402
403 pub fn id(mut self, id: ItemId) -> Self {
405 self.id = Some(id);
406 self
407 }
408
409 pub fn item_type(mut self, item_type: ItemType) -> Self {
411 self.item_type = Some(item_type);
412 self
413 }
414
415 pub fn name(mut self, name: impl Into<String>) -> Self {
417 self.name = Some(name.into());
418 self
419 }
420
421 pub fn description(mut self, desc: impl Into<String>) -> Self {
423 self.description = Some(desc.into());
424 self
425 }
426
427 pub fn source(mut self, source: SourceLocation) -> Self {
429 self.source = Some(source);
430 self
431 }
432
433 pub fn upstream(mut self, upstream: UpstreamRefs) -> Self {
435 self.upstream = upstream;
436 self
437 }
438
439 pub fn downstream(mut self, downstream: DownstreamRefs) -> Self {
441 self.downstream = downstream;
442 self
443 }
444
445 pub fn specification(mut self, spec: impl Into<String>) -> Self {
447 self.attributes.specification = Some(spec.into());
448 self
449 }
450
451 pub fn platform(mut self, platform: impl Into<String>) -> Self {
453 self.attributes.platform = Some(platform.into());
454 self
455 }
456
457 pub fn depends_on(mut self, id: ItemId) -> Self {
459 self.attributes.depends_on.push(id);
460 self
461 }
462
463 pub fn attributes(mut self, attrs: ItemAttributes) -> Self {
465 self.attributes = attrs;
466 self
467 }
468
469 pub fn build(self) -> Result<Item, ValidationError> {
471 let id = self.id.ok_or_else(|| ValidationError::MissingField {
472 field: "id".to_string(),
473 file: self
474 .source
475 .as_ref()
476 .map(|s| s.file_path.display().to_string())
477 .unwrap_or_default(),
478 })?;
479
480 let item_type = self
481 .item_type
482 .ok_or_else(|| ValidationError::MissingField {
483 field: "type".to_string(),
484 file: self
485 .source
486 .as_ref()
487 .map(|s| s.file_path.display().to_string())
488 .unwrap_or_default(),
489 })?;
490
491 let name = self.name.ok_or_else(|| ValidationError::MissingField {
492 field: "name".to_string(),
493 file: self
494 .source
495 .as_ref()
496 .map(|s| s.file_path.display().to_string())
497 .unwrap_or_default(),
498 })?;
499
500 let source = self.source.ok_or_else(|| ValidationError::MissingField {
501 field: "source".to_string(),
502 file: String::new(),
503 })?;
504
505 if item_type.requires_specification() && self.attributes.specification.is_none() {
507 return Err(ValidationError::MissingField {
508 field: "specification".to_string(),
509 file: source.file_path.display().to_string(),
510 });
511 }
512
513 Ok(Item {
514 id,
515 item_type,
516 name,
517 description: self.description,
518 source,
519 upstream: self.upstream,
520 downstream: self.downstream,
521 attributes: self.attributes,
522 })
523 }
524}
525
526#[cfg(test)]
527mod tests {
528 use super::*;
529 use std::path::PathBuf;
530
531 #[test]
532 fn test_item_id_valid() {
533 assert!(ItemId::new("SOL-001").is_ok());
534 assert!(ItemId::new("UC_002").is_ok());
535 assert!(ItemId::new("SYSREQ-123-A").is_ok());
536 }
537
538 #[test]
539 fn test_item_id_invalid() {
540 assert!(ItemId::new("").is_err());
541 assert!(ItemId::new("SOL 001").is_err());
542 assert!(ItemId::new("SOL.001").is_err());
543 }
544
545 #[test]
546 fn test_item_type_display() {
547 assert_eq!(ItemType::Solution.display_name(), "Solution");
548 assert_eq!(
549 ItemType::SystemRequirement.display_name(),
550 "System Requirement"
551 );
552 }
553
554 #[test]
555 fn test_item_type_requires_specification() {
556 assert!(ItemType::SystemRequirement.requires_specification());
557 assert!(ItemType::HardwareRequirement.requires_specification());
558 assert!(ItemType::SoftwareRequirement.requires_specification());
559 assert!(!ItemType::Solution.requires_specification());
560 assert!(!ItemType::Scenario.requires_specification());
561 }
562
563 #[test]
564 fn test_item_builder() {
565 let source = SourceLocation {
566 repository: PathBuf::from("/repo"),
567 file_path: PathBuf::from("docs/SOL-001.md"),
568 line: 1,
569 git_ref: None,
570 };
571
572 let item = ItemBuilder::new()
573 .id(ItemId::new_unchecked("SOL-001"))
574 .item_type(ItemType::Solution)
575 .name("Test Solution")
576 .source(source)
577 .build();
578
579 assert!(item.is_ok());
580 let item = item.unwrap();
581 assert_eq!(item.id.as_str(), "SOL-001");
582 assert_eq!(item.name, "Test Solution");
583 }
584}