1use std::fs;
2use std::io::{self, Read};
3use std::path::PathBuf;
4
5use color_eyre::eyre::{Report, Result, WrapErr};
6use schemaui::{DocumentFormat, parse_document_str};
7use serde_json::Value;
8
9#[derive(Debug)]
10pub enum InputSource {
11 File(PathBuf),
12 Stdin,
13}
14
15pub fn load_value(spec: &str, format: DocumentFormat, label: &str) -> Result<Value> {
16 if spec == "-" {
17 let contents = read_from_source(&InputSource::Stdin)?;
18 return parse_contents(&contents, format, label);
19 }
20
21 let path = PathBuf::from(spec);
22 match read_from_source(&InputSource::File(path.clone())) {
23 Ok(contents) => parse_contents(&contents, format, label),
24 Err(err) => {
25 if is_not_found(&err) {
26 let inline_label = format!("inline {label}");
27 return parse_contents(spec, format, &inline_label);
28 }
29 Err(err.wrap_err(format!("failed to load {label} from {}", path.display())))
30 }
31 }
32}
33
34fn read_from_source(source: &InputSource) -> Result<String> {
35 match source {
36 InputSource::Stdin => {
37 let mut buffer = String::new();
38 io::stdin()
39 .read_to_string(&mut buffer)
40 .wrap_err("failed to read from stdin")?;
41 Ok(buffer)
42 }
43 InputSource::File(path) => fs::read_to_string(path)
44 .wrap_err_with(|| format!("failed to read file {}", path.display())),
45 }
46}
47
48fn parse_contents(contents: &str, format: DocumentFormat, label: &str) -> Result<Value> {
49 match parse_document_str(contents, format) {
50 Ok(value) => Ok(value),
51 Err(primary) => {
52 for candidate in DocumentFormat::available_formats() {
53 if candidate == format {
54 continue;
55 }
56 if let Ok(value) = parse_document_str(contents, candidate) {
57 return Ok(value);
58 }
59 }
60 Err(Report::msg(format!(
61 "failed to parse {label}: tried {} (first error: {primary})",
62 format_list()
63 )))
64 }
65 }
66}
67
68pub fn is_not_found(err: &Report) -> bool {
69 err.downcast_ref::<io::Error>()
70 .is_some_and(|io_err| io_err.kind() == io::ErrorKind::NotFound)
71}
72
73fn format_list() -> &'static str {
74 #[cfg(all(feature = "yaml", feature = "toml"))]
75 {
76 "JSON/YAML/TOML"
77 }
78 #[cfg(all(feature = "yaml", not(feature = "toml")))]
79 {
80 "JSON/YAML"
81 }
82 #[cfg(all(not(feature = "yaml"), feature = "toml"))]
83 {
84 "JSON/TOML"
85 }
86 #[cfg(all(not(feature = "yaml"), not(feature = "toml")))]
87 {
88 "JSON"
89 }
90}