use clap::ValueEnum;
use once_cell::sync::Lazy;
use regex::Regex;
use std::fmt;
use std::path::Path;
#[non_exhaustive]
#[derive(ValueEnum, Debug, PartialEq, Eq, Clone, Copy)]
pub enum Encoding {
Json,
#[clap(alias = "yml")]
Yaml,
Toml,
Json5,
Csv,
#[clap(alias = "qs")]
QueryString,
Xml,
#[clap(alias = "txt")]
Text,
Gron,
Hcl,
}
static FIRST_LINES: Lazy<Vec<(Encoding, Regex)>> = Lazy::new(|| {
vec![
(
Encoding::Xml,
Regex::new(
r#"^(?x:
<\?xml\s
| \s*<(?:[\w-]+):Envelope\s+
| \s*(?i:<!DOCTYPE\s+)
)"#,
)
.unwrap(),
),
(
Encoding::Hcl,
Regex::new(
r#"^(?xi:
[a-z_][a-z0-9_-]*\s+
(?:(?:[a-z_][a-z0-9_-]*|"[^"]*")\s+)*\{
)"#,
)
.unwrap(),
),
(Encoding::Yaml, Regex::new(r"^(?:%YAML.*|---\s*)$").unwrap()),
(
Encoding::Toml,
Regex::new(
r#"^(?xi:
# array of tables
\[\[\s*[a-z0-9_-]+(?:\s*\.\s*(?:[a-z0-9_-]+|"[^"]*"))*\s*\]\]\s*
# table
| \[\s*[a-z0-9_-]+(?:\s*\.\s*(?:[a-z0-9_-]+|"[^"]*"))*\s*\]\s*
)$"#,
)
.unwrap(),
),
(
Encoding::Json,
Regex::new(r#"^(?:\{\s*(?:"|$)|\[\s*$)"#).unwrap(),
),
]
});
impl Encoding {
pub fn from_path<P>(path: P) -> Option<Encoding>
where
P: AsRef<Path>,
{
let ext = path.as_ref().extension()?.to_str()?;
match ext {
"json" => Some(Encoding::Json),
"yaml" | "yml" => Some(Encoding::Yaml),
"toml" => Some(Encoding::Toml),
"json5" => Some(Encoding::Json5),
"csv" => Some(Encoding::Csv),
"xml" => Some(Encoding::Xml),
"txt" | "text" => Some(Encoding::Text),
"hcl" | "tf" => Some(Encoding::Hcl),
_ => None,
}
}
pub fn from_first_line(line: &str) -> Option<Encoding> {
if line.is_empty() {
return None;
}
for (encoding, regex) in FIRST_LINES.iter() {
if regex.is_match(line) {
return Some(*encoding);
}
}
None
}
pub fn as_str(&self) -> &'static str {
match self {
Encoding::Json => "json",
Encoding::Yaml => "yaml",
Encoding::Toml => "toml",
Encoding::Json5 => "json5",
Encoding::Csv => "csv",
Encoding::QueryString => "query-string",
Encoding::Xml => "xml",
Encoding::Text => "text",
Encoding::Gron => "gron",
Encoding::Hcl => "hcl",
}
}
}
impl fmt::Display for Encoding {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
fmt::Display::fmt(self.as_str(), f)
}
}
#[cfg(test)]
mod tests {
use super::*;
use pretty_assertions::assert_eq;
#[test]
fn test_encoding_from_path() {
assert_eq!(Encoding::from_path("foo.yaml"), Some(Encoding::Yaml));
assert_eq!(Encoding::from_path("foo.yml"), Some(Encoding::Yaml));
assert_eq!(Encoding::from_path("foo.json"), Some(Encoding::Json));
assert_eq!(Encoding::from_path("foo.json5"), Some(Encoding::Json5));
assert_eq!(Encoding::from_path("foo.toml"), Some(Encoding::Toml));
assert_eq!(Encoding::from_path("foo.bak"), None);
assert_eq!(Encoding::from_path("foo"), None);
}
#[test]
fn test_encoding_from_first_line() {
assert_eq!(Encoding::from_first_line(""), None);
assert_eq!(Encoding::from_first_line(r#"["foo"]"#), None);
assert_eq!(
Encoding::from_first_line(r#"resource "aws_s3_bucket" "my-bucket" {"#),
Some(Encoding::Hcl)
);
assert_eq!(Encoding::from_first_line("{ "), Some(Encoding::Json));
assert_eq!(Encoding::from_first_line("[ "), Some(Encoding::Json));
assert_eq!(
Encoding::from_first_line(r#"{"foo": 1 }"#),
Some(Encoding::Json)
);
assert_eq!(
Encoding::from_first_line(r#"[foo .bar."baz".qux]"#),
Some(Encoding::Toml)
);
assert_eq!(
Encoding::from_first_line(r#"[[foo .bar."baz".qux]] "#),
Some(Encoding::Toml)
);
assert_eq!(Encoding::from_first_line("%YAML 1.2"), Some(Encoding::Yaml));
assert_eq!(
Encoding::from_first_line("<!doctype html>"),
Some(Encoding::Xml)
);
assert_eq!(
Encoding::from_first_line(r#"<?xml version="1.0" ?>"#),
Some(Encoding::Xml)
);
assert_eq!(
Encoding::from_first_line(
r#"<soap:Envelope xmlns:soap="http://www.w3.org/2003/05/soap-envelope/" soap:encodingStyle="http://www.w3.org/2003/05/soap-encoding">"#
),
Some(Encoding::Xml)
);
}
}