Skip to main content

ought_spec/
types.rs

1use std::path::PathBuf;
2use std::time::Duration;
3
4use serde::{Deserialize, Serialize};
5
6/// A parsed `.ought.md` spec file.
7#[derive(Debug, Clone, Serialize, Deserialize)]
8pub struct Spec {
9    pub name: String,
10    pub metadata: Metadata,
11    pub sections: Vec<Section>,
12    pub source_path: PathBuf,
13}
14
15/// Frontmatter metadata from the top of a spec file.
16#[derive(Debug, Clone, Default, Serialize, Deserialize)]
17pub struct Metadata {
18    pub context: Option<String>,
19    pub sources: Vec<String>,
20    pub schemas: Vec<String>,
21    pub requires: Vec<SpecRef>,
22}
23
24/// A reference to another spec file (from `requires:` or inline links).
25#[derive(Debug, Clone, Serialize, Deserialize)]
26pub struct SpecRef {
27    pub label: String,
28    pub path: PathBuf,
29    pub anchor: Option<String>,
30}
31
32/// A section within a spec (maps to markdown headings).
33#[derive(Debug, Clone, Serialize, Deserialize)]
34pub struct Section {
35    pub title: String,
36    pub depth: u8,
37    pub prose: String,
38    pub clauses: Vec<Clause>,
39    pub subsections: Vec<Section>,
40}
41
42/// The core IR type — a single testable clause.
43#[derive(Debug, Clone, Serialize, Deserialize)]
44pub struct Clause {
45    pub id: ClauseId,
46    pub keyword: Keyword,
47    pub severity: Severity,
48    pub text: String,
49    pub condition: Option<String>,
50    pub otherwise: Vec<Clause>,
51    pub temporal: Option<Temporal>,
52    pub hints: Vec<String>,
53    pub source_location: SourceLocation,
54    pub content_hash: String,
55}
56
57/// Stable identifier for a clause, derived from section path + clause text.
58/// e.g. `auth::login::must_return_jwt`
59#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
60pub struct ClauseId(pub String);
61
62impl std::fmt::Display for ClauseId {
63    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
64        f.write_str(&self.0)
65    }
66}
67
68/// Deontic keyword — the operator on a clause.
69#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
70pub enum Keyword {
71    Must,
72    MustNot,
73    Should,
74    ShouldNot,
75    May,
76    Wont,
77    Given,
78    Otherwise,
79    MustAlways,
80    MustBy,
81}
82
83/// Severity level derived from the keyword.
84#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
85pub enum Severity {
86    Required,
87    Recommended,
88    Optional,
89    NegativeConfirmation,
90}
91
92impl Keyword {
93    pub fn severity(self) -> Severity {
94        match self {
95            Keyword::Must | Keyword::MustNot | Keyword::MustAlways | Keyword::MustBy => {
96                Severity::Required
97            }
98            Keyword::Should | Keyword::ShouldNot => Severity::Recommended,
99            Keyword::May => Severity::Optional,
100            Keyword::Wont => Severity::NegativeConfirmation,
101            Keyword::Given | Keyword::Otherwise => Severity::Required,
102        }
103    }
104}
105
106/// Temporal qualifier for MUST ALWAYS and MUST BY clauses.
107#[derive(Debug, Clone, Serialize, Deserialize)]
108pub enum Temporal {
109    /// Must hold across all states/inputs. Generates property-based tests.
110    Invariant,
111    /// Must complete within the given duration. Generates timed assertions.
112    Deadline(Duration),
113}
114
115/// Location of a clause in a source file.
116#[derive(Debug, Clone, Serialize, Deserialize)]
117pub struct SourceLocation {
118    pub file: PathBuf,
119    pub line: usize,
120}
121
122/// An error encountered during parsing.
123#[derive(Debug, Clone, thiserror::Error)]
124#[error("{file}:{line}: {message}")]
125pub struct ParseError {
126    pub file: PathBuf,
127    pub line: usize,
128    pub message: String,
129}