use std::fmt;
use std::path::Path;
use std::str::FromStr;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
#[non_exhaustive]
pub enum Goal {
#[default]
Module,
Script,
}
impl Goal {
#[doc(hidden)]
#[must_use]
pub const fn source_type(self) -> &'static str {
match self {
Goal::Module => "module",
Goal::Script => "script",
}
}
#[must_use]
pub fn from_extension(path: impl AsRef<Path>) -> Option<Goal> {
path.as_ref()
.extension()
.is_some_and(|ext| ext.eq_ignore_ascii_case("mjs") || ext.eq_ignore_ascii_case("mts"))
.then_some(Goal::Module)
}
}
impl FromStr for Goal {
type Err = ParseGoalError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s {
"module" => Ok(Goal::Module),
"script" => Ok(Goal::Script),
_ => Err(ParseGoalError),
}
}
}
impl fmt::Display for Goal {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(self.source_type())
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[non_exhaustive]
pub struct ParseGoalError;
impl fmt::Display for ParseGoalError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str("expected 'script' or 'module'")
}
}
impl std::error::Error for ParseGoalError {}
#[cfg(test)]
mod tests {
use super::Goal;
#[test]
fn source_type_round_trips() {
for goal in [Goal::Module, Goal::Script] {
assert_eq!(goal.source_type().parse::<Goal>(), Ok(goal));
assert_eq!(goal.to_string(), goal.source_type());
}
}
#[test]
fn from_extension_settles_the_module_names_in_any_case() {
for module in ["a.mjs", "a.mts", "A.MJS", "dir/x.Mts", "a.svelte.mjs"] {
assert_eq!(Goal::from_extension(module), Some(Goal::Module), "{module}");
}
for unsettled in [
"a.js", "a.ts", "a.cjs", "a.cts", "mjs", ".mjs", "a.mjsx", "a.svelte",
] {
assert_eq!(Goal::from_extension(unsettled), None, "{unsettled}");
}
}
#[test]
fn from_str_rejects_unknown() {
assert!("sloppy".parse::<Goal>().is_err());
assert!("Module".parse::<Goal>().is_err()); assert!("".parse::<Goal>().is_err());
}
#[test]
fn default_is_module() {
assert_eq!(Goal::default(), Goal::Module);
}
#[test]
fn only_the_module_only_extensions_settle_a_goal() {
for path in ["a.mjs", "a.mts", "deep/dir/a.mjs", "a.config.mjs"] {
assert_eq!(
Goal::from_extension(path),
Some(Goal::Module),
"{path} is an ES module by its own extension"
);
}
for path in [
"a.js", "a.mjsx", "a.ts", "a.cjs", "a.cts", "a.svelte", "a.css", "amjs", "a", "",
] {
assert_eq!(
Goal::from_extension(path),
None,
"{path} settles no goal — the caller's default stands"
);
}
}
}