1use crate::parsing::ast::{LemmaRepository, 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 Volatile,
12 Registry(Arc<LemmaRepository>),
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::Volatile => write!(f, "volatile"),
20 SourceType::Registry(repo) => {
21 if let Some(name) = &repo.name {
22 write!(f, "{}", name)
23 } else {
24 write!(f, "<anonymous registry>")
25 }
26 }
27 }
28 }
29}
30
31#[derive(Debug, Clone, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
35pub struct Source {
36 pub source_type: SourceType,
38
39 pub span: Span,
41}
42
43impl Source {
44 #[must_use]
45 pub fn new(source_type: SourceType, span: Span) -> Self {
46 Self { source_type, span }
47 }
48
49 #[must_use]
51 pub fn text_from<'a>(
52 &self,
53 sources: &'a HashMap<SourceType, String>,
54 ) -> Option<std::borrow::Cow<'a, str>> {
55 let s = sources.get(&self.source_type)?;
56 s.get(self.span.start..self.span.end)
57 .map(std::borrow::Cow::Borrowed)
58 }
59
60 #[must_use]
64 pub fn extract_text(&self, source: &str) -> Option<String> {
65 let bytes = source.as_bytes();
66 if self.span.start < bytes.len() && self.span.end <= bytes.len() {
67 Some(String::from_utf8_lossy(&bytes[self.span.start..self.span.end]).to_string())
68 } else {
69 None
70 }
71 }
72}
73
74#[cfg(test)]
75mod tests {
76 use super::*;
77 use std::collections::HashMap;
78
79 #[test]
80 fn test_extract_text_valid() {
81 let source = "hello world";
82 let span = Span {
83 start: 0,
84 end: 5,
85 line: 1,
86 col: 0,
87 };
88 let loc = Source::new(SourceType::Volatile, span);
89 assert_eq!(loc.extract_text(source), Some("hello".to_string()));
90 }
91
92 #[test]
93 fn test_extract_text_full_string() {
94 let source = "hello world";
95 let span = Span {
96 start: 0,
97 end: 11,
98 line: 1,
99 col: 0,
100 };
101 let loc = Source::new(SourceType::Volatile, span);
102 assert_eq!(loc.extract_text(source), Some("hello world".to_string()));
103 }
104
105 #[test]
106 fn test_extract_text_empty() {
107 let source = "hello world";
108 let span = Span {
109 start: 5,
110 end: 5,
111 line: 1,
112 col: 5,
113 };
114 let loc = Source::new(SourceType::Volatile, span);
115 assert_eq!(loc.extract_text(source), Some("".to_string()));
116 }
117
118 #[test]
119 fn test_extract_text_out_of_bounds_start() {
120 let source = "hello";
121 let span = Span {
122 start: 10,
123 end: 15,
124 line: 1,
125 col: 10,
126 };
127 let loc = Source::new(SourceType::Volatile, span);
128 assert_eq!(loc.extract_text(source), None);
129 }
130
131 #[test]
132 fn test_extract_text_out_of_bounds_end() {
133 let source = "hello";
134 let span = Span {
135 start: 0,
136 end: 10,
137 line: 1,
138 col: 0,
139 };
140 let loc = Source::new(SourceType::Volatile, span);
141 assert_eq!(loc.extract_text(source), None);
142 }
143
144 #[test]
145 fn test_extract_text_unicode() {
146 let source = "hello 世界";
147 let span = Span {
148 start: 6,
149 end: 12,
150 line: 1,
151 col: 6,
152 };
153 let loc = Source::new(SourceType::Volatile, span);
154 assert_eq!(loc.extract_text(source), Some("世界".to_string()));
155 }
156
157 #[test]
158 fn test_text_from() {
159 let mut sources = HashMap::new();
160 let st = SourceType::Path(Arc::new(PathBuf::from("test.lemma")));
161 sources.insert(st.clone(), "hello world".to_string());
162 let loc = Source::new(
163 st,
164 Span {
165 start: 0,
166 end: 5,
167 line: 1,
168 col: 0,
169 },
170 );
171 assert_eq!(loc.text_from(&sources).as_deref(), Some("hello"));
172 }
173}