]> git.ipfire.org Git - thirdparty/vuejs/router.git/commitdiff
chore: static path matcher
authorEduardo San Martin Morote <posva13@gmail.com>
Wed, 26 Jun 2024 13:52:51 +0000 (15:52 +0200)
committerEduardo San Martin Morote <posva13@gmail.com>
Wed, 4 Dec 2024 15:10:35 +0000 (16:10 +0100)
packages/router/src/new-route-resolver/index.ts [moved from packages/router/src/new-matcher/index.ts with 100% similarity]
packages/router/src/new-route-resolver/matcher-location.ts [moved from packages/router/src/new-matcher/matcher-location.ts with 100% similarity]
packages/router/src/new-route-resolver/matcher-pattern.ts [moved from packages/router/src/new-matcher/matcher-pattern.ts with 53% similarity]
packages/router/src/new-route-resolver/matcher.spec.ts [moved from packages/router/src/new-matcher/matcher.spec.ts with 85% similarity]
packages/router/src/new-route-resolver/matcher.test-d.ts [moved from packages/router/src/new-matcher/matcher.test-d.ts with 64% similarity]
packages/router/src/new-route-resolver/matcher.ts [moved from packages/router/src/new-matcher/matcher.ts with 87% similarity]
packages/router/src/new-route-resolver/matchers/path-param.ts [new file with mode: 0644]
packages/router/src/new-route-resolver/matchers/path-static.ts [new file with mode: 0644]

similarity index 53%
rename from packages/router/src/new-matcher/matcher-pattern.ts
rename to packages/router/src/new-route-resolver/matcher-pattern.ts
index f368a04f83121c589da2f3523fb5dbbcd4d67537..a9f8f5e83b035884ae7d79198c8fb04853827478 100644 (file)
@@ -6,19 +6,34 @@ import type {
 } from './matcher'
 import type { MatcherParamsFormatted } from './matcher-location'
 
+/**
+ * Allows to match, extract, parse and build a path. Tailored to iterate through route records and check if a location
+ * matches. When it cannot match, it returns `null` instead of throwing to not force a try/catch block around each
+ * iteration in for loops.
+ */
 export interface MatcherPattern {
   /**
    * Name of the matcher. Unique across all matchers.
    */
   name: MatcherName
 
+  // TODO: add route record to be able to build the matched
+
   /**
-   * Extracts from a formatted, unencoded params object the ones belonging to the path, query, and hash.
-   * @param params - Params to extract from.
+   * Extracts from an unencoded, parsed params object the ones belonging to the path, query, and hash in their
+   * serialized format but still unencoded. e.g. `{ id: 2 }` -> `{ id: '2' }`. If any params are missing, return `null`.
+   *
+   * @param params - Params to extract from. If any params are missing, throws
    */
-  unformatParams(
+  matchParams(
     params: MatcherParamsFormatted
-  ): [path: MatcherPathParams, query: MatcherQueryParams, hash: string]
+  ):
+    | readonly [
+        pathParams: MatcherPathParams,
+        queryParams: MatcherQueryParams,
+        hashParam: string
+      ]
+    | null
 
   /**
    * Extracts the defined params from an encoded path, query, and hash parsed from a URL. Does not apply formatting or
@@ -44,23 +59,34 @@ export interface MatcherPattern {
     path: string
     query: MatcherQueryParams
     hash: string
-  }): [path: MatcherPathParams, query: MatcherQueryParams, hash: string] | null
+  }):
+    | readonly [
+        pathParams: MatcherPathParams,
+        queryParams: MatcherQueryParams,
+        hashParam: string
+      ]
+    | null
 
   /**
    * Takes encoded params object to form the `path`,
-   * @param path - encoded path params
+   *
+   * @param pathParams - encoded path params
    */
-  buildPath(path: MatcherPathParams): string
+  buildPath(pathParams: MatcherPathParams): string
 
   /**
-   * Runs the decoded params through the formatting functions if any.
-   * @param params - Params to format.
+   * Runs the decoded params through the parsing functions if any, allowing them to be in be of a type other than a
+   * string.
+   *
+   * @param pathParams - decoded path params
+   * @param queryParams - decoded query params
+   * @param hashParam - decoded hash param
    */
-  formatParams(
-    path: MatcherPathParams,
-    query: MatcherQueryParams,
-    hash: string
-  ): MatcherParamsFormatted
+  parseParams(
+    pathParams: MatcherPathParams,
+    queryParams: MatcherQueryParams,
+    hashParam: string
+  ): MatcherParamsFormatted | null
 }
 
 interface PatternParamOptions_Base<T = unknown> {
@@ -69,7 +95,11 @@ interface PatternParamOptions_Base<T = unknown> {
   default?: T | (() => T)
 }
 
-export interface PatternParamOptions extends PatternParamOptions_Base {}
+export interface PatternPathParamOptions<T = unknown>
+  extends PatternParamOptions_Base<T> {
+  re: RegExp
+  keys: string[]
+}
 
 export interface PatternQueryParamOptions<T = unknown>
   extends PatternParamOptions_Base<T> {
@@ -82,16 +112,16 @@ export interface PatternHashParamOptions
   extends PatternParamOptions_Base<string> {}
 
 export interface MatcherPatternPath {
-  build(path: MatcherPathParams): string
+  buildPath(path: MatcherPathParams): string
   match(path: string): MatcherPathParams
-  format(params: MatcherPathParams): MatcherParamsFormatted
-  unformat(params: MatcherParamsFormatted): MatcherPathParams
+  parse?(params: MatcherPathParams): MatcherParamsFormatted
+  serialize?(params: MatcherParamsFormatted): MatcherPathParams
 }
 
 export interface MatcherPatternQuery {
   match(query: MatcherQueryParams): MatcherQueryParams
-  format(params: MatcherQueryParams): MatcherParamsFormatted
-  unformat(params: MatcherParamsFormatted): MatcherQueryParams
+  parse(params: MatcherQueryParams): MatcherParamsFormatted
+  serialize(params: MatcherParamsFormatted): MatcherQueryParams
 }
 
 export interface MatcherPatternHash {
@@ -100,8 +130,8 @@ export interface MatcherPatternHash {
    * @param hash - encoded hash
    */
   match(hash: string): string
-  format(hash: string): MatcherParamsFormatted
-  unformat(params: MatcherParamsFormatted): string
+  parse(hash: string): MatcherParamsFormatted
+  serialize(params: MatcherParamsFormatted): string
 }
 
 export class MatcherPatternImpl implements MatcherPattern {
@@ -116,37 +146,42 @@ export class MatcherPatternImpl implements MatcherPattern {
     path: string
     query: MatcherQueryParams
     hash: string
-  }): [path: MatcherPathParams, query: MatcherQueryParams, hash: string] {
-    return [
-      this.path.match(location.path),
-      this.query?.match(location.query) ?? {},
-      this.hash?.match(location.hash) ?? '',
-    ]
+  }) {
+    // TODO: is this performant? Compare to a check with `null
+    try {
+      return [
+        this.path.match(location.path),
+        this.query?.match(location.query) ?? {},
+        this.hash?.match(location.hash) ?? '',
+      ] as const
+    } catch {
+      return null
+    }
   }
 
-  formatParams(
+  parseParams(
     path: MatcherPathParams,
     query: MatcherQueryParams,
     hash: string
   ): MatcherParamsFormatted {
     return {
-      ...this.path.format(path),
-      ...this.query?.format(query),
-      ...this.hash?.format(hash),
+      ...this.path.parse?.(path),
+      ...this.query?.parse(query),
+      ...this.hash?.parse(hash),
     }
   }
 
   buildPath(path: MatcherPathParams): string {
-    return this.path.build(path)
+    return this.path.buildPath(path)
   }
 
-  unformatParams(
+  matchParams(
     params: MatcherParamsFormatted
   ): [path: MatcherPathParams, query: MatcherQueryParams, hash: string] {
     return [
-      this.path.unformat(params),
-      this.query?.unformat(params) ?? {},
-      this.hash?.unformat(params) ?? '',
+      this.path.serialize?.(params) ?? {},
+      this.query?.serialize(params) ?? {},
+      this.hash?.serialize(params) ?? '',
     ]
   }
 }
similarity index 85%
rename from packages/router/src/new-matcher/matcher.spec.ts
rename to packages/router/src/new-route-resolver/matcher.spec.ts
index 9c6ccb2fe48243beb244f0c139b7b0fb46b1a2d6..52d8b208aa9ff75af5fcb3d43cbd7cf3258829bf 100644 (file)
@@ -10,9 +10,9 @@ function createMatcherPattern(
 
 const EMPTY_PATH_PATTERN_MATCHER = {
   match: (path: string) => ({}),
-  format: (params: {}) => ({}),
-  unformat: (params: {}) => ({}),
-  build: () => '/',
+  parse: (params: {}) => ({}),
+  serialize: (params: {}) => ({}),
+  buildPath: () => '/',
 } satisfies MatcherPatternPath
 
 describe('Matcher', () => {
@@ -42,9 +42,9 @@ describe('Matcher', () => {
               if (!match) throw new Error('no match')
               return { id: match[1] }
             },
-            format: (params: { id: string }) => ({ id: Number(params.id) }),
-            unformat: (params: { id: number }) => ({ id: String(params.id) }),
-            build: params => `/foo/${params.id}`,
+            parse: (params: { id: string }) => ({ id: Number(params.id) }),
+            serialize: (params: { id: number }) => ({ id: String(params.id) }),
+            buildPath: params => `/foo/${params.id}`,
           })
         )
 
@@ -69,8 +69,8 @@ describe('Matcher', () => {
             match: query => ({
               id: Array.isArray(query.id) ? query.id[0] : query.id,
             }),
-            format: (params: { id: string }) => ({ id: Number(params.id) }),
-            unformat: (params: { id: number }) => ({ id: String(params.id) }),
+            parse: (params: { id: string }) => ({ id: Number(params.id) }),
+            serialize: (params: { id: number }) => ({ id: String(params.id) }),
           })
         )
 
@@ -94,8 +94,8 @@ describe('Matcher', () => {
             undefined,
             {
               match: hash => hash,
-              format: hash => ({ a: hash.slice(1) }),
-              unformat: ({ a }) => '#a',
+              parse: hash => ({ a: hash.slice(1) }),
+              serialize: ({ a }) => '#a',
             }
           )
         )
@@ -138,26 +138,26 @@ describe('Matcher', () => {
           createMatcherPattern(
             Symbol('foo'),
             {
-              build: params => `/foo/${params.id}`,
+              buildPath: params => `/foo/${params.id}`,
               match: path => {
                 const match = path.match(/^\/foo\/([^/]+?)$/)
                 if (!match) throw new Error('no match')
                 return { id: match[1] }
               },
-              format: params => ({ id: Number(params.id) }),
-              unformat: params => ({ id: String(params.id) }),
+              parse: params => ({ id: Number(params.id) }),
+              serialize: params => ({ id: String(params.id) }),
             },
             {
               match: query => ({
                 id: Array.isArray(query.id) ? query.id[0] : query.id,
               }),
-              format: params => ({ q: Number(params.id) }),
-              unformat: params => ({ id: String(params.q) }),
+              parse: params => ({ q: Number(params.id) }),
+              serialize: params => ({ id: String(params.q) }),
             },
             {
               match: hash => hash,
-              format: hash => ({ a: hash.slice(1) }),
-              unformat: ({ a }) => '#a',
+              parse: hash => ({ a: hash.slice(1) }),
+              serialize: ({ a }) => '#a',
             }
           )
         )
similarity index 64%
rename from packages/router/src/new-matcher/matcher.test-d.ts
rename to packages/router/src/new-route-resolver/matcher.test-d.ts
index fbf150e2eb4b96cea747a98ec430448e93c23b3f..412cb0719870d84906a7d175c3de9be6357b6d5d 100644 (file)
@@ -1,5 +1,5 @@
 import { describe, it } from 'vitest'
-import { NEW_MatcherLocationResolved, createCompiledMatcher } from './matcher'
+import { NEW_LocationResolved, createCompiledMatcher } from './matcher'
 
 describe('Matcher', () => {
   it('resolves locations', () => {
@@ -7,10 +7,10 @@ describe('Matcher', () => {
     matcher.resolve('/foo')
     // @ts-expect-error: needs currentLocation
     matcher.resolve('foo')
-    matcher.resolve('foo', {} as NEW_MatcherLocationResolved)
+    matcher.resolve('foo', {} as NEW_LocationResolved)
     matcher.resolve({ name: 'foo', params: {} })
     // @ts-expect-error: needs currentLocation
     matcher.resolve({ params: { id: 1 } })
-    matcher.resolve({ params: { id: 1 } }, {} as NEW_MatcherLocationResolved)
+    matcher.resolve({ params: { id: 1 } }, {} as NEW_LocationResolved)
   })
 })
similarity index 87%
rename from packages/router/src/new-matcher/matcher.ts
rename to packages/router/src/new-route-resolver/matcher.ts
index 9e39f44e66023da0f460b98d7fa9e8adc3992720..4aa742e9303a53aa50d8ee7529affff417ef8033 100644 (file)
@@ -21,13 +21,13 @@ import type {
 export type MatcherName = string | symbol
 
 /**
- * Matcher capable of resolving route locations.
+ * Manage and resolve routes. Also handles the encoding, decoding, parsing and serialization of params, query, and hash.
  */
-export interface NEW_Matcher_Resolve {
+export interface RouteResolver {
   /**
    * Resolves an absolute location (like `/path/to/somewhere`).
    */
-  resolve(absoluteLocation: `/${string}`): NEW_MatcherLocationResolved
+  resolve(absoluteLocation: `/${string}`): NEW_LocationResolved
 
   /**
    * Resolves a string location relative to another location. A relative location can be `./same-folder`,
@@ -35,13 +35,13 @@ export interface NEW_Matcher_Resolve {
    */
   resolve(
     relativeLocation: string,
-    currentLocation: NEW_MatcherLocationResolved
-  ): NEW_MatcherLocationResolved
+    currentLocation: NEW_LocationResolved
+  ): NEW_LocationResolved
 
   /**
    * Resolves a location by its name. Any required params or query must be passed in the `options` argument.
    */
-  resolve(location: MatcherLocationAsName): NEW_MatcherLocationResolved
+  resolve(location: MatcherLocationAsName): NEW_LocationResolved
 
   /**
    * Resolves a location by its path. Any required query must be passed.
@@ -56,8 +56,8 @@ export interface NEW_Matcher_Resolve {
    */
   resolve(
     relativeLocation: MatcherLocationAsRelative,
-    currentLocation: NEW_MatcherLocationResolved
-  ): NEW_MatcherLocationResolved
+    currentLocation: NEW_LocationResolved
+  ): NEW_LocationResolved
 
   addRoute(matcher: MatcherPattern, parent?: MatcherPattern): void
   removeRoute(matcher: MatcherPattern): void
@@ -66,11 +66,11 @@ export interface NEW_Matcher_Resolve {
 
 type MatcherResolveArgs =
   | [absoluteLocation: `/${string}`]
-  | [relativeLocation: string, currentLocation: NEW_MatcherLocationResolved]
+  | [relativeLocation: string, currentLocation: NEW_LocationResolved]
   | [location: MatcherLocationAsName]
   | [
       relativeLocation: MatcherLocationAsRelative,
-      currentLocation: NEW_MatcherLocationResolved
+      currentLocation: NEW_LocationResolved
     ]
 
 /**
@@ -87,7 +87,7 @@ export interface NEW_Matcher_Dynamic {
 
 type TODO = any
 
-export interface NEW_MatcherLocationResolved {
+export interface NEW_LocationResolved {
   name: MatcherName
   fullPath: string
   path: string
@@ -198,12 +198,9 @@ export const NO_MATCH_LOCATION = {
   name: Symbol('no-match'),
   params: {},
   matched: [],
-} satisfies Omit<
-  NEW_MatcherLocationResolved,
-  'path' | 'hash' | 'query' | 'fullPath'
->
+} satisfies Omit<NEW_LocationResolved, 'path' | 'hash' | 'query' | 'fullPath'>
 
-export function createCompiledMatcher(): NEW_Matcher_Resolve {
+export function createCompiledMatcher(): RouteResolver {
   const matchers = new Map<MatcherName, MatcherPattern>()
 
   // TODO: allow custom encode/decode functions
@@ -216,7 +213,7 @@ export function createCompiledMatcher(): NEW_Matcher_Resolve {
   // )
   // const decodeQuery = transformObject.bind(null, decode, decode)
 
-  function resolve(...args: MatcherResolveArgs): NEW_MatcherLocationResolved {
+  function resolve(...args: MatcherResolveArgs): NEW_LocationResolved {
     const [location, currentLocation] = args
     if (typeof location === 'string') {
       // string location, e.g. '/foo', '../bar', 'baz'
@@ -228,7 +225,7 @@ export function createCompiledMatcher(): NEW_Matcher_Resolve {
       for (matcher of matchers.values()) {
         const params = matcher.matchLocation(url)
         if (params) {
-          parsedParams = matcher.formatParams(
+          parsedParams = matcher.parseParams(
             transformObject(String, decode, params[0]),
             // already decoded
             params[1],
@@ -268,7 +265,17 @@ export function createCompiledMatcher(): NEW_Matcher_Resolve {
 
       // unencoded params in a formatted form that the user came up with
       const params = location.params ?? currentLocation!.params
-      const mixedUnencodedParams = matcher.unformatParams(params)
+      const mixedUnencodedParams = matcher.matchParams(params)
+
+      if (!mixedUnencodedParams) {
+        throw new Error(
+          `Invalid params for matcher "${String(name)}":\n${JSON.stringify(
+            params,
+            null,
+            2
+          )}`
+        )
+      }
 
       const path = matcher.buildPath(
         // encode the values before building the path
diff --git a/packages/router/src/new-route-resolver/matchers/path-param.ts b/packages/router/src/new-route-resolver/matchers/path-param.ts
new file mode 100644 (file)
index 0000000..e17e780
--- /dev/null
@@ -0,0 +1,48 @@
+import type { MatcherPathParams } from '../matcher'
+import { MatcherParamsFormatted } from '../matcher-location'
+import type {
+  MatcherPatternPath,
+  PatternPathParamOptions,
+} from '../matcher-pattern'
+
+export class PatterParamPath<T> implements MatcherPatternPath {
+  options: Required<Omit<PatternPathParamOptions<T>, 'default'>> & {
+    default: undefined | (() => T) | T
+  }
+
+  constructor(options: PatternPathParamOptions<T>) {
+    this.options = {
+      set: String,
+      default: undefined,
+      ...options,
+    }
+  }
+
+  match(path: string): MatcherPathParams {
+    const match = this.options.re.exec(path)?.groups ?? {}
+    if (!match) {
+      throw new Error(
+        `Path "${path}" does not match the pattern "${String(
+          this.options.re
+        )}"}`
+      )
+    }
+    const params: MatcherPathParams = {}
+    for (let i = 0; i < this.options.keys.length; i++) {
+      params[this.options.keys[i]] = match[i + 1] ?? null
+    }
+    return params
+  }
+
+  buildPath(path: MatcherPathParams): string {
+    throw new Error('Method not implemented.')
+  }
+
+  parse(params: MatcherPathParams): MatcherParamsFormatted {
+    throw new Error('Method not implemented.')
+  }
+
+  serialize(params: MatcherParamsFormatted): MatcherPathParams {
+    throw new Error('Method not implemented.')
+  }
+}
diff --git a/packages/router/src/new-route-resolver/matchers/path-static.ts b/packages/router/src/new-route-resolver/matchers/path-static.ts
new file mode 100644 (file)
index 0000000..0d6ebd3
--- /dev/null
@@ -0,0 +1,15 @@
+import type { MatcherPatternPath } from '../matcher-pattern'
+
+export class PathMatcherStatic implements MatcherPatternPath {
+  constructor(private path: string) {}
+
+  match(path: string) {
+    if (this.path === path) return {}
+    throw new Error()
+    // return this.path === path ? {} : null
+  }
+
+  buildPath() {
+    return this.path
+  }
+}