1#![forbid(unsafe_code)]
2#![doc = include_str!("../README.md")]
3
4use core::{fmt, str::FromStr};
5use std::error::Error;
6use use_ecmascript::{EcmaScriptParseError, EcmaScriptTarget};
7
8#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
10#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
11pub struct TypeScriptVersion {
12 major: u16,
13 minor: Option<u16>,
14 patch: Option<u16>,
15}
16
17impl TypeScriptVersion {
18 pub const fn new(
25 major: u16,
26 minor: Option<u16>,
27 patch: Option<u16>,
28 ) -> Result<Self, TypeScriptVersionParseError> {
29 if major == 0 || (minor.is_none() && patch.is_some()) {
30 Err(TypeScriptVersionParseError::InvalidVersion)
31 } else {
32 Ok(Self {
33 major,
34 minor,
35 patch,
36 })
37 }
38 }
39
40 #[must_use]
42 pub const fn major(self) -> u16 {
43 self.major
44 }
45
46 #[must_use]
48 pub const fn minor(self) -> Option<u16> {
49 self.minor
50 }
51
52 #[must_use]
54 pub const fn patch(self) -> Option<u16> {
55 self.patch
56 }
57}
58
59impl fmt::Display for TypeScriptVersion {
60 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
61 match (self.minor, self.patch) {
62 (Some(minor), Some(patch)) => write!(formatter, "{}.{}.{}", self.major, minor, patch),
63 (Some(minor), None) => write!(formatter, "{}.{}", self.major, minor),
64 (None, _) => write!(formatter, "{}", self.major),
65 }
66 }
67}
68
69impl FromStr for TypeScriptVersion {
70 type Err = TypeScriptVersionParseError;
71
72 fn from_str(input: &str) -> Result<Self, Self::Err> {
73 let trimmed = input.trim().trim_start_matches('v');
74 if trimmed.is_empty() {
75 return Err(TypeScriptVersionParseError::Empty);
76 }
77
78 let parts = trimmed.split('.').collect::<Vec<_>>();
79 if parts.len() > 3 || parts.iter().any(|part| part.is_empty()) {
80 return Err(TypeScriptVersionParseError::InvalidVersion);
81 }
82
83 let major = parse_version_part(parts[0])?;
84 let minor = parse_optional_version_part(parts.get(1).copied())?;
85 let patch = parse_optional_version_part(parts.get(2).copied())?;
86 Self::new(major, minor, patch)
87 }
88}
89
90#[derive(Clone, Copy, Debug, Eq, PartialEq)]
92pub enum TypeScriptVersionParseError {
93 Empty,
94 InvalidVersion,
95}
96
97impl fmt::Display for TypeScriptVersionParseError {
98 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
99 match self {
100 Self::Empty => formatter.write_str("TypeScript version cannot be empty"),
101 Self::InvalidVersion => formatter.write_str("invalid TypeScript version"),
102 }
103 }
104}
105
106impl Error for TypeScriptVersionParseError {}
107
108#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
110#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
111pub enum TsModuleResolution {
112 Classic,
113 Node,
114 Node10,
115 Node16,
116 NodeNext,
117 Bundler,
118}
119
120impl fmt::Display for TsModuleResolution {
121 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
122 formatter.write_str(match self {
123 Self::Classic => "classic",
124 Self::Node => "node",
125 Self::Node10 => "node10",
126 Self::Node16 => "node16",
127 Self::NodeNext => "nodenext",
128 Self::Bundler => "bundler",
129 })
130 }
131}
132
133impl FromStr for TsModuleResolution {
134 type Err = TsOptionParseError;
135
136 fn from_str(input: &str) -> Result<Self, Self::Err> {
137 let trimmed = input.trim();
138 if trimmed.is_empty() {
139 return Err(TsOptionParseError::Empty);
140 }
141
142 match trimmed.to_ascii_lowercase().as_str() {
143 "classic" => Ok(Self::Classic),
144 "node" | "nodejs" => Ok(Self::Node),
145 "node10" => Ok(Self::Node10),
146 "node16" => Ok(Self::Node16),
147 "nodenext" | "node_next" | "node-next" => Ok(Self::NodeNext),
148 "bundler" => Ok(Self::Bundler),
149 _ => Err(TsOptionParseError::Unknown),
150 }
151 }
152}
153
154#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
156#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
157pub enum TsTarget {
158 EcmaScript(EcmaScriptTarget),
159 Latest,
160}
161
162impl fmt::Display for TsTarget {
163 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
164 match self {
165 Self::EcmaScript(target) => fmt::Display::fmt(target, formatter),
166 Self::Latest => formatter.write_str("latest"),
167 }
168 }
169}
170
171impl From<EcmaScriptTarget> for TsTarget {
172 fn from(value: EcmaScriptTarget) -> Self {
173 Self::EcmaScript(value)
174 }
175}
176
177impl FromStr for TsTarget {
178 type Err = TsTargetParseError;
179
180 fn from_str(input: &str) -> Result<Self, Self::Err> {
181 let trimmed = input.trim();
182 if trimmed.is_empty() {
183 return Err(TsTargetParseError::Empty);
184 }
185 if trimmed.eq_ignore_ascii_case("latest") {
186 return Ok(Self::Latest);
187 }
188 trimmed
189 .parse::<EcmaScriptTarget>()
190 .map(Self::EcmaScript)
191 .map_err(TsTargetParseError::EcmaScript)
192 }
193}
194
195#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
197#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
198pub enum TsStrictness {
199 Loose,
200 Strict,
201}
202
203impl fmt::Display for TsStrictness {
204 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
205 formatter.write_str(match self {
206 Self::Loose => "loose",
207 Self::Strict => "strict",
208 })
209 }
210}
211
212impl FromStr for TsStrictness {
213 type Err = TsOptionParseError;
214
215 fn from_str(input: &str) -> Result<Self, Self::Err> {
216 let trimmed = input.trim();
217 if trimmed.is_empty() {
218 return Err(TsOptionParseError::Empty);
219 }
220
221 match trimmed.to_ascii_lowercase().as_str() {
222 "loose" | "false" | "off" => Ok(Self::Loose),
223 "strict" | "true" | "on" => Ok(Self::Strict),
224 _ => Err(TsOptionParseError::Unknown),
225 }
226 }
227}
228
229#[derive(Clone, Copy, Debug, Eq, PartialEq)]
231pub enum TsOptionParseError {
232 Empty,
233 Unknown,
234}
235
236impl fmt::Display for TsOptionParseError {
237 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
238 match self {
239 Self::Empty => formatter.write_str("TypeScript option cannot be empty"),
240 Self::Unknown => formatter.write_str("unknown TypeScript option"),
241 }
242 }
243}
244
245impl Error for TsOptionParseError {}
246
247#[derive(Clone, Copy, Debug, Eq, PartialEq)]
249pub enum TsTargetParseError {
250 Empty,
251 EcmaScript(EcmaScriptParseError),
252}
253
254impl fmt::Display for TsTargetParseError {
255 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
256 match self {
257 Self::Empty => formatter.write_str("TypeScript target cannot be empty"),
258 Self::EcmaScript(error) => write!(formatter, "invalid ECMAScript target: {error}"),
259 }
260 }
261}
262
263impl Error for TsTargetParseError {
264 fn source(&self) -> Option<&(dyn Error + 'static)> {
265 match self {
266 Self::Empty => None,
267 Self::EcmaScript(error) => Some(error),
268 }
269 }
270}
271
272fn parse_version_part(input: &str) -> Result<u16, TypeScriptVersionParseError> {
273 input
274 .parse::<u16>()
275 .map_err(|_error| TypeScriptVersionParseError::InvalidVersion)
276}
277
278fn parse_optional_version_part(
279 input: Option<&str>,
280) -> Result<Option<u16>, TypeScriptVersionParseError> {
281 input.map(parse_version_part).transpose()
282}
283
284#[cfg(test)]
285mod tests {
286 use super::{TsModuleResolution, TsStrictness, TsTarget, TypeScriptVersion};
287
288 #[test]
289 fn parses_versions() -> Result<(), Box<dyn std::error::Error>> {
290 let version: TypeScriptVersion = "v5.4.2".parse()?;
291 assert_eq!(version.major(), 5);
292 assert_eq!(version.minor(), Some(4));
293 assert_eq!(version.patch(), Some(2));
294 assert_eq!(version.to_string(), "5.4.2");
295 Ok(())
296 }
297
298 #[test]
299 fn parses_options() -> Result<(), Box<dyn std::error::Error>> {
300 assert_eq!(
301 "nodenext".parse::<TsModuleResolution>()?,
302 TsModuleResolution::NodeNext
303 );
304 assert_eq!("es2022".parse::<TsTarget>()?.to_string(), "ES2022");
305 assert_eq!("strict".parse::<TsStrictness>()?, TsStrictness::Strict);
306 Ok(())
307 }
308}