1use std::borrow::Cow;
8use std::fmt;
9use std::path::Path;
10
11use garde::Validate;
12use serde::de::DeserializeOwned;
13use serde_saphyr::{
14 DefaultMessageFormatter, DuplicateKeyPolicy, Localizer, Location, MergeKeyPolicy,
15};
16
17#[derive(Clone, Debug, PartialEq, Eq)]
19pub struct Input {
20 name: String,
21 text: String,
22 lines_before: usize,
23}
24
25#[derive(Clone, Debug, PartialEq, Eq)]
28pub struct InputError {
29 pub name: String,
31 pub problems: Vec<Problem>,
33}
34
35#[derive(Clone, Debug, PartialEq, Eq)]
37pub struct Problem {
38 pub line: Option<usize>,
40 pub column: Option<usize>,
42 pub field: String,
45 pub message: String,
47}
48
49impl Input {
50 #[must_use]
52 pub fn new(name: impl Into<String>, text: impl Into<String>) -> Self {
53 Self {
54 name: name.into(),
55 text: text.into(),
56 lines_before: 0,
57 }
58 }
59
60 pub fn read(path: &Path) -> Result<Self, InputError> {
66 let name = path.display().to_string();
67 match std::fs::read_to_string(path) {
68 Ok(text) => Ok(Self::new(name, text)),
69 Err(error) => Err(InputError::whole(name, error.to_string())),
70 }
71 }
72
73 #[must_use]
75 pub fn name(&self) -> &str {
76 &self.name
77 }
78
79 #[must_use]
81 pub fn text(&self) -> &str {
82 &self.text
83 }
84
85 #[must_use]
90 pub fn frontmatter(&self) -> Option<(Self, &str)> {
91 let text = self.text.strip_prefix('\u{feff}').unwrap_or(&self.text);
92 let rest = text
93 .strip_prefix("---\n")
94 .or_else(|| text.strip_prefix("---\r\n"))?;
95 let mut offset = 0;
96 for line in rest.split_inclusive('\n') {
97 if line.trim_end() == "---" {
98 let matter = Self {
99 name: self.name.clone(),
100 text: rest[..offset].to_owned(),
101 lines_before: self.lines_before + 1,
102 };
103 return Some((matter, &rest[offset + line.len()..]));
104 }
105 offset += line.len();
106 }
107 None
108 }
109
110 pub fn parse<T: DeserializeOwned>(&self) -> Result<T, InputError> {
117 let options = serde_saphyr::options! {
118 strict_booleans: true,
119 duplicate_keys: DuplicateKeyPolicy::Error,
120 merge_keys: MergeKeyPolicy::Error,
121 with_snippet: false,
122 };
123 serde_saphyr::from_str_with_options(&self.text, options).map_err(|error| {
124 let location = error.location();
125 InputError {
126 name: self.name.clone(),
127 problems: vec![Problem {
128 line: location
129 .and_then(|at| usize::try_from(at.line()).ok())
130 .map(|line| line + self.lines_before),
131 column: location.and_then(|at| usize::try_from(at.column()).ok()),
132 field: String::new(),
133 message: error
134 .without_snippet()
135 .render_with_formatter(&DefaultMessageFormatter.with_localizer(&Unplaced)),
136 }],
137 }
138 })
139 }
140
141 pub fn check<T: Validate + ?Sized>(
149 &self,
150 value: &T,
151 context: &T::Context,
152 ) -> Result<(), InputError> {
153 let Err(report) = value.validate_with(context) else {
154 return Ok(());
155 };
156 let document = yamled::Document::parse(self.text.as_str()).ok();
157 let problems = report
158 .iter()
159 .map(|(path, error)| {
160 let field = path.to_string();
161 let location = document
162 .as_ref()
163 .and_then(|document| document.locate_nearest(&yaml_path(&field)));
164 Problem {
165 line: location.map(|at| at.line + self.lines_before),
166 column: location.map(|at| at.column),
167 field,
168 message: error.message().to_owned(),
169 }
170 })
171 .collect();
172 Err(InputError {
173 name: self.name.clone(),
174 problems,
175 })
176 }
177
178 pub fn load<T>(&self) -> Result<T, InputError>
184 where
185 T: DeserializeOwned + Validate,
186 T::Context: Default,
187 {
188 let value = self.parse::<T>()?;
189 self.check(&value, &T::Context::default())?;
190 Ok(value)
191 }
192}
193
194struct Unplaced;
198
199impl Localizer for Unplaced {
200 fn attach_location<'a>(&self, base: Cow<'a, str>, _: Location) -> Cow<'a, str> {
201 base
202 }
203}
204
205fn yaml_path(field: &str) -> yamled::Path {
209 let mut path = yamled::Path::root();
210 for part in field.split('.') {
211 let mut pieces = part.split('[');
212 if let Some(key) = pieces.next().filter(|key| !key.is_empty()) {
213 path = path.key(key);
214 }
215 for index in pieces {
216 let index = index.trim_end_matches(']');
217 path = match index.parse::<usize>() {
218 Ok(index) => path.index(index),
219 Err(_) => path.key(index),
220 };
221 }
222 }
223 path
224}
225
226impl InputError {
227 fn whole(name: String, message: String) -> Self {
228 Self {
229 name,
230 problems: vec![Problem {
231 line: None,
232 column: None,
233 field: String::new(),
234 message,
235 }],
236 }
237 }
238}
239
240impl fmt::Display for InputError {
243 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
244 for (index, problem) in self.problems.iter().enumerate() {
245 if index > 0 {
246 writeln!(formatter)?;
247 }
248 write!(formatter, "{}", self.name)?;
249 if let Some(line) = problem.line {
250 write!(formatter, ":{line}")?;
251 if let Some(column) = problem.column {
252 write!(formatter, ":{column}")?;
253 }
254 }
255 if problem.field.is_empty() {
256 write!(formatter, ": {}", problem.message)?;
257 } else {
258 write!(formatter, ": {}: {}", problem.field, problem.message)?;
259 }
260 }
261 Ok(())
262 }
263}
264
265impl std::error::Error for InputError {}