#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum Reader {
Json,
Yaml,
Toml,
Ini,
Dotenv,
Csv,
Tsv,
Text,
}
impl Reader {
pub(crate) const fn name(self) -> &'static str {
match self {
Self::Json => "json",
Self::Yaml => "yaml",
Self::Toml => "toml",
Self::Ini => "ini",
Self::Dotenv => "env",
Self::Csv => "csv",
Self::Tsv => "tsv",
Self::Text => "text",
}
}
}
const ALIASES: [(&str, Reader); 13] = [
("json", Reader::Json),
("jsonc", Reader::Json),
("yaml", Reader::Yaml),
("yml", Reader::Yaml),
("csv", Reader::Csv),
("tsv", Reader::Tsv),
("toml", Reader::Toml),
("ini", Reader::Ini),
("properties", Reader::Ini),
("env", Reader::Dotenv),
("dotenv", Reader::Dotenv),
("text", Reader::Text),
("txt", Reader::Text),
];
pub(crate) const SUPPORTED_FORMATS: [&str; 8] =
["json", "yaml", "csv", "tsv", "toml", "ini", "env", "text"];
pub(crate) const FALLBACK_FORMAT: &str = Reader::Text.name();
fn normalise(value: &str) -> String {
value.trim().trim_start_matches('.').to_lowercase()
}
pub(crate) fn canonical(format: &str) -> Reader {
ALIASES
.iter()
.find(|(alias, _)| *alias == format)
.map_or(Reader::Text, |(_, reader)| *reader)
}
pub(crate) fn resolve_format(format: Option<&str>, filename: Option<&str>) -> &'static str {
resolve_reader(format, filename).name()
}
fn resolve_reader(format: Option<&str>, filename: Option<&str>) -> Reader {
if let Some(name) = format {
let direct = canonical(&normalise(name));
if direct != Reader::Text {
return direct;
}
}
let Some(filename) = filename else {
return Reader::Text;
};
let whole = canonical(&normalise(filename));
if whole != Reader::Text {
return whole;
}
if is_dotenv(&filename.trim().to_lowercase()) {
return Reader::Dotenv;
}
filename
.rsplit_once('.')
.map_or(Reader::Text, |(_, extension)| {
canonical(&normalise(extension))
})
}
fn is_dotenv(name: &str) -> bool {
name == ".env"
|| name.starts_with(".env.")
|| name == "env"
|| name
.strip_suffix(".env")
.is_some_and(|stem| !stem.is_empty())
}
#[cfg(test)]
mod dotenv_tests {
use super::{Reader, resolve_reader as resolve};
#[test]
fn every_dotenv_spelling_resolves() {
for name in [
".env",
".env.local",
".env.production",
".env.test.local",
"app.env",
"env",
] {
assert_eq!(resolve(None, Some(name)), Reader::Dotenv, "{name}");
}
}
#[test]
fn a_name_that_merely_starts_with_env_is_not_dotenv() {
for name in [".envrc", "environment.json", "env.ts", "sender.env.rs"] {
assert_ne!(resolve(None, Some(name)), Reader::Dotenv, "{name}");
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn every_offered_format_resolves_to_itself() {
for format in SUPPORTED_FORMATS {
assert_eq!(resolve_format(Some(format), None), format, "{format}");
}
}
#[test]
fn the_common_aliases_are_honoured() {
for (alias, expected) in [
("jsonc", "json"),
("yml", "yaml"),
("tsv", "tsv"),
("properties", "ini"),
("dotenv", "env"),
] {
assert_eq!(resolve_format(Some(alias), None), expected, "{alias}");
}
}
#[test]
fn a_name_is_normalised_before_it_is_matched() {
assert_eq!(resolve_format(Some(" JSON "), None), "json");
assert_eq!(resolve_format(Some(".toml"), None), "toml");
}
#[test]
fn a_filename_supplies_the_format_when_none_is_named() {
assert_eq!(resolve_format(None, Some("config.toml")), "toml");
assert_eq!(resolve_format(None, Some("data.CSV")), "csv");
}
#[test]
fn a_dotfile_resolves_by_its_whole_name() {
assert_eq!(resolve_format(None, Some(".env")), "env");
assert_eq!(resolve_format(None, Some("env")), "env");
}
#[test]
fn anything_unrecognised_falls_back() {
for name in ["markdown", "dockerfile", "", "wat", "conf", "cfg"] {
assert_eq!(resolve_format(Some(name), None), FALLBACK_FORMAT, "{name}");
}
assert_eq!(resolve_format(None, Some("README.md")), FALLBACK_FORMAT);
assert_eq!(resolve_format(None, Some("main.rs")), FALLBACK_FORMAT);
assert_eq!(resolve_format(None, None), FALLBACK_FORMAT);
}
#[test]
fn an_unresolved_format_defers_to_the_filename() {
assert_eq!(resolve_format(Some("nonsense"), Some("a.toml")), "toml");
}
#[test]
fn the_offered_list_matches_the_alias_table() {
for format in SUPPORTED_FORMATS {
assert!(
ALIASES.iter().any(|(_, reader)| reader.name() == format),
"{format} is offered but no alias produces it"
);
}
for (_, reader) in ALIASES {
assert!(
SUPPORTED_FORMATS.contains(&reader.name()),
"{} is produced but not offered",
reader.name()
);
}
}
#[test]
fn every_reader_is_offered_and_resolves_back_to_itself() {
for reader in [
Reader::Json,
Reader::Yaml,
Reader::Toml,
Reader::Ini,
Reader::Dotenv,
Reader::Csv,
Reader::Tsv,
Reader::Text,
] {
let name = match reader {
Reader::Json => "json",
Reader::Yaml => "yaml",
Reader::Toml => "toml",
Reader::Ini => "ini",
Reader::Dotenv => "env",
Reader::Csv => "csv",
Reader::Tsv => "tsv",
Reader::Text => "text",
};
assert_eq!(reader.name(), name);
assert!(
SUPPORTED_FORMATS.contains(&name),
"{name} is a reader and is not offered"
);
assert_eq!(canonical(name), reader, "{name} does not resolve to itself");
}
}
}