#![forbid(unsafe_code)]
#![doc = include_str!("../README.md")]
use core::{fmt, str::FromStr};
use std::error::Error;
use use_ecmascript::{EcmaScriptParseError, EcmaScriptTarget};
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
pub struct TypeScriptVersion {
major: u16,
minor: Option<u16>,
patch: Option<u16>,
}
impl TypeScriptVersion {
pub const fn new(
major: u16,
minor: Option<u16>,
patch: Option<u16>,
) -> Result<Self, TypeScriptVersionParseError> {
if major == 0 || (minor.is_none() && patch.is_some()) {
Err(TypeScriptVersionParseError::InvalidVersion)
} else {
Ok(Self {
major,
minor,
patch,
})
}
}
#[must_use]
pub const fn major(self) -> u16 {
self.major
}
#[must_use]
pub const fn minor(self) -> Option<u16> {
self.minor
}
#[must_use]
pub const fn patch(self) -> Option<u16> {
self.patch
}
}
impl fmt::Display for TypeScriptVersion {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
match (self.minor, self.patch) {
(Some(minor), Some(patch)) => write!(formatter, "{}.{}.{}", self.major, minor, patch),
(Some(minor), None) => write!(formatter, "{}.{}", self.major, minor),
(None, _) => write!(formatter, "{}", self.major),
}
}
}
impl FromStr for TypeScriptVersion {
type Err = TypeScriptVersionParseError;
fn from_str(input: &str) -> Result<Self, Self::Err> {
let trimmed = input.trim().trim_start_matches('v');
if trimmed.is_empty() {
return Err(TypeScriptVersionParseError::Empty);
}
let parts = trimmed.split('.').collect::<Vec<_>>();
if parts.len() > 3 || parts.iter().any(|part| part.is_empty()) {
return Err(TypeScriptVersionParseError::InvalidVersion);
}
let major = parse_version_part(parts[0])?;
let minor = parse_optional_version_part(parts.get(1).copied())?;
let patch = parse_optional_version_part(parts.get(2).copied())?;
Self::new(major, minor, patch)
}
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum TypeScriptVersionParseError {
Empty,
InvalidVersion,
}
impl fmt::Display for TypeScriptVersionParseError {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Empty => formatter.write_str("TypeScript version cannot be empty"),
Self::InvalidVersion => formatter.write_str("invalid TypeScript version"),
}
}
}
impl Error for TypeScriptVersionParseError {}
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
pub enum TsModuleResolution {
Classic,
Node,
Node10,
Node16,
NodeNext,
Bundler,
}
impl fmt::Display for TsModuleResolution {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter.write_str(match self {
Self::Classic => "classic",
Self::Node => "node",
Self::Node10 => "node10",
Self::Node16 => "node16",
Self::NodeNext => "nodenext",
Self::Bundler => "bundler",
})
}
}
impl FromStr for TsModuleResolution {
type Err = TsOptionParseError;
fn from_str(input: &str) -> Result<Self, Self::Err> {
let trimmed = input.trim();
if trimmed.is_empty() {
return Err(TsOptionParseError::Empty);
}
match trimmed.to_ascii_lowercase().as_str() {
"classic" => Ok(Self::Classic),
"node" | "nodejs" => Ok(Self::Node),
"node10" => Ok(Self::Node10),
"node16" => Ok(Self::Node16),
"nodenext" | "node_next" | "node-next" => Ok(Self::NodeNext),
"bundler" => Ok(Self::Bundler),
_ => Err(TsOptionParseError::Unknown),
}
}
}
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
pub enum TsTarget {
EcmaScript(EcmaScriptTarget),
Latest,
}
impl fmt::Display for TsTarget {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::EcmaScript(target) => fmt::Display::fmt(target, formatter),
Self::Latest => formatter.write_str("latest"),
}
}
}
impl From<EcmaScriptTarget> for TsTarget {
fn from(value: EcmaScriptTarget) -> Self {
Self::EcmaScript(value)
}
}
impl FromStr for TsTarget {
type Err = TsTargetParseError;
fn from_str(input: &str) -> Result<Self, Self::Err> {
let trimmed = input.trim();
if trimmed.is_empty() {
return Err(TsTargetParseError::Empty);
}
if trimmed.eq_ignore_ascii_case("latest") {
return Ok(Self::Latest);
}
trimmed
.parse::<EcmaScriptTarget>()
.map(Self::EcmaScript)
.map_err(TsTargetParseError::EcmaScript)
}
}
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
pub enum TsStrictness {
Loose,
Strict,
}
impl fmt::Display for TsStrictness {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter.write_str(match self {
Self::Loose => "loose",
Self::Strict => "strict",
})
}
}
impl FromStr for TsStrictness {
type Err = TsOptionParseError;
fn from_str(input: &str) -> Result<Self, Self::Err> {
let trimmed = input.trim();
if trimmed.is_empty() {
return Err(TsOptionParseError::Empty);
}
match trimmed.to_ascii_lowercase().as_str() {
"loose" | "false" | "off" => Ok(Self::Loose),
"strict" | "true" | "on" => Ok(Self::Strict),
_ => Err(TsOptionParseError::Unknown),
}
}
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum TsOptionParseError {
Empty,
Unknown,
}
impl fmt::Display for TsOptionParseError {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Empty => formatter.write_str("TypeScript option cannot be empty"),
Self::Unknown => formatter.write_str("unknown TypeScript option"),
}
}
}
impl Error for TsOptionParseError {}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum TsTargetParseError {
Empty,
EcmaScript(EcmaScriptParseError),
}
impl fmt::Display for TsTargetParseError {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Empty => formatter.write_str("TypeScript target cannot be empty"),
Self::EcmaScript(error) => write!(formatter, "invalid ECMAScript target: {error}"),
}
}
}
impl Error for TsTargetParseError {
fn source(&self) -> Option<&(dyn Error + 'static)> {
match self {
Self::Empty => None,
Self::EcmaScript(error) => Some(error),
}
}
}
fn parse_version_part(input: &str) -> Result<u16, TypeScriptVersionParseError> {
input
.parse::<u16>()
.map_err(|_error| TypeScriptVersionParseError::InvalidVersion)
}
fn parse_optional_version_part(
input: Option<&str>,
) -> Result<Option<u16>, TypeScriptVersionParseError> {
input.map(parse_version_part).transpose()
}
#[cfg(test)]
mod tests {
use super::{TsModuleResolution, TsStrictness, TsTarget, TypeScriptVersion};
#[test]
fn parses_versions() -> Result<(), Box<dyn std::error::Error>> {
let version: TypeScriptVersion = "v5.4.2".parse()?;
assert_eq!(version.major(), 5);
assert_eq!(version.minor(), Some(4));
assert_eq!(version.patch(), Some(2));
assert_eq!(version.to_string(), "5.4.2");
Ok(())
}
#[test]
fn parses_options() -> Result<(), Box<dyn std::error::Error>> {
assert_eq!(
"nodenext".parse::<TsModuleResolution>()?,
TsModuleResolution::NodeNext
);
assert_eq!("es2022".parse::<TsTarget>()?.to_string(), "ES2022");
assert_eq!("strict".parse::<TsStrictness>()?, TsStrictness::Strict);
Ok(())
}
}