Skip to main content

use_next/
lib.rs

1#![forbid(unsafe_code)]
2#![doc = include_str!("../README.md")]
3
4use core::{fmt, str::FromStr};
5use std::error::Error;
6
7/// `Next.js` version-family labels.
8#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
9pub enum NextJsVersionFamily {
10    Next12,
11    Next13,
12    Next14,
13    Next15,
14    Next16,
15}
16
17impl NextJsVersionFamily {
18    /// Returns the version-family label.
19    #[must_use]
20    pub const fn as_str(self) -> &'static str {
21        match self {
22            Self::Next12 => "next12",
23            Self::Next13 => "next13",
24            Self::Next14 => "next14",
25            Self::Next15 => "next15",
26            Self::Next16 => "next16",
27        }
28    }
29}
30
31impl fmt::Display for NextJsVersionFamily {
32    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
33        formatter.write_str(self.as_str())
34    }
35}
36
37impl FromStr for NextJsVersionFamily {
38    type Err = NextJsRouteError;
39
40    fn from_str(input: &str) -> Result<Self, Self::Err> {
41        match normalized_label(input)?.as_str() {
42            "next12" | "12" => Ok(Self::Next12),
43            "next13" | "13" => Ok(Self::Next13),
44            "next14" | "14" => Ok(Self::Next14),
45            "next15" | "15" => Ok(Self::Next15),
46            "next16" | "16" => Ok(Self::Next16),
47            _ => Err(NextJsRouteError::UnknownLabel),
48        }
49    }
50}
51
52/// `Next.js` router-kind labels.
53#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
54pub enum NextJsRouterKind {
55    PagesRouter,
56    AppRouter,
57}
58
59impl NextJsRouterKind {
60    /// Returns the router-kind label.
61    #[must_use]
62    pub const fn as_str(self) -> &'static str {
63        match self {
64            Self::PagesRouter => "pages-router",
65            Self::AppRouter => "app-router",
66        }
67    }
68}
69
70impl fmt::Display for NextJsRouterKind {
71    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
72        formatter.write_str(self.as_str())
73    }
74}
75
76impl FromStr for NextJsRouterKind {
77    type Err = NextJsRouteError;
78
79    fn from_str(input: &str) -> Result<Self, Self::Err> {
80        match normalized_label(input)?.as_str() {
81            "pagesrouter" | "pages" => Ok(Self::PagesRouter),
82            "approuter" | "app" => Ok(Self::AppRouter),
83            _ => Err(NextJsRouteError::UnknownLabel),
84        }
85    }
86}
87
88/// `Next.js` directory labels.
89#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
90pub enum NextJsDirectoryKind {
91    App,
92    Pages,
93    Components,
94    Public,
95    Styles,
96    Lib,
97    Api,
98    Middleware,
99}
100
101impl NextJsDirectoryKind {
102    /// Returns the directory label.
103    #[must_use]
104    pub const fn as_str(self) -> &'static str {
105        match self {
106            Self::App => "app",
107            Self::Pages => "pages",
108            Self::Components => "components",
109            Self::Public => "public",
110            Self::Styles => "styles",
111            Self::Lib => "lib",
112            Self::Api => "api",
113            Self::Middleware => "middleware",
114        }
115    }
116}
117
118impl fmt::Display for NextJsDirectoryKind {
119    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
120        formatter.write_str(self.as_str())
121    }
122}
123
124impl FromStr for NextJsDirectoryKind {
125    type Err = NextJsRouteError;
126
127    fn from_str(input: &str) -> Result<Self, Self::Err> {
128        match normalized_label(input)?.as_str() {
129            "app" => Ok(Self::App),
130            "pages" => Ok(Self::Pages),
131            "components" => Ok(Self::Components),
132            "public" => Ok(Self::Public),
133            "styles" => Ok(Self::Styles),
134            "lib" | "library" => Ok(Self::Lib),
135            "api" => Ok(Self::Api),
136            "middleware" => Ok(Self::Middleware),
137            _ => Err(NextJsRouteError::UnknownLabel),
138        }
139    }
140}
141
142/// `Next.js` file-kind labels.
143#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
144pub enum NextJsFileKind {
145    Page,
146    Layout,
147    Loading,
148    Error,
149    NotFound,
150    Route,
151    Middleware,
152    Config,
153    Metadata,
154}
155
156impl NextJsFileKind {
157    /// Returns the file-kind label.
158    #[must_use]
159    pub const fn as_str(self) -> &'static str {
160        match self {
161            Self::Page => "page",
162            Self::Layout => "layout",
163            Self::Loading => "loading",
164            Self::Error => "error",
165            Self::NotFound => "not-found",
166            Self::Route => "route",
167            Self::Middleware => "middleware",
168            Self::Config => "config",
169            Self::Metadata => "metadata",
170        }
171    }
172}
173
174impl fmt::Display for NextJsFileKind {
175    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
176        formatter.write_str(self.as_str())
177    }
178}
179
180impl FromStr for NextJsFileKind {
181    type Err = NextJsRouteError;
182
183    fn from_str(input: &str) -> Result<Self, Self::Err> {
184        match normalized_label(input)?.as_str() {
185            "page" => Ok(Self::Page),
186            "layout" => Ok(Self::Layout),
187            "loading" => Ok(Self::Loading),
188            "error" => Ok(Self::Error),
189            "notfound" => Ok(Self::NotFound),
190            "route" => Ok(Self::Route),
191            "middleware" => Ok(Self::Middleware),
192            "config" => Ok(Self::Config),
193            "metadata" => Ok(Self::Metadata),
194            _ => Err(NextJsRouteError::UnknownLabel),
195        }
196    }
197}
198
199/// `Next.js` rendering mode labels.
200#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
201pub enum NextJsRenderingMode {
202    Ssg,
203    Ssr,
204    Isr,
205    Csr,
206    Rsc,
207    Hybrid,
208}
209
210impl NextJsRenderingMode {
211    /// Returns the rendering mode label.
212    #[must_use]
213    pub const fn as_str(self) -> &'static str {
214        match self {
215            Self::Ssg => "ssg",
216            Self::Ssr => "ssr",
217            Self::Isr => "isr",
218            Self::Csr => "csr",
219            Self::Rsc => "rsc",
220            Self::Hybrid => "hybrid",
221        }
222    }
223}
224
225impl fmt::Display for NextJsRenderingMode {
226    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
227        formatter.write_str(self.as_str())
228    }
229}
230
231impl FromStr for NextJsRenderingMode {
232    type Err = NextJsRouteError;
233
234    fn from_str(input: &str) -> Result<Self, Self::Err> {
235        match normalized_label(input)?.as_str() {
236            "ssg" => Ok(Self::Ssg),
237            "ssr" => Ok(Self::Ssr),
238            "isr" => Ok(Self::Isr),
239            "csr" => Ok(Self::Csr),
240            "rsc" => Ok(Self::Rsc),
241            "hybrid" => Ok(Self::Hybrid),
242            _ => Err(NextJsRouteError::UnknownLabel),
243        }
244    }
245}
246
247/// `Next.js` route-kind labels.
248#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
249pub enum NextJsRouteKind {
250    Page,
251    ApiRoute,
252    RouteHandler,
253    Middleware,
254}
255
256impl NextJsRouteKind {
257    /// Returns the route-kind label.
258    #[must_use]
259    pub const fn as_str(self) -> &'static str {
260        match self {
261            Self::Page => "page",
262            Self::ApiRoute => "api-route",
263            Self::RouteHandler => "route-handler",
264            Self::Middleware => "middleware",
265        }
266    }
267}
268
269impl fmt::Display for NextJsRouteKind {
270    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
271        formatter.write_str(self.as_str())
272    }
273}
274
275impl FromStr for NextJsRouteKind {
276    type Err = NextJsRouteError;
277
278    fn from_str(input: &str) -> Result<Self, Self::Err> {
279        match normalized_label(input)?.as_str() {
280            "page" => Ok(Self::Page),
281            "apiroute" | "api" => Ok(Self::ApiRoute),
282            "routehandler" | "handler" => Ok(Self::RouteHandler),
283            "middleware" => Ok(Self::Middleware),
284            _ => Err(NextJsRouteError::UnknownLabel),
285        }
286    }
287}
288
289/// `Next.js` runtime-kind labels.
290#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
291pub enum NextJsRuntimeKind {
292    NodeJs,
293    Edge,
294}
295
296impl NextJsRuntimeKind {
297    /// Returns the runtime-kind label.
298    #[must_use]
299    pub const fn as_str(self) -> &'static str {
300        match self {
301            Self::NodeJs => "nodejs",
302            Self::Edge => "edge",
303        }
304    }
305}
306
307impl fmt::Display for NextJsRuntimeKind {
308    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
309        formatter.write_str(self.as_str())
310    }
311}
312
313impl FromStr for NextJsRuntimeKind {
314    type Err = NextJsRouteError;
315
316    fn from_str(input: &str) -> Result<Self, Self::Err> {
317        match normalized_label(input)?.as_str() {
318            "nodejs" | "node" => Ok(Self::NodeJs),
319            "edge" => Ok(Self::Edge),
320            _ => Err(NextJsRouteError::UnknownLabel),
321        }
322    }
323}
324
325/// Common `Next.js` config file labels.
326#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
327pub enum NextJsConfigFile {
328    NextConfigJs,
329    NextConfigMjs,
330    NextConfigTs,
331}
332
333impl NextJsConfigFile {
334    /// Returns the config file label.
335    #[must_use]
336    pub const fn as_str(self) -> &'static str {
337        match self {
338            Self::NextConfigJs => "next.config.js",
339            Self::NextConfigMjs => "next.config.mjs",
340            Self::NextConfigTs => "next.config.ts",
341        }
342    }
343}
344
345impl fmt::Display for NextJsConfigFile {
346    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
347        formatter.write_str(self.as_str())
348    }
349}
350
351impl FromStr for NextJsConfigFile {
352    type Err = NextJsRouteError;
353
354    fn from_str(input: &str) -> Result<Self, Self::Err> {
355        match normalized_label(input)?.as_str() {
356            "nextconfigjs" => Ok(Self::NextConfigJs),
357            "nextconfigmjs" => Ok(Self::NextConfigMjs),
358            "nextconfigts" => Ok(Self::NextConfigTs),
359            _ => Err(NextJsRouteError::UnknownLabel),
360        }
361    }
362}
363
364/// `Next.js` metadata-kind labels.
365#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
366pub enum NextJsMetadataKind {
367    StaticMetadata,
368    GeneratedMetadata,
369    FileBasedMetadata,
370}
371
372impl NextJsMetadataKind {
373    /// Returns the metadata-kind label.
374    #[must_use]
375    pub const fn as_str(self) -> &'static str {
376        match self {
377            Self::StaticMetadata => "static-metadata",
378            Self::GeneratedMetadata => "generated-metadata",
379            Self::FileBasedMetadata => "file-based-metadata",
380        }
381    }
382}
383
384impl fmt::Display for NextJsMetadataKind {
385    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
386        formatter.write_str(self.as_str())
387    }
388}
389
390impl FromStr for NextJsMetadataKind {
391    type Err = NextJsRouteError;
392
393    fn from_str(input: &str) -> Result<Self, Self::Err> {
394        match normalized_label(input)?.as_str() {
395            "staticmetadata" | "static" => Ok(Self::StaticMetadata),
396            "generatedmetadata" | "generated" => Ok(Self::GeneratedMetadata),
397            "filebasedmetadata" | "filebased" => Ok(Self::FileBasedMetadata),
398            _ => Err(NextJsRouteError::UnknownLabel),
399        }
400    }
401}
402
403/// Validated `Next.js` route segment metadata.
404#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
405pub struct NextJsRouteSegment(String);
406
407impl NextJsRouteSegment {
408    /// Creates `Next.js` route segment metadata.
409    ///
410    /// # Errors
411    ///
412    /// Returns [`NextJsRouteError`] when `input` is empty or contains a path separator or control character.
413    pub fn new(input: &str) -> Result<Self, NextJsRouteError> {
414        let trimmed = input.trim();
415        if trimmed.is_empty() {
416            return Err(NextJsRouteError::Empty);
417        }
418        if let Some(character) = trimmed
419            .chars()
420            .find(|character| character.is_control() || matches!(character, '/' | '\\'))
421        {
422            return Err(NextJsRouteError::InvalidCharacter { character });
423        }
424        Ok(Self(trimmed.to_string()))
425    }
426
427    /// Returns the route segment.
428    #[must_use]
429    pub fn as_str(&self) -> &str {
430        &self.0
431    }
432}
433
434impl fmt::Display for NextJsRouteSegment {
435    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
436        formatter.write_str(self.as_str())
437    }
438}
439
440impl FromStr for NextJsRouteSegment {
441    type Err = NextJsRouteError;
442
443    fn from_str(input: &str) -> Result<Self, Self::Err> {
444        Self::new(input)
445    }
446}
447
448impl TryFrom<&str> for NextJsRouteSegment {
449    type Error = NextJsRouteError;
450
451    fn try_from(value: &str) -> Result<Self, Self::Error> {
452        Self::new(value)
453    }
454}
455
456/// Validated `Next.js` dynamic segment metadata.
457#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
458pub struct NextJsDynamicSegment(String);
459
460impl NextJsDynamicSegment {
461    /// Creates `Next.js` dynamic segment metadata.
462    ///
463    /// # Errors
464    ///
465    /// Returns [`NextJsRouteError`] when `input` is not shaped like `[id]`, `[...slug]`, or `[[...slug]]`.
466    pub fn new(input: &str) -> Result<Self, NextJsRouteError> {
467        let trimmed = input.trim();
468        if trimmed.is_empty() {
469            return Err(NextJsRouteError::Empty);
470        }
471        let Some(inner) = dynamic_segment_inner(trimmed) else {
472            return Err(NextJsRouteError::InvalidDynamicSegment);
473        };
474        if inner.is_empty() || !inner.chars().all(is_segment_name_character) {
475            return Err(NextJsRouteError::InvalidDynamicSegment);
476        }
477        Ok(Self(trimmed.to_string()))
478    }
479
480    /// Returns the dynamic segment.
481    #[must_use]
482    pub fn as_str(&self) -> &str {
483        &self.0
484    }
485}
486
487impl fmt::Display for NextJsDynamicSegment {
488    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
489        formatter.write_str(self.as_str())
490    }
491}
492
493impl FromStr for NextJsDynamicSegment {
494    type Err = NextJsRouteError;
495
496    fn from_str(input: &str) -> Result<Self, Self::Err> {
497        Self::new(input)
498    }
499}
500
501impl TryFrom<&str> for NextJsDynamicSegment {
502    type Error = NextJsRouteError;
503
504    fn try_from(value: &str) -> Result<Self, Self::Error> {
505        Self::new(value)
506    }
507}
508
509/// Validated `Next.js` parallel route name metadata.
510#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
511pub struct NextJsParallelRouteName(String);
512
513impl NextJsParallelRouteName {
514    /// Creates `Next.js` parallel route name metadata.
515    ///
516    /// # Errors
517    ///
518    /// Returns [`NextJsRouteError`] when `input` does not start with `@` and include an ASCII-safe suffix.
519    pub fn new(input: &str) -> Result<Self, NextJsRouteError> {
520        let trimmed = input.trim();
521        if trimmed.is_empty() {
522            return Err(NextJsRouteError::Empty);
523        }
524        let Some(name) = trimmed.strip_prefix('@') else {
525            return Err(NextJsRouteError::InvalidParallelRouteName);
526        };
527        if name.is_empty() || !name.chars().all(is_segment_name_character) {
528            return Err(NextJsRouteError::InvalidParallelRouteName);
529        }
530        Ok(Self(trimmed.to_string()))
531    }
532
533    /// Returns the parallel route name.
534    #[must_use]
535    pub fn as_str(&self) -> &str {
536        &self.0
537    }
538}
539
540impl fmt::Display for NextJsParallelRouteName {
541    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
542        formatter.write_str(self.as_str())
543    }
544}
545
546impl FromStr for NextJsParallelRouteName {
547    type Err = NextJsRouteError;
548
549    fn from_str(input: &str) -> Result<Self, Self::Err> {
550        Self::new(input)
551    }
552}
553
554impl TryFrom<&str> for NextJsParallelRouteName {
555    type Error = NextJsRouteError;
556
557    fn try_from(value: &str) -> Result<Self, Self::Error> {
558        Self::new(value)
559    }
560}
561
562/// Validated `Next.js` intercepting route pattern metadata.
563#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
564pub struct NextJsInterceptingRoutePattern(String);
565
566impl NextJsInterceptingRoutePattern {
567    /// Creates `Next.js` intercepting route pattern metadata.
568    ///
569    /// # Errors
570    ///
571    /// Returns [`NextJsRouteError`] when `input` is empty or is not ASCII-safe intercepting route text.
572    pub fn new(input: &str) -> Result<Self, NextJsRouteError> {
573        let trimmed = input.trim();
574        if trimmed.is_empty() {
575            return Err(NextJsRouteError::Empty);
576        }
577        if !(trimmed.contains('(') && trimmed.contains(')')) {
578            return Err(NextJsRouteError::InvalidInterceptingRoutePattern);
579        }
580        if let Some(character) = trimmed
581            .chars()
582            .find(|character| !is_intercepting_route_character(*character))
583        {
584            return Err(NextJsRouteError::InvalidCharacter { character });
585        }
586        Ok(Self(trimmed.to_string()))
587    }
588
589    /// Returns the intercepting route pattern.
590    #[must_use]
591    pub fn as_str(&self) -> &str {
592        &self.0
593    }
594}
595
596impl fmt::Display for NextJsInterceptingRoutePattern {
597    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
598        formatter.write_str(self.as_str())
599    }
600}
601
602impl FromStr for NextJsInterceptingRoutePattern {
603    type Err = NextJsRouteError;
604
605    fn from_str(input: &str) -> Result<Self, Self::Err> {
606        Self::new(input)
607    }
608}
609
610impl TryFrom<&str> for NextJsInterceptingRoutePattern {
611    type Error = NextJsRouteError;
612
613    fn try_from(value: &str) -> Result<Self, Self::Error> {
614        Self::new(value)
615    }
616}
617
618/// Error returned when `Next.js` route metadata is invalid.
619#[derive(Clone, Copy, Debug, Eq, PartialEq)]
620pub enum NextJsRouteError {
621    Empty,
622    InvalidCharacter { character: char },
623    InvalidDynamicSegment,
624    InvalidParallelRouteName,
625    InvalidInterceptingRoutePattern,
626    UnknownLabel,
627}
628
629impl fmt::Display for NextJsRouteError {
630    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
631        match self {
632            Self::Empty => formatter.write_str("Next.js metadata text cannot be empty"),
633            Self::InvalidCharacter { character } => {
634                write!(
635                    formatter,
636                    "invalid Next.js metadata character `{character}`"
637                )
638            }
639            Self::InvalidDynamicSegment => formatter.write_str("invalid Next.js dynamic segment"),
640            Self::InvalidParallelRouteName => {
641                formatter.write_str("invalid Next.js parallel route name")
642            }
643            Self::InvalidInterceptingRoutePattern => {
644                formatter.write_str("invalid Next.js intercepting route pattern")
645            }
646            Self::UnknownLabel => formatter.write_str("unknown Next.js metadata label"),
647        }
648    }
649}
650
651impl Error for NextJsRouteError {}
652
653fn dynamic_segment_inner(input: &str) -> Option<&str> {
654    if let Some(inner) = input
655        .strip_prefix("[[...")
656        .and_then(|value| value.strip_suffix("]]"))
657    {
658        return Some(inner);
659    }
660    if let Some(inner) = input
661        .strip_prefix("[...")
662        .and_then(|value| value.strip_suffix(']'))
663    {
664        return Some(inner);
665    }
666    input
667        .strip_prefix('[')
668        .and_then(|value| value.strip_suffix(']'))
669}
670
671const fn is_segment_name_character(character: char) -> bool {
672    character.is_ascii_alphanumeric() || matches!(character, '_' | '-')
673}
674
675const fn is_intercepting_route_character(character: char) -> bool {
676    character.is_ascii_alphanumeric()
677        || matches!(
678            character,
679            '.' | '(' | ')' | '/' | '[' | ']' | '@' | '_' | '-'
680        )
681}
682
683fn normalized_label(input: &str) -> Result<String, NextJsRouteError> {
684    let trimmed = input.trim();
685    if trimmed.is_empty() {
686        return Err(NextJsRouteError::Empty);
687    }
688    Ok(trimmed
689        .chars()
690        .filter(|character| !matches!(character, '-' | '_' | ' ' | '.'))
691        .flat_map(char::to_lowercase)
692        .collect())
693}
694
695#[cfg(test)]
696mod tests {
697    use super::{
698        NextJsConfigFile, NextJsDirectoryKind, NextJsDynamicSegment, NextJsFileKind,
699        NextJsInterceptingRoutePattern, NextJsMetadataKind, NextJsParallelRouteName,
700        NextJsRenderingMode, NextJsRouteError, NextJsRouteKind, NextJsRouteSegment,
701        NextJsRouterKind, NextJsRuntimeKind, NextJsVersionFamily,
702    };
703
704    #[test]
705    fn validates_route_segments() -> Result<(), NextJsRouteError> {
706        let segment = NextJsRouteSegment::new("blog")?;
707        assert_eq!(segment.as_str(), "blog");
708        assert_eq!(NextJsRouteSegment::new(""), Err(NextJsRouteError::Empty));
709        assert_eq!(
710            NextJsRouteSegment::new("blog/posts"),
711            Err(NextJsRouteError::InvalidCharacter { character: '/' })
712        );
713        Ok(())
714    }
715
716    #[test]
717    fn validates_dynamic_segments() -> Result<(), NextJsRouteError> {
718        assert_eq!(NextJsDynamicSegment::new("[id]")?.as_str(), "[id]");
719        assert_eq!(
720            NextJsDynamicSegment::new("[...slug]")?.as_str(),
721            "[...slug]"
722        );
723        assert_eq!(
724            NextJsDynamicSegment::new("[[...slug]]")?.as_str(),
725            "[[...slug]]"
726        );
727        assert_eq!(
728            NextJsDynamicSegment::new("[]"),
729            Err(NextJsRouteError::InvalidDynamicSegment)
730        );
731        assert_eq!(
732            NextJsDynamicSegment::new("[slug.part]"),
733            Err(NextJsRouteError::InvalidDynamicSegment)
734        );
735        Ok(())
736    }
737
738    #[test]
739    fn validates_parallel_and_intercepting_routes() -> Result<(), NextJsRouteError> {
740        assert_eq!(NextJsParallelRouteName::new("@modal")?.as_str(), "@modal");
741        assert_eq!(
742            NextJsParallelRouteName::new("modal"),
743            Err(NextJsRouteError::InvalidParallelRouteName)
744        );
745        assert_eq!(
746            NextJsParallelRouteName::new("@"),
747            Err(NextJsRouteError::InvalidParallelRouteName)
748        );
749        assert_eq!(
750            NextJsInterceptingRoutePattern::new("(.)feed")?.as_str(),
751            "(.)feed"
752        );
753        assert_eq!(
754            NextJsInterceptingRoutePattern::new("feed"),
755            Err(NextJsRouteError::InvalidInterceptingRoutePattern)
756        );
757        Ok(())
758    }
759
760    #[test]
761    fn parses_labels() -> Result<(), NextJsRouteError> {
762        assert_eq!(
763            "next15".parse::<NextJsVersionFamily>()?,
764            NextJsVersionFamily::Next15
765        );
766        assert_eq!(
767            "app-router".parse::<NextJsRouterKind>()?,
768            NextJsRouterKind::AppRouter
769        );
770        assert_eq!(
771            "components".parse::<NextJsDirectoryKind>()?,
772            NextJsDirectoryKind::Components
773        );
774        assert_eq!(
775            "not-found".parse::<NextJsFileKind>()?,
776            NextJsFileKind::NotFound
777        );
778        assert_eq!(
779            "rsc".parse::<NextJsRenderingMode>()?,
780            NextJsRenderingMode::Rsc
781        );
782        assert_eq!(
783            "api-route".parse::<NextJsRouteKind>()?,
784            NextJsRouteKind::ApiRoute
785        );
786        assert_eq!(
787            "node.js".parse::<NextJsRuntimeKind>()?,
788            NextJsRuntimeKind::NodeJs
789        );
790        assert_eq!(
791            "next.config.ts".parse::<NextJsConfigFile>()?,
792            NextJsConfigFile::NextConfigTs
793        );
794        assert_eq!(
795            "file-based-metadata".parse::<NextJsMetadataKind>()?,
796            NextJsMetadataKind::FileBasedMetadata
797        );
798        assert_eq!(NextJsRuntimeKind::Edge.to_string(), "edge");
799        Ok(())
800    }
801}