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::{one_line, Value};
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::{resolve, Layers};
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!(f32::from_value(&Value::Float(f64::INFINITY))
415 .expect("an infinity reads as one")
416 .is_infinite());
417 }
418
419 static PROPS: &[PropMeta] = &[
420 PropMeta {
421 default: Some(Const::Int(4)),
422 envs: &["MYCLI_JOBS"],
423 ..PropMeta::new("jobs", Ty::Uint)
424 },
425 PropMeta {
426 default: Some(Const::Bool(false)),
427 envs: &["MYCLI_RAW"],
428 ..PropMeta::new("raw", Ty::Bool)
429 },
430 PropMeta {
431 envs: &["MYCLI_CACHE_DIR"],
432 ..PropMeta::new("cache_dir", Ty::Option(&Ty::Path))
433 },
434 PropMeta {
437 envs: &["MYCLI_EXCLUDE"],
438 parse: Some(Parser::ListByComma),
439 ..PropMeta::new("exclude", Ty::List(&Ty::String))
440 },
441 PropMeta {
442 envs: &["MYCLI_PORTS"],
443 parse: Some(Parser::ListByComma),
444 ..PropMeta::new("ports", Ty::List(&Ty::Uint))
445 },
446 PropMeta {
447 envs: &["MYCLI_ALIASES"],
448 ..PropMeta::new("aliases", Ty::Map(&Ty::String))
449 },
450 PropMeta {
451 envs: &["MYCLI_RATIO"],
452 ..PropMeta::new("ratio", Ty::Float)
453 },
454 PropMeta::new("profile", Ty::String),
456 ];
457 const REGISTRY: Registry = Registry::new(PROPS);
458
459 fn id(key: &str) -> PropId {
460 REGISTRY.lookup(key).expect("declared").id
461 }
462
463 struct Text(&'static [(&'static str, &'static str)]);
465
466 impl Layer for Text {
467 fn source(&self) -> SourceKind {
468 SourceKind::ENV
469 }
470 fn load(&self, ctx: &LayerCtx) -> Result<LayerOutput, LayerError> {
471 let mut out = LayerOutput::new();
472 for (key, raw) in self.0 {
473 let origin = Origin::new(SourceKind::ENV, format!("MYCLI_{}", key.to_uppercase()));
474 match ctx.entry_for_key(key, raw, origin) {
475 Ok(entry) => out.push(entry),
476 Err(warning) => out.warn(warning),
477 }
478 }
479 Ok(out)
480 }
481 }
482
483 #[test]
484 fn a_resolution_reads_as_the_types_a_struct_holds() {
485 let layer = Text(&[
486 ("jobs", "8"),
487 ("raw", "yes"),
488 ("cache_dir", "/tmp/cache"),
489 ("exclude", "target,dist"),
490 ("ports", "80,443"),
491 ("ratio", "0.5"),
492 ("profile", "release"),
493 ]);
494 let resolved = resolve(REGISTRY, Layers::new().then(&layer)).expect("resolves");
495
496 let mut fold = resolved.fold();
497 let jobs: Option<u64> = fold.required(id("jobs"));
498 let raw: Option<bool> = fold.required(id("raw"));
499 let cache_dir: Option<PathBuf> = fold.optional(id("cache_dir"));
500 let exclude: Option<Vec<String>> = fold.required(id("exclude"));
501 let ports: Option<Vec<u64>> = fold.required(id("ports"));
502 let ratio: Option<f64> = fold.required(id("ratio"));
503 let profile: Option<String> = fold.required(id("profile"));
504 fold.finish().expect("every value fits its field");
505
506 assert_eq!(jobs, Some(8));
507 assert_eq!(raw, Some(true));
508 assert_eq!(cache_dir, Some(PathBuf::from("/tmp/cache")));
509 assert_eq!(
510 exclude,
511 Some(vec!["target".to_string(), "dist".to_string()]),
512 "a list-typed setting keeps the order the file gave it"
513 );
514 assert_eq!(
515 ports,
516 Some(vec![80, 443]),
517 "and reads its items as the type"
518 );
519 assert_eq!(ratio, Some(0.5));
520 assert_eq!(profile, Some("release".to_string()));
521 }
522
523 #[test]
524 fn a_declared_default_is_read_like_any_other_value() {
525 let resolved = resolve(REGISTRY, Layers::new()).expect("resolves");
528 let mut fold = resolved.fold();
529 let jobs: Option<u64> = fold.required(id("jobs"));
530 let cache_dir: Option<PathBuf> = fold.optional(id("cache_dir"));
531 let exclude: Option<Vec<String>> = fold.optional(id("exclude"));
532 assert_eq!(jobs, Some(4));
533 assert_eq!(cache_dir, None, "no default, and absence is not an error");
534 assert_eq!(exclude, None);
535 }
536
537 #[test]
538 fn a_setting_with_no_value_and_no_default_says_which_one() {
539 let resolved = resolve(REGISTRY, Layers::new()).expect("resolves");
543 let mut fold = resolved.fold();
544 let profile: Option<String> = fold.required(id("profile"));
545 assert_eq!(profile, None);
546 let err = fold.finish().expect_err("should not read");
547 assert_eq!(err.to_string(), "profile has no value and no default");
548 }
549
550 #[test]
551 fn a_value_the_field_cannot_hold_names_where_it_came_from() {
552 let mut resolved = resolve(REGISTRY, Layers::new()).expect("resolves");
557 resolved.coerced(id("jobs"), Value::Int(-1), "one job when raw");
558
559 let mut fold = resolved.fold();
560 let jobs: Option<u64> = fold.required(id("jobs"));
561 assert_eq!(jobs, None);
562 let err = fold.finish().expect_err("should not read");
563 assert_eq!(
564 err.to_string(),
565 "jobs expected a non-negative integer but has `-1` (set by one job when raw)"
566 );
567 }
568
569 #[test]
570 fn every_bad_value_is_reported_and_not_only_the_first() {
571 let mut resolved = resolve(REGISTRY, Layers::new()).expect("resolves");
574 resolved.coerced(id("jobs"), Value::Int(-1), "a hook");
575 resolved.coerced(id("raw"), Value::String("sometimes".into()), "a hook");
576 resolved.coerced(
577 id("ports"),
578 Value::List(vec![Value::Int(80), Value::Int(-443)]),
579 "a hook",
580 );
581
582 let mut fold = resolved.fold();
583 let _: Option<u64> = fold.required(id("jobs"));
584 let _: Option<bool> = fold.required(id("raw"));
585 let _: Option<Vec<u64>> = fold.required(id("ports"));
586 let _: Option<String> = fold.required(id("profile"));
587 let err = fold.finish().expect_err("should not read");
588
589 let message = err.to_string();
590 let lines: Vec<&str> = message.lines().collect();
591 assert_eq!(lines.len(), 4, "{err}");
592 assert!(
593 lines[0].starts_with("jobs expected a non-negative integer"),
594 "{err}"
595 );
596 assert!(
597 lines[1].starts_with("raw expected a boolean but has `sometimes`"),
598 "{err}"
599 );
600 assert!(
602 lines[2].starts_with("ports expected a non-negative integer but has `-443`"),
603 "{err}"
604 );
605 assert!(lines[3].starts_with("profile has no value"), "{err}");
606 }
607
608 #[test]
609 fn a_failure_stays_on_its_own_line_whatever_the_value_holds() {
610 let mut resolved = resolve(REGISTRY, Layers::new()).expect("resolves");
614 resolved.coerced(
615 id("jobs"),
616 Value::String("two\nor three".into()),
617 "a hook\nover two lines",
618 );
619 let mut fold = resolved.fold();
620 let _: Option<u64> = fold.required(id("jobs"));
621 let _: Option<String> = fold.required(id("profile"));
622 let err = fold.finish().expect_err("should not read");
623
624 let message = err.to_string();
625 assert_eq!(message.lines().count(), 2, "{message}");
626 assert!(
627 message.starts_with(
628 "jobs expected a non-negative integer but has `two\\nor three` \
629 (set by a hook\\nover two lines)"
630 ),
631 "{message}"
632 );
633 }
634
635 #[test]
636 fn a_failure_about_an_empty_value_still_names_one() {
637 let mut resolved = resolve(REGISTRY, Layers::new()).expect("resolves");
642 resolved.coerced(id("jobs"), Value::List(Vec::new()), "a hook");
643 let mut fold = resolved.fold();
644 let jobs: Option<u64> = fold.required(id("jobs"));
645 assert_eq!(jobs, None);
646 let err = fold.finish().expect_err("a list is not an integer");
647 assert_eq!(
648 err.to_string(),
649 "jobs expected a non-negative integer but has `[]` (set by a hook)"
650 );
651 }
652
653 #[test]
654 fn a_type_only_the_tool_understands_is_read_as_whatever_the_field_says() {
655 static ANY: &[PropMeta] = &[PropMeta::new("either", Ty::Any)];
658 const ANY_REGISTRY: Registry = Registry::new(ANY);
659 let layer = Text(&[]);
660 let mut resolved = resolve(ANY_REGISTRY, Layers::new().then(&layer)).expect("resolves");
661 let id = ANY_REGISTRY.lookup("either").expect("declared").id;
662 resolved.coerced(id, Value::Map(BTreeMap::new()), "a hook");
663
664 let mut fold = resolved.fold();
665 let text: Option<String> = fold.optional(id);
666 assert_eq!(text, None);
667 let err = fold.finish().expect_err("a table is not a string");
668 assert!(
669 err.to_string().starts_with("either expected a string"),
670 "{err}"
671 );
672 }
673
674 #[test]
675 fn a_table_setting_reads_as_a_map_of_the_declared_type() {
676 let layer = Text(&[("aliases", "lts")]);
677 let mut resolved = resolve(REGISTRY, Layers::new().then(&layer)).expect("resolves");
678 resolved.coerced(
679 id("aliases"),
680 Value::Map(
681 [("node".to_string(), Value::from("20"))]
682 .into_iter()
683 .collect(),
684 ),
685 "a hook",
686 );
687 let mut fold = resolved.fold();
688 let aliases: Option<BTreeMap<String, String>> = fold.optional(id("aliases"));
689 fold.finish().expect("reads");
690 assert_eq!(
691 aliases,
692 Some(
693 [("node".to_string(), "20".to_string())]
694 .into_iter()
695 .collect()
696 )
697 );
698 }
699
700 #[test]
701 fn one_setting_can_be_read_without_a_fold() {
702 let layer = Text(&[("jobs", "12")]);
704 let resolved = resolve(REGISTRY, Layers::new().then(&layer)).expect("resolves");
705 assert_eq!(resolved.read::<u64>(id("jobs")), Ok(Some(12)));
706 assert_eq!(resolved.read::<PathBuf>(id("cache_dir")), Ok(None));
707 let err = resolved
708 .read::<bool>(id("jobs"))
709 .expect_err("not a boolean");
710 assert_eq!(
711 err.to_string(),
712 "jobs expected a boolean but has `12` (set by MYCLI_JOBS)"
713 );
714 }
715
716 #[test]
717 fn a_lossy_fold_falls_back_to_the_declared_default_and_still_reports() {
718 let resolved = resolve(REGISTRY, Layers::new().then(&Text(&[]))).expect("resolves");
719 let mut resolved = resolved;
720 resolved.coerced(id("jobs"), Value::Int(-1), "a hook that got it wrong");
722
723 let mut strict = resolved.fold();
726 assert_eq!(strict.required::<u64>(id("jobs")), None);
727 assert_eq!(strict.required::<bool>(id("raw")), Some(false));
728 strict.finish().expect_err("one field did not read");
729
730 let mut lossy = resolved.fold_lossy();
733 assert_eq!(
734 lossy.required::<u64>(id("jobs")),
735 Some(4),
736 "the declared default, not the hook's -1"
737 );
738 assert_eq!(lossy.required::<bool>(id("raw")), Some(false));
739 let errors = lossy.into_errors();
740 assert_eq!(errors.0.len(), 1, "{errors}");
741 assert_eq!(errors.0[0].key, "jobs");
742 }
743
744 #[test]
745 fn a_lossy_fold_does_not_invent_a_default_that_was_never_declared() {
746 let resolved = resolve(REGISTRY, Layers::new().then(&Text(&[]))).expect("resolves");
750 let mut lossy = resolved.fold_lossy();
751 assert_eq!(lossy.required::<String>(id("profile")), None);
752 let errors = lossy.into_errors();
753 assert_eq!(errors.0.len(), 1, "{errors}");
754 assert!(matches!(errors.0[0].kind, ReadErrorKind::Missing));
755 }
756
757 #[test]
758 fn a_lossy_fold_leaves_an_optional_field_empty_when_its_value_is_bad() {
759 let resolved = resolve(
762 REGISTRY,
763 Layers::new().then(&Text(&[("MYCLI_RATIO", "0.5")])),
764 )
765 .expect("resolves");
766 let mut resolved = resolved;
767 resolved.coerced(id("ratio"), Value::from("not a number"), "a hook, again");
768
769 let mut lossy = resolved.fold_lossy();
770 assert_eq!(lossy.optional::<f64>(id("ratio")), None);
771 assert_eq!(lossy.into_errors().0.len(), 1);
772 }
773}