1use std::fmt;
7use std::str::FromStr;
8
9use serde::{Deserialize, Serialize};
10
11#[derive(
13 Debug, Clone, Copy, Default, PartialEq, Eq, Hash, Serialize, Deserialize, schemars::JsonSchema,
14)]
15#[serde(rename_all = "snake_case")]
16pub enum FormatKind {
17 #[default]
19 Speckit,
20 Openspec,
22 Kiro,
24}
25
26impl FormatKind {
27 pub const ALL: [Self; 3] = [Self::Speckit, Self::Openspec, Self::Kiro];
29
30 #[must_use]
32 pub const fn name(self) -> &'static str {
33 match self {
34 Self::Speckit => "speckit",
35 Self::Openspec => "openspec",
36 Self::Kiro => "kiro",
37 }
38 }
39
40 #[must_use]
42 pub const fn title(self) -> &'static str {
43 match self {
44 Self::Speckit => "Spec Kit",
45 Self::Openspec => "OpenSpec",
46 Self::Kiro => "Kiro",
47 }
48 }
49}
50
51impl FormatKind {
52 #[must_use]
54 pub fn is_reference(&self) -> bool {
55 *self == Self::Speckit
56 }
57}
58
59impl fmt::Display for FormatKind {
60 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
61 f.write_str(self.name())
62 }
63}
64
65impl FromStr for FormatKind {
66 type Err = String;
67
68 fn from_str(s: &str) -> Result<Self, Self::Err> {
69 Self::ALL
70 .into_iter()
71 .find(|k| k.name() == s)
72 .ok_or_else(|| format!("`{s}` is not a format (speckit, openspec or kiro)"))
73 }
74}
75
76#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
78pub struct SourceRange {
79 pub path: String,
81 pub start: usize,
83 pub end: usize,
85}
86
87#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
89pub struct Scenario {
90 pub key: String,
92 pub raw: String,
94 pub source: SourceRange,
96}
97
98#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
101pub struct ScenarioRef {
102 pub id: String,
104 pub text: String,
106}
107
108#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
111pub struct Requirement {
112 pub key: String,
114 pub title: String,
116 pub text: String,
118 pub scenarios: Vec<Scenario>,
120 pub source: SourceRange,
122}
123
124#[derive(Debug, Clone, PartialEq, Eq, Serialize, schemars::JsonSchema)]
126pub struct OpenSpecMeta {
127 pub schema: String,
129 pub context: Option<String>,
131 pub unit_kind: String,
133 pub requirements: Vec<DeltaRequirement>,
135}
136
137#[derive(Debug, Clone, PartialEq, Eq, Serialize, schemars::JsonSchema)]
139pub struct DeltaRequirement {
140 pub capability: String,
142 pub operation: String,
144 pub name: String,
146 pub scenarios: Vec<String>,
148 pub path: String,
150 pub line: u32,
152}
153
154#[cfg(test)]
155mod tests {
156 use super::*;
157
158 #[test]
159 fn formats_parse_and_serialize_by_name() {
160 for k in FormatKind::ALL {
161 assert_eq!(k.name().parse::<FormatKind>(), Ok(k));
162 assert_eq!(serde_json::to_value(k).unwrap_or_default(), k.name());
163 }
164 let err = "bogus".parse::<FormatKind>().unwrap_err();
165 assert_eq!(err, "`bogus` is not a format (speckit, openspec or kiro)");
166 }
167}