1use crate::parsing::ast::Span;
2use std::collections::HashMap;
3use std::fmt;
4use std::path::PathBuf;
5use std::sync::Arc;
6
7#[derive(Debug, Clone, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
8#[serde(rename_all = "snake_case")]
9pub enum SourceType {
10 Path(Arc<PathBuf>),
11 Dependency(String),
12 Volatile,
13}
14
15impl fmt::Display for SourceType {
16 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
17 match self {
18 SourceType::Path(path) => write!(f, "{}", path.display()),
19 SourceType::Dependency(id) => write!(f, "{id}"),
20 SourceType::Volatile => write!(f, "volatile"),
21 }
22 }
23}
24
25impl SourceType {
26 pub fn from_binding_label(label: &str) -> Result<Self, String> {
28 if label.trim().is_empty() {
29 return Err("source label must be non-empty".to_string());
30 }
31 if label.starts_with('@') {
32 return Ok(SourceType::Dependency(label.to_string()));
33 }
34 Ok(SourceType::Path(Arc::new(PathBuf::from(label))))
35 }
36}
37
38#[derive(Debug, Clone, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
42pub struct Source {
43 pub source_type: SourceType,
45
46 pub span: Span,
48}
49
50impl Source {
51 #[must_use]
52 pub fn new(source_type: SourceType, span: Span) -> Self {
53 Self { source_type, span }
54 }
55
56 #[must_use]
58 pub fn text_from<'a>(
59 &self,
60 sources: &'a HashMap<SourceType, String>,
61 ) -> Option<std::borrow::Cow<'a, str>> {
62 let s = sources.get(&self.source_type)?;
63 s.get(self.span.start..self.span.end)
64 .map(std::borrow::Cow::Borrowed)
65 }
66
67 #[must_use]
71 pub fn extract_text(&self, source: &str) -> Option<String> {
72 let bytes = source.as_bytes();
73 if self.span.start < bytes.len() && self.span.end <= bytes.len() {
74 Some(String::from_utf8_lossy(&bytes[self.span.start..self.span.end]).to_string())
75 } else {
76 None
77 }
78 }
79}
80
81#[cfg(test)]
82mod tests {
83 use super::*;
84 use std::collections::HashMap;
85
86 #[test]
87 fn test_extract_text_valid() {
88 let source = "hello world";
89 let span = Span {
90 start: 0,
91 end: 5,
92 line: 1,
93 col: 0,
94 };
95 let loc = Source::new(SourceType::Volatile, span);
96 assert_eq!(loc.extract_text(source), Some("hello".to_string()));
97 }
98
99 #[test]
100 fn test_extract_text_full_string() {
101 let source = "hello world";
102 let span = Span {
103 start: 0,
104 end: 11,
105 line: 1,
106 col: 0,
107 };
108 let loc = Source::new(SourceType::Volatile, span);
109 assert_eq!(loc.extract_text(source), Some("hello world".to_string()));
110 }
111
112 #[test]
113 fn test_extract_text_empty() {
114 let source = "hello world";
115 let span = Span {
116 start: 5,
117 end: 5,
118 line: 1,
119 col: 5,
120 };
121 let loc = Source::new(SourceType::Volatile, span);
122 assert_eq!(loc.extract_text(source), Some("".to_string()));
123 }
124
125 #[test]
126 fn test_extract_text_out_of_bounds_start() {
127 let source = "hello";
128 let span = Span {
129 start: 10,
130 end: 15,
131 line: 1,
132 col: 10,
133 };
134 let loc = Source::new(SourceType::Volatile, span);
135 assert_eq!(loc.extract_text(source), None);
136 }
137
138 #[test]
139 fn test_extract_text_out_of_bounds_end() {
140 let source = "hello";
141 let span = Span {
142 start: 0,
143 end: 10,
144 line: 1,
145 col: 0,
146 };
147 let loc = Source::new(SourceType::Volatile, span);
148 assert_eq!(loc.extract_text(source), None);
149 }
150
151 #[test]
152 fn test_extract_text_unicode() {
153 let source = "hello 世界";
154 let span = Span {
155 start: 6,
156 end: 12,
157 line: 1,
158 col: 6,
159 };
160 let loc = Source::new(SourceType::Volatile, span);
161 assert_eq!(loc.extract_text(source), Some("世界".to_string()));
162 }
163
164 #[test]
165 fn test_text_from() {
166 let mut sources = HashMap::new();
167 let st = SourceType::Path(Arc::new(PathBuf::from("test.lemma")));
168 sources.insert(st.clone(), "hello world".to_string());
169 let loc = Source::new(
170 st,
171 Span {
172 start: 0,
173 end: 5,
174 line: 1,
175 col: 0,
176 },
177 );
178 assert_eq!(loc.text_from(&sources).as_deref(), Some("hello"));
179 }
180
181 #[test]
182 fn from_binding_label_path_and_dependency() {
183 assert_eq!(
184 SourceType::from_binding_label("app.lemma").unwrap(),
185 SourceType::Path(Arc::new(PathBuf::from("app.lemma")))
186 );
187 assert_eq!(
188 SourceType::from_binding_label("@iso/countries").unwrap(),
189 SourceType::Dependency("@iso/countries".to_string())
190 );
191 assert_eq!(
192 SourceType::from_binding_label("volatile").unwrap(),
193 SourceType::Path(Arc::new(PathBuf::from("volatile")))
194 );
195 assert!(SourceType::from_binding_label("").is_err());
196 assert!(SourceType::from_binding_label(" ").is_err());
197 }
198}