1use std::collections::BTreeMap;
22use std::fmt;
23use std::path::PathBuf;
24
25use crate::registry::PropId;
26use crate::resolve::Resolved;
27use crate::source::Origin;
28use crate::ty::TypeError;
29use crate::value::{Value, one_line};
30
31pub trait FromValue: Sized {
37 fn from_value(value: &Value) -> Result<Self, TypeError>;
39}
40
41#[derive(Debug, Clone, PartialEq)]
43pub struct ReadError {
44 pub key: &'static str,
46 pub origin: Option<Origin>,
49 pub kind: ReadErrorKind,
50}
51
52#[derive(Debug, Clone, PartialEq)]
54pub enum ReadErrorKind {
55 Type(TypeError),
57 Missing,
59}
60
61impl fmt::Display for ReadError {
62 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
63 match &self.kind {
64 ReadErrorKind::Type(err) => write!(
73 f,
74 "{} expected {} but has `{}`",
75 self.key,
76 err.expected,
77 one_line(&err.found)
78 )?,
79 ReadErrorKind::Missing => write!(f, "{} has no value and no default", self.key)?,
80 }
81 if let Some(origin) = &self.origin {
82 write!(f, " (set by {})", one_line(origin.describe()))?;
83 }
84 Ok(())
85 }
86}
87
88#[derive(Debug, Clone, PartialEq)]
90pub struct ReadErrors(pub Vec<ReadError>);
91
92impl fmt::Display for ReadErrors {
93 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
94 for (i, error) in self.0.iter().enumerate() {
95 if i > 0 {
96 f.write_str("\n")?;
97 }
98 write!(f, "{error}")?;
99 }
100 Ok(())
101 }
102}
103
104impl std::error::Error for ReadErrors {}
105
106pub struct Fold<'a> {
111 resolved: &'a Resolved,
112 errors: Vec<ReadError>,
113 lossy: bool,
115}
116
117impl Resolved {
118 pub fn fold(&self) -> Fold<'_> {
120 Fold {
121 resolved: self,
122 errors: Vec::new(),
123 lossy: false,
124 }
125 }
126
127 pub fn fold_lossy(&self) -> Fold<'_> {
139 Fold {
140 resolved: self,
141 errors: Vec::new(),
142 lossy: true,
143 }
144 }
145
146 pub fn read<T: FromValue>(&self, id: PropId) -> Result<Option<T>, ReadError> {
151 let mut fold = self.fold();
152 let value = fold.optional(id);
153 match fold.errors.pop() {
154 Some(error) => Err(error),
155 None => Ok(value),
156 }
157 }
158}
159
160impl Fold<'_> {
161 pub fn optional<T: FromValue>(&mut self, id: PropId) -> Option<T> {
166 let value = self.resolved.get(id)?;
167 match T::from_value(value) {
168 Ok(value) => Some(value),
169 Err(err) => {
170 self.errors.push(ReadError {
171 key: self.resolved.registry().get(id).key,
172 origin: self.resolved.origin(id).cloned(),
173 kind: ReadErrorKind::Type(err),
174 });
175 self.fallback(id)
176 }
177 }
178 }
179
180 pub fn required<T: FromValue>(&mut self, id: PropId) -> Option<T> {
186 if self.resolved.get(id).is_none() {
187 self.errors.push(ReadError {
188 key: self.resolved.registry().get(id).key,
189 origin: None,
190 kind: ReadErrorKind::Missing,
191 });
192 return None;
196 }
197 self.optional(id)
198 }
199
200 fn fallback<T: FromValue>(&self, id: PropId) -> Option<T> {
206 if !self.lossy {
207 return None;
208 }
209 let default = self.resolved.registry().get(id).default?;
210 T::from_value(&default.to_value()).ok()
211 }
212
213 pub fn errors(&self) -> &[ReadError] {
215 &self.errors
216 }
217
218 pub fn finish(self) -> Result<(), ReadErrors> {
220 if self.errors.is_empty() {
221 Ok(())
222 } else {
223 Err(ReadErrors(self.errors))
224 }
225 }
226
227 pub fn into_errors(self) -> ReadErrors {
232 ReadErrors(self.errors)
233 }
234}
235
236fn mismatch(expected: &'static str, value: &Value) -> TypeError {
238 TypeError {
239 expected,
240 found: crate::value::shown(value),
243 }
244}
245
246impl FromValue for Value {
247 fn from_value(value: &Value) -> Result<Self, TypeError> {
251 Ok(value.clone())
252 }
253}
254
255impl FromValue for bool {
256 fn from_value(value: &Value) -> Result<Self, TypeError> {
257 match value {
258 Value::Bool(b) => Ok(*b),
259 other => Err(mismatch("a boolean", other)),
260 }
261 }
262}
263
264impl FromValue for i64 {
265 fn from_value(value: &Value) -> Result<Self, TypeError> {
266 match value {
267 Value::Int(i) => Ok(*i),
268 other => Err(mismatch("an integer", other)),
269 }
270 }
271}
272
273impl FromValue for u64 {
274 fn from_value(value: &Value) -> Result<Self, TypeError> {
275 match value {
276 Value::Int(i) => {
280 Self::try_from(*i).map_err(|_| mismatch("a non-negative integer", value))
281 }
282 other => Err(mismatch("a non-negative integer", other)),
283 }
284 }
285}
286
287impl FromValue for f64 {
288 fn from_value(value: &Value) -> Result<Self, TypeError> {
289 match value {
290 Value::Float(f) => Ok(*f),
291 Value::Int(i) => Ok(*i as Self),
294 other => Err(mismatch("a number", other)),
295 }
296 }
297}
298
299impl FromValue for f32 {
300 fn from_value(value: &Value) -> Result<Self, TypeError> {
301 let wide = f64::from_value(value)?;
302 let narrow = wide as Self;
303 if narrow.is_infinite() && wide.is_finite() {
307 return Err(mismatch("a number that fits 32 bits", value));
308 }
309 Ok(narrow)
310 }
311}
312
313macro_rules! narrower_int {
317 ($($ty:ty => $expected:literal,)*) => {$(
318 impl FromValue for $ty {
319 fn from_value(value: &Value) -> Result<Self, TypeError> {
320 match value {
321 Value::Int(i) => {
322 Self::try_from(*i).map_err(|_| mismatch($expected, value))
323 }
324 other => Err(mismatch($expected, other)),
325 }
326 }
327 }
328 )*};
329}
330
331narrower_int! {
332 u8 => "a non-negative integer that fits 8 bits",
333 u16 => "a non-negative integer that fits 16 bits",
334 u32 => "a non-negative integer that fits 32 bits",
335 usize => "a non-negative integer",
336 i8 => "an integer that fits 8 bits",
337 i16 => "an integer that fits 16 bits",
338 i32 => "an integer that fits 32 bits",
339 isize => "an integer",
340}
341
342impl FromValue for String {
343 fn from_value(value: &Value) -> Result<Self, TypeError> {
344 match value {
345 Value::String(s) => Ok(s.clone()),
346 other => Err(mismatch("a string", other)),
347 }
348 }
349}
350
351impl FromValue for PathBuf {
352 fn from_value(value: &Value) -> Result<Self, TypeError> {
353 match value {
354 Value::String(s) => Ok(Self::from(s)),
355 other => Err(mismatch("a path", other)),
356 }
357 }
358}
359
360impl<T: FromValue> FromValue for Vec<T> {
361 fn from_value(value: &Value) -> Result<Self, TypeError> {
362 match value {
363 Value::List(items) => items.iter().map(T::from_value).collect(),
367 other => Err(mismatch("a list", other)),
368 }
369 }
370}
371
372impl<T: FromValue> FromValue for BTreeMap<String, T> {
373 fn from_value(value: &Value) -> Result<Self, TypeError> {
374 match value {
375 Value::Map(entries) => entries
376 .iter()
377 .map(|(key, value)| T::from_value(value).map(|value| (key.clone(), value)))
378 .collect(),
379 other => Err(mismatch("a table", other)),
380 }
381 }
382}
383
384#[cfg(test)]
385mod tests {
386 use super::*;
387 use crate::layer::{Layer, LayerCtx, LayerError, LayerOutput};
388 use crate::registry::{PropMeta, Registry};
389 use crate::resolve::{Layers, resolve};
390 use crate::source::SourceKind;
391 use crate::ty::{Parser, Ty};
392 use crate::value::Const;
393
394 #[test]
400 fn a_value_too_wide_for_the_field_is_reported_rather_than_wrapped() {
401 u8::from_value(&Value::Int(256)).expect_err("256 does not fit 8 bits");
402 i8::from_value(&Value::Int(-129)).expect_err("-129 does not fit 8 bits");
403 usize::from_value(&Value::Int(-1)).expect_err("-1 is not non-negative");
404 assert_eq!(u8::from_value(&Value::Int(255)).expect("255 fits"), 255);
405
406 f32::from_value(&Value::Float(1e300)).expect_err("1e300 is not an f32");
407 f32::from_value(&Value::Float(-1e300)).expect_err("-1e300 is not an f32");
408 assert_eq!(
410 f32::from_value(&Value::Float(0.1)).expect("0.1 fits"),
411 0.1_f32
412 );
413 assert!(
415 f32::from_value(&Value::Float(f64::INFINITY))
416 .expect("an infinity reads as one")
417 .is_infinite()
418 );
419 }
420
421 static PROPS: &[PropMeta] = &[
422 PropMeta {
423 default: Some(Const::Int(4)),
424 envs: &["MYCLI_JOBS"],
425 ..PropMeta::new("jobs", Ty::Uint)
426 },
427 PropMeta {
428 default: Some(Const::Bool(false)),
429 envs: &["MYCLI_RAW"],
430 ..PropMeta::new("raw", Ty::Bool)
431 },
432 PropMeta {
433 envs: &["MYCLI_CACHE_DIR"],
434 ..PropMeta::new("cache_dir", Ty::Option(&Ty::Path))
435 },
436 PropMeta {
439 envs: &["MYCLI_EXCLUDE"],
440 parse: Some(Parser::ListByComma),
441 ..PropMeta::new("exclude", Ty::List(&Ty::String))
442 },
443 PropMeta {
444 envs: &["MYCLI_PORTS"],
445 parse: Some(Parser::ListByComma),
446 ..PropMeta::new("ports", Ty::List(&Ty::Uint))
447 },
448 PropMeta {
449 envs: &["MYCLI_ALIASES"],
450 ..PropMeta::new("aliases", Ty::Map(&Ty::String))
451 },
452 PropMeta {
453 envs: &["MYCLI_RATIO"],
454 ..PropMeta::new("ratio", Ty::Float)
455 },
456 PropMeta::new("profile", Ty::String),
458 ];
459 const REGISTRY: Registry = Registry::new(PROPS);
460
461 fn id(key: &str) -> PropId {
462 REGISTRY.lookup(key).expect("declared").id
463 }
464
465 struct Text(&'static [(&'static str, &'static str)]);
467
468 impl Layer for Text {
469 fn source(&self) -> SourceKind {
470 SourceKind::ENV
471 }
472 fn load(&self, ctx: &LayerCtx) -> Result<LayerOutput, LayerError> {
473 let mut out = LayerOutput::new();
474 for (key, raw) in self.0 {
475 let origin = Origin::new(SourceKind::ENV, format!("MYCLI_{}", key.to_uppercase()));
476 match ctx.entry_for_key(key, raw, origin) {
477 Ok(entry) => out.push(entry),
478 Err(warning) => out.warn(warning),
479 }
480 }
481 Ok(out)
482 }
483 }
484
485 #[test]
486 fn a_resolution_reads_as_the_types_a_struct_holds() {
487 let layer = Text(&[
488 ("jobs", "8"),
489 ("raw", "yes"),
490 ("cache_dir", "/tmp/cache"),
491 ("exclude", "target,dist"),
492 ("ports", "80,443"),
493 ("ratio", "0.5"),
494 ("profile", "release"),
495 ]);
496 let resolved = resolve(REGISTRY, Layers::new().then(&layer)).expect("resolves");
497
498 let mut fold = resolved.fold();
499 let jobs: Option<u64> = fold.required(id("jobs"));
500 let raw: Option<bool> = fold.required(id("raw"));
501 let cache_dir: Option<PathBuf> = fold.optional(id("cache_dir"));
502 let exclude: Option<Vec<String>> = fold.required(id("exclude"));
503 let ports: Option<Vec<u64>> = fold.required(id("ports"));
504 let ratio: Option<f64> = fold.required(id("ratio"));
505 let profile: Option<String> = fold.required(id("profile"));
506 fold.finish().expect("every value fits its field");
507
508 assert_eq!(jobs, Some(8));
509 assert_eq!(raw, Some(true));
510 assert_eq!(cache_dir, Some(PathBuf::from("/tmp/cache")));
511 assert_eq!(
512 exclude,
513 Some(vec!["target".to_string(), "dist".to_string()]),
514 "a list-typed setting keeps the order the file gave it"
515 );
516 assert_eq!(
517 ports,
518 Some(vec![80, 443]),
519 "and reads its items as the type"
520 );
521 assert_eq!(ratio, Some(0.5));
522 assert_eq!(profile, Some("release".to_string()));
523 }
524
525 #[test]
526 fn a_declared_default_is_read_like_any_other_value() {
527 let resolved = resolve(REGISTRY, Layers::new()).expect("resolves");
530 let mut fold = resolved.fold();
531 let jobs: Option<u64> = fold.required(id("jobs"));
532 let cache_dir: Option<PathBuf> = fold.optional(id("cache_dir"));
533 let exclude: Option<Vec<String>> = fold.optional(id("exclude"));
534 assert_eq!(jobs, Some(4));
535 assert_eq!(cache_dir, None, "no default, and absence is not an error");
536 assert_eq!(exclude, None);
537 }
538
539 #[test]
540 fn a_setting_with_no_value_and_no_default_says_which_one() {
541 let resolved = resolve(REGISTRY, Layers::new()).expect("resolves");
545 let mut fold = resolved.fold();
546 let profile: Option<String> = fold.required(id("profile"));
547 assert_eq!(profile, None);
548 let err = fold.finish().expect_err("should not read");
549 assert_eq!(err.to_string(), "profile has no value and no default");
550 }
551
552 #[test]
553 fn a_value_the_field_cannot_hold_names_where_it_came_from() {
554 let mut resolved = resolve(REGISTRY, Layers::new()).expect("resolves");
559 resolved.coerced(id("jobs"), Value::Int(-1), "one job when raw");
560
561 let mut fold = resolved.fold();
562 let jobs: Option<u64> = fold.required(id("jobs"));
563 assert_eq!(jobs, None);
564 let err = fold.finish().expect_err("should not read");
565 assert_eq!(
566 err.to_string(),
567 "jobs expected a non-negative integer but has `-1` (set by one job when raw)"
568 );
569 }
570
571 #[test]
572 fn every_bad_value_is_reported_and_not_only_the_first() {
573 let mut resolved = resolve(REGISTRY, Layers::new()).expect("resolves");
576 resolved.coerced(id("jobs"), Value::Int(-1), "a hook");
577 resolved.coerced(id("raw"), Value::String("sometimes".into()), "a hook");
578 resolved.coerced(
579 id("ports"),
580 Value::List(vec![Value::Int(80), Value::Int(-443)]),
581 "a hook",
582 );
583
584 let mut fold = resolved.fold();
585 let _: Option<u64> = fold.required(id("jobs"));
586 let _: Option<bool> = fold.required(id("raw"));
587 let _: Option<Vec<u64>> = fold.required(id("ports"));
588 let _: Option<String> = fold.required(id("profile"));
589 let err = fold.finish().expect_err("should not read");
590
591 let message = err.to_string();
592 let lines: Vec<&str> = message.lines().collect();
593 assert_eq!(lines.len(), 4, "{err}");
594 assert!(
595 lines[0].starts_with("jobs expected a non-negative integer"),
596 "{err}"
597 );
598 assert!(
599 lines[1].starts_with("raw expected a boolean but has `sometimes`"),
600 "{err}"
601 );
602 assert!(
604 lines[2].starts_with("ports expected a non-negative integer but has `-443`"),
605 "{err}"
606 );
607 assert!(lines[3].starts_with("profile has no value"), "{err}");
608 }
609
610 #[test]
611 fn a_failure_stays_on_its_own_line_whatever_the_value_holds() {
612 let mut resolved = resolve(REGISTRY, Layers::new()).expect("resolves");
616 resolved.coerced(
617 id("jobs"),
618 Value::String("two\nor three".into()),
619 "a hook\nover two lines",
620 );
621 let mut fold = resolved.fold();
622 let _: Option<u64> = fold.required(id("jobs"));
623 let _: Option<String> = fold.required(id("profile"));
624 let err = fold.finish().expect_err("should not read");
625
626 let message = err.to_string();
627 assert_eq!(message.lines().count(), 2, "{message}");
628 assert!(
629 message.starts_with(
630 "jobs expected a non-negative integer but has `two\\nor three` \
631 (set by a hook\\nover two lines)"
632 ),
633 "{message}"
634 );
635 }
636
637 #[test]
638 fn a_failure_about_an_empty_value_still_names_one() {
639 let mut resolved = resolve(REGISTRY, Layers::new()).expect("resolves");
644 resolved.coerced(id("jobs"), Value::List(Vec::new()), "a hook");
645 let mut fold = resolved.fold();
646 let jobs: Option<u64> = fold.required(id("jobs"));
647 assert_eq!(jobs, None);
648 let err = fold.finish().expect_err("a list is not an integer");
649 assert_eq!(
650 err.to_string(),
651 "jobs expected a non-negative integer but has `[]` (set by a hook)"
652 );
653 }
654
655 #[test]
656 fn a_type_only_the_tool_understands_is_read_as_whatever_the_field_says() {
657 static ANY: &[PropMeta] = &[PropMeta::new("either", Ty::Any)];
660 const ANY_REGISTRY: Registry = Registry::new(ANY);
661 let layer = Text(&[]);
662 let mut resolved = resolve(ANY_REGISTRY, Layers::new().then(&layer)).expect("resolves");
663 let id = ANY_REGISTRY.lookup("either").expect("declared").id;
664 resolved.coerced(id, Value::Map(BTreeMap::new()), "a hook");
665
666 let mut fold = resolved.fold();
667 let text: Option<String> = fold.optional(id);
668 assert_eq!(text, None);
669 let err = fold.finish().expect_err("a table is not a string");
670 assert!(
671 err.to_string().starts_with("either expected a string"),
672 "{err}"
673 );
674 }
675
676 #[test]
677 fn a_table_setting_reads_as_a_map_of_the_declared_type() {
678 let layer = Text(&[("aliases", "lts")]);
679 let mut resolved = resolve(REGISTRY, Layers::new().then(&layer)).expect("resolves");
680 resolved.coerced(
681 id("aliases"),
682 Value::Map(
683 [("node".to_string(), Value::from("20"))]
684 .into_iter()
685 .collect(),
686 ),
687 "a hook",
688 );
689 let mut fold = resolved.fold();
690 let aliases: Option<BTreeMap<String, String>> = fold.optional(id("aliases"));
691 fold.finish().expect("reads");
692 assert_eq!(
693 aliases,
694 Some(
695 [("node".to_string(), "20".to_string())]
696 .into_iter()
697 .collect()
698 )
699 );
700 }
701
702 #[test]
703 fn one_setting_can_be_read_without_a_fold() {
704 let layer = Text(&[("jobs", "12")]);
706 let resolved = resolve(REGISTRY, Layers::new().then(&layer)).expect("resolves");
707 assert_eq!(resolved.read::<u64>(id("jobs")), Ok(Some(12)));
708 assert_eq!(resolved.read::<PathBuf>(id("cache_dir")), Ok(None));
709 let err = resolved
710 .read::<bool>(id("jobs"))
711 .expect_err("not a boolean");
712 assert_eq!(
713 err.to_string(),
714 "jobs expected a boolean but has `12` (set by MYCLI_JOBS)"
715 );
716 }
717
718 #[test]
719 fn a_lossy_fold_falls_back_to_the_declared_default_and_still_reports() {
720 let resolved = resolve(REGISTRY, Layers::new().then(&Text(&[]))).expect("resolves");
721 let mut resolved = resolved;
722 resolved.coerced(id("jobs"), Value::Int(-1), "a hook that got it wrong");
724
725 let mut strict = resolved.fold();
728 assert_eq!(strict.required::<u64>(id("jobs")), None);
729 assert_eq!(strict.required::<bool>(id("raw")), Some(false));
730 strict.finish().expect_err("one field did not read");
731
732 let mut lossy = resolved.fold_lossy();
735 assert_eq!(
736 lossy.required::<u64>(id("jobs")),
737 Some(4),
738 "the declared default, not the hook's -1"
739 );
740 assert_eq!(lossy.required::<bool>(id("raw")), Some(false));
741 let errors = lossy.into_errors();
742 assert_eq!(errors.0.len(), 1, "{errors}");
743 assert_eq!(errors.0[0].key, "jobs");
744 }
745
746 #[test]
747 fn a_lossy_fold_does_not_invent_a_default_that_was_never_declared() {
748 let resolved = resolve(REGISTRY, Layers::new().then(&Text(&[]))).expect("resolves");
752 let mut lossy = resolved.fold_lossy();
753 assert_eq!(lossy.required::<String>(id("profile")), None);
754 let errors = lossy.into_errors();
755 assert_eq!(errors.0.len(), 1, "{errors}");
756 assert!(matches!(errors.0[0].kind, ReadErrorKind::Missing));
757 }
758
759 #[test]
760 fn a_lossy_fold_leaves_an_optional_field_empty_when_its_value_is_bad() {
761 let resolved = resolve(
764 REGISTRY,
765 Layers::new().then(&Text(&[("MYCLI_RATIO", "0.5")])),
766 )
767 .expect("resolves");
768 let mut resolved = resolved;
769 resolved.coerced(id("ratio"), Value::from("not a number"), "a hook, again");
770
771 let mut lossy = resolved.fold_lossy();
772 assert_eq!(lossy.optional::<f64>(id("ratio")), None);
773 assert_eq!(lossy.into_errors().0.len(), 1);
774 }
775}