1use std::fmt;
8use std::path::Path;
9
10use garde::Validate;
11use serde::de::DeserializeOwned;
12use serde_saphyr::{DuplicateKeyPolicy, MergeKeyPolicy};
13
14#[derive(Clone, Debug, PartialEq, Eq)]
16pub struct Input {
17 name: String,
18 text: String,
19 lines_before: usize,
20}
21
22#[derive(Clone, Debug, PartialEq, Eq)]
25pub struct InputError {
26 pub name: String,
28 pub problems: Vec<Problem>,
30}
31
32#[derive(Clone, Debug, PartialEq, Eq)]
34pub struct Problem {
35 pub line: Option<usize>,
37 pub column: Option<usize>,
39 pub field: String,
42 pub message: String,
44}
45
46impl Input {
47 #[must_use]
49 pub fn new(name: impl Into<String>, text: impl Into<String>) -> Self {
50 Self {
51 name: name.into(),
52 text: text.into(),
53 lines_before: 0,
54 }
55 }
56
57 pub fn read(path: &Path) -> Result<Self, InputError> {
63 let name = path.display().to_string();
64 match std::fs::read_to_string(path) {
65 Ok(text) => Ok(Self::new(name, text)),
66 Err(error) => Err(InputError::whole(name, error.to_string())),
67 }
68 }
69
70 #[must_use]
72 pub fn name(&self) -> &str {
73 &self.name
74 }
75
76 #[must_use]
78 pub fn text(&self) -> &str {
79 &self.text
80 }
81
82 #[must_use]
87 pub fn frontmatter(&self) -> Option<(Self, &str)> {
88 let text = self.text.strip_prefix('\u{feff}').unwrap_or(&self.text);
89 let rest = text
90 .strip_prefix("---\n")
91 .or_else(|| text.strip_prefix("---\r\n"))?;
92 let mut offset = 0;
93 for line in rest.split_inclusive('\n') {
94 if line.trim_end() == "---" {
95 let matter = Self {
96 name: self.name.clone(),
97 text: rest[..offset].to_owned(),
98 lines_before: self.lines_before + 1,
99 };
100 return Some((matter, &rest[offset + line.len()..]));
101 }
102 offset += line.len();
103 }
104 None
105 }
106
107 pub fn parse<T: DeserializeOwned>(&self) -> Result<T, InputError> {
114 let options = serde_saphyr::options! {
115 strict_booleans: true,
116 duplicate_keys: DuplicateKeyPolicy::Error,
117 merge_keys: MergeKeyPolicy::Error,
118 with_snippet: false,
119 };
120 serde_saphyr::from_str_with_options(&self.text, options).map_err(|error| {
121 let location = error.location();
122 InputError {
123 name: self.name.clone(),
124 problems: vec![Problem {
125 line: location
126 .and_then(|at| usize::try_from(at.line()).ok())
127 .map(|line| line + self.lines_before),
128 column: location.and_then(|at| usize::try_from(at.column()).ok()),
129 field: String::new(),
130 message: error.without_snippet().to_string(),
131 }],
132 }
133 })
134 }
135
136 pub fn check<T: Validate + ?Sized>(
144 &self,
145 value: &T,
146 context: &T::Context,
147 ) -> Result<(), InputError> {
148 let Err(report) = value.validate_with(context) else {
149 return Ok(());
150 };
151 let document = yamled::Document::parse(self.text.as_str()).ok();
152 let problems = report
153 .iter()
154 .map(|(path, error)| {
155 let field = path.to_string();
156 let location = document
157 .as_ref()
158 .and_then(|document| document.locate_nearest(&yaml_path(&field)));
159 Problem {
160 line: location.map(|at| at.line + self.lines_before),
161 column: location.map(|at| at.column),
162 field,
163 message: error.message().to_owned(),
164 }
165 })
166 .collect();
167 Err(InputError {
168 name: self.name.clone(),
169 problems,
170 })
171 }
172
173 pub fn load<T>(&self) -> Result<T, InputError>
179 where
180 T: DeserializeOwned + Validate,
181 T::Context: Default,
182 {
183 let value = self.parse::<T>()?;
184 self.check(&value, &T::Context::default())?;
185 Ok(value)
186 }
187}
188
189fn yaml_path(field: &str) -> yamled::Path {
193 let mut path = yamled::Path::root();
194 for part in field.split('.') {
195 let mut pieces = part.split('[');
196 if let Some(key) = pieces.next().filter(|key| !key.is_empty()) {
197 path = path.key(key);
198 }
199 for index in pieces {
200 let index = index.trim_end_matches(']');
201 path = match index.parse::<usize>() {
202 Ok(index) => path.index(index),
203 Err(_) => path.key(index),
204 };
205 }
206 }
207 path
208}
209
210impl InputError {
211 fn whole(name: String, message: String) -> Self {
212 Self {
213 name,
214 problems: vec![Problem {
215 line: None,
216 column: None,
217 field: String::new(),
218 message,
219 }],
220 }
221 }
222}
223
224impl fmt::Display for InputError {
227 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
228 for (index, problem) in self.problems.iter().enumerate() {
229 if index > 0 {
230 writeln!(formatter)?;
231 }
232 write!(formatter, "{}", self.name)?;
233 if let Some(line) = problem.line {
234 write!(formatter, ":{line}")?;
235 if let Some(column) = problem.column {
236 write!(formatter, ":{column}")?;
237 }
238 }
239 if problem.field.is_empty() {
240 write!(formatter, ": {}", problem.message)?;
241 } else {
242 write!(formatter, ": {}: {}", problem.field, problem.message)?;
243 }
244 }
245 Ok(())
246 }
247}
248
249impl std::error::Error for InputError {}