1use serde::{Deserialize, Serialize};
4
5#[derive(Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
7pub struct SourceLocation {
8 pub file: String,
9 pub line: usize,
10 pub column: usize,
11}
12
13impl SourceLocation {
14 pub fn new(file: impl Into<String>, line: usize, column: usize) -> Self {
15 SourceLocation {
16 file: file.into(),
17 line,
18 column,
19 }
20 }
21
22 pub fn unknown() -> Self {
23 SourceLocation {
24 file: "<unknown>".to_string(),
25 line: 0,
26 column: 0,
27 }
28 }
29}
30
31impl std::fmt::Display for SourceLocation {
32 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
33 write!(f, "{}:{}:{}", self.file, self.line, self.column)
34 }
35}
36
37#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
39pub struct SourceSpan {
40 pub start: SourceLocation,
41 pub end: SourceLocation,
42}
43
44impl SourceSpan {
45 pub fn new(start: SourceLocation, end: SourceLocation) -> Self {
46 SourceSpan { start, end }
47 }
48
49 pub fn single(location: SourceLocation) -> Self {
50 SourceSpan {
51 start: location.clone(),
52 end: location,
53 }
54 }
55
56 pub fn unknown() -> Self {
57 SourceSpan {
58 start: SourceLocation::unknown(),
59 end: SourceLocation::unknown(),
60 }
61 }
62}
63
64impl std::fmt::Display for SourceSpan {
65 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
66 if self.start.file == self.end.file {
67 if self.start.line == self.end.line {
68 write!(
69 f,
70 "{}:{}:{}-{}",
71 self.start.file, self.start.line, self.start.column, self.end.column
72 )
73 } else {
74 write!(
75 f,
76 "{} (lines {}-{})",
77 self.start.file, self.start.line, self.end.line
78 )
79 }
80 } else {
81 write!(f, "{} to {}", self.start, self.end)
82 }
83 }
84}
85
86#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
88pub struct Provenance {
89 pub rule_id: Option<String>,
91 pub source_file: Option<String>,
93 pub span: Option<SourceSpan>,
95 pub attributes: Vec<(String, String)>,
97}
98
99impl Provenance {
100 pub fn new() -> Self {
101 Provenance {
102 rule_id: None,
103 source_file: None,
104 span: None,
105 attributes: Vec::new(),
106 }
107 }
108
109 pub fn with_rule_id(mut self, rule_id: impl Into<String>) -> Self {
110 self.rule_id = Some(rule_id.into());
111 self
112 }
113
114 pub fn with_source_file(mut self, source_file: impl Into<String>) -> Self {
115 self.source_file = Some(source_file.into());
116 self
117 }
118
119 pub fn with_span(mut self, span: SourceSpan) -> Self {
120 self.span = Some(span);
121 self
122 }
123
124 pub fn with_attribute(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
125 self.attributes.push((key.into(), value.into()));
126 self
127 }
128
129 pub fn get_attribute(&self, key: &str) -> Option<&str> {
130 self.attributes
131 .iter()
132 .find(|(k, _)| k == key)
133 .map(|(_, v)| v.as_str())
134 }
135}
136
137impl Default for Provenance {
138 fn default() -> Self {
139 Self::new()
140 }
141}
142
143#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
145pub struct Metadata {
146 pub name: Option<String>,
148 pub span: Option<SourceSpan>,
150 pub provenance: Option<Provenance>,
152 pub attributes: Vec<(String, String)>,
154}
155
156impl Metadata {
157 pub fn new() -> Self {
158 Self::default()
159 }
160
161 pub fn with_name(mut self, name: impl Into<String>) -> Self {
162 self.name = Some(name.into());
163 self
164 }
165
166 pub fn with_span(mut self, span: SourceSpan) -> Self {
167 self.span = Some(span);
168 self
169 }
170
171 pub fn with_provenance(mut self, provenance: Provenance) -> Self {
172 self.provenance = Some(provenance);
173 self
174 }
175
176 pub fn with_attribute(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
177 self.attributes.push((key.into(), value.into()));
178 self
179 }
180
181 pub fn get_attribute(&self, key: &str) -> Option<&str> {
182 self.attributes
183 .iter()
184 .find(|(k, _)| k == key)
185 .map(|(_, v)| v.as_str())
186 }
187}
188
189#[cfg(test)]
190mod tests {
191 use super::*;
192
193 #[test]
194 fn test_source_location() {
195 let loc = SourceLocation::new("test.tl", 10, 5);
196 assert_eq!(loc.line, 10);
197 assert_eq!(loc.column, 5);
198 assert_eq!(loc.to_string(), "test.tl:10:5");
199 }
200
201 #[test]
202 fn test_source_span() {
203 let start = SourceLocation::new("test.tl", 10, 5);
204 let end = SourceLocation::new("test.tl", 10, 20);
205 let span = SourceSpan::new(start, end);
206 assert_eq!(span.to_string(), "test.tl:10:5-20");
207 }
208
209 #[test]
210 fn test_provenance() {
211 let prov = Provenance::new()
212 .with_rule_id("rule_1")
213 .with_source_file("test.tl")
214 .with_attribute("author", "system");
215
216 assert_eq!(prov.rule_id, Some("rule_1".to_string()));
217 assert_eq!(prov.get_attribute("author"), Some("system"));
218 }
219
220 #[test]
221 fn test_metadata() {
222 let span = SourceSpan::single(SourceLocation::new("test.tl", 10, 5));
223 let prov = Provenance::new().with_rule_id("rule_1");
224
225 let meta = Metadata::new()
226 .with_name("transitivity")
227 .with_span(span.clone())
228 .with_provenance(prov.clone())
229 .with_attribute("version", "1.0");
230
231 assert_eq!(meta.name, Some("transitivity".to_string()));
232 assert_eq!(meta.span, Some(span));
233 assert_eq!(meta.provenance, Some(prov));
234 assert_eq!(meta.get_attribute("version"), Some("1.0"));
235 }
236}