@occultist/occultist 0.0.10 → 0.0.12

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (47) hide show
  1. package/dist/actions/actionSets.d.ts +7 -2
  2. package/dist/actions/actionSets.js +67 -9
  3. package/dist/actions/actions.d.ts +9 -8
  4. package/dist/actions/actions.js +11 -11
  5. package/dist/actions/context.d.ts +5 -1
  6. package/dist/actions/context.js +4 -0
  7. package/dist/actions/core.d.ts +8 -5
  8. package/dist/actions/core.js +20 -9
  9. package/dist/actions/route.d.ts +39 -0
  10. package/dist/actions/route.js +166 -0
  11. package/dist/actions/types.d.ts +3 -3
  12. package/dist/actions/writer.d.ts +1 -1
  13. package/dist/actions/writer.js +1 -3
  14. package/dist/cache/cache.d.ts +4 -3
  15. package/dist/cache/cache.js +10 -8
  16. package/dist/mod.d.ts +1 -1
  17. package/dist/mod.js +1 -1
  18. package/dist/registry.d.ts +53 -18
  19. package/dist/registry.js +60 -38
  20. package/dist/request.js +1 -2
  21. package/dist/scopes.d.ts +5 -4
  22. package/dist/scopes.js +17 -14
  23. package/dist/scopes.test.js +1 -1
  24. package/dist/types.d.ts +22 -1
  25. package/dist/utils/getRequestBodyValues.js +2 -0
  26. package/dist/utils/getRequestIRIValues.js +6 -9
  27. package/lib/actions/actionSets.ts +95 -11
  28. package/lib/actions/actions.ts +17 -17
  29. package/lib/actions/context.ts +7 -1
  30. package/lib/actions/core.ts +31 -11
  31. package/lib/actions/route.ts +202 -0
  32. package/lib/actions/types.ts +4 -3
  33. package/lib/actions/writer.ts +2 -4
  34. package/lib/cache/cache.ts +10 -7
  35. package/lib/mod.ts +1 -1
  36. package/lib/registry.ts +117 -52
  37. package/lib/request.ts +1 -2
  38. package/lib/scopes.test.ts +1 -1
  39. package/lib/scopes.ts +32 -23
  40. package/lib/types.ts +27 -1
  41. package/lib/utils/getRequestBodyValues.ts +2 -0
  42. package/lib/utils/getRequestIRIValues.ts +11 -14
  43. package/package.json +1 -1
  44. package/lib/actions/path.test.ts +0 -15
  45. package/lib/actions/path.ts +0 -99
  46. package/lib/registry.test.ts +0 -174
  47. package/lib/utils/normalizeURL.ts +0 -15
@@ -9,7 +9,7 @@ export type ActionAcceptMatch = {
9
9
  type: 'match';
10
10
  action: ImplementedAction;
11
11
  contentType?: string;
12
- language?: string;
12
+ languageTag?: string;
13
13
  encoding?: string;
14
14
  };
15
15
  export type ActionMatchResult = UnsupportedContentTypeMatch | ActionAcceptMatch;
@@ -18,6 +18,11 @@ export type ActionMatchResult = UnsupportedContentTypeMatch | ActionAcceptMatch;
18
18
  */
19
19
  export declare class ActionSet {
20
20
  #private;
21
- constructor(rootIRI: string, method: string, path: string, meta: ActionCore[]);
21
+ constructor(rootIRI: string, method: string, path: string, meta: ActionCore[], reverseExtensions: Map<string, string>);
22
+ /**
23
+ * @param method HTTP method to match against.
24
+ * @param path The pathname of the request.
25
+ * @param accept The accept cache of th erequest.
26
+ */
22
27
  matches(method: string, path: string, accept: Accept): null | ActionMatchResult;
23
28
  }
@@ -8,42 +8,100 @@ export class ActionSet {
8
8
  #method;
9
9
  #urlPattern;
10
10
  #contentTypeActionMap;
11
+ #extensionMap = new Map();
11
12
  #ctc;
12
- constructor(rootIRI, method, path, meta) {
13
+ #autoLanguageTags;
14
+ constructor(rootIRI, method, path, meta, reverseExtensions) {
13
15
  this.#rootIRI = rootIRI;
14
16
  this.#method = method;
17
+ [
18
+ this.#contentTypeActionMap,
19
+ this.#extensionMap,
20
+ this.#ctc,
21
+ this.#autoLanguageTags,
22
+ ] = this.#process(meta, reverseExtensions);
23
+ if (this.#extensionMap.size > 0) {
24
+ path += ':auto1(\\.[\\w\\-]+)?';
25
+ // language tags are only enabled if file extensions are
26
+ if (this.#autoLanguageTags) {
27
+ path += ':auto2(\\.[a-zA-Z0-9\\-]+)?';
28
+ }
29
+ }
15
30
  this.#urlPattern = makeURLPattern(path, rootIRI);
16
- [this.#contentTypeActionMap, this.#ctc] = this.#process(meta);
17
31
  }
32
+ /**
33
+ * @param method HTTP method to match against.
34
+ * @param path The pathname of the request.
35
+ * @param accept The accept cache of th erequest.
36
+ */
18
37
  matches(method, path, accept) {
19
38
  if (method !== this.#method) {
20
39
  return null;
21
40
  }
22
- else if (!this.#urlPattern.test(path, this.#rootIRI)) {
41
+ const res = this.#urlPattern.exec(path, this.#rootIRI);
42
+ if (res == null) {
23
43
  return null;
24
44
  }
25
- const contentType = accept.negotiate(this.#ctc);
45
+ let contentType;
46
+ let languageTag;
47
+ if (res.pathname.groups.auto1 != null && res.pathname.groups.auto2 != null) {
48
+ languageTag = res.pathname.groups.auto1.replace('.', '');
49
+ const fileExtension = res.pathname.groups.auto2.replace('.', '');
50
+ contentType = this.#extensionMap.get(fileExtension);
51
+ if (contentType == null)
52
+ return null;
53
+ }
54
+ else if (res.pathname.groups.auto1 != null || res.pathname.groups.auto2 != null) {
55
+ const autoParam = res.pathname.groups.auto1 ?? res.pathname.groups.auto2;
56
+ const fileExtension = autoParam.replace('.', '');
57
+ contentType = this.#extensionMap.get(fileExtension);
58
+ if (contentType == null)
59
+ return null;
60
+ }
61
+ if (contentType == null) {
62
+ contentType = accept.negotiate(this.#ctc);
63
+ }
64
+ if (contentType == null)
65
+ return null;
26
66
  const action = this.#contentTypeActionMap.get(contentType);
27
- if (contentType != null && action != null) {
67
+ if (action != null) {
28
68
  return {
29
69
  type: 'match',
30
70
  action,
31
71
  contentType,
72
+ languageTag,
32
73
  };
33
74
  }
34
75
  return null;
35
76
  }
36
- #process(meta) {
77
+ #process(meta, reverseExtensions) {
78
+ let autoLanguageTags = false;
37
79
  const contentTypes = [];
38
80
  const contentTypeActionMap = new Map();
39
- for (let i = 0; i < meta.length; i++) {
81
+ const extensionMap = new Map();
82
+ let l1 = meta.length;
83
+ for (let i = 0; i < l1; i++) {
40
84
  const action = meta[i].action;
41
- for (let j = 0; j < action.contentTypes.length; j++) {
85
+ let l2 = action.contentTypes.length;
86
+ for (let j = 0; j < l2; j++) {
42
87
  const contentType = action.contentTypes[j];
43
88
  contentTypes.push(contentType);
44
89
  contentTypeActionMap.set(contentType, action);
90
+ if (!autoLanguageTags && meta[i].autoLanguageTags) {
91
+ autoLanguageTags = true;
92
+ }
93
+ if (meta[i].autoFileExtensions &&
94
+ reverseExtensions.has(contentType) &&
95
+ !extensionMap.has(contentType)) {
96
+ extensionMap.set(reverseExtensions.get(contentType), contentType);
97
+ }
45
98
  }
46
99
  }
47
- return [contentTypeActionMap, new ContentTypeCache(contentTypes)];
100
+ return [
101
+ contentTypeActionMap,
102
+ extensionMap,
103
+ new ContentTypeCache(contentTypes),
104
+ autoLanguageTags,
105
+ ];
48
106
  }
49
107
  }
@@ -3,6 +3,7 @@ import type { JSONLDContext, JSONObject, TypeDef } from "../jsonld.ts";
3
3
  import type { Registry } from '../registry.ts';
4
4
  import type { Scope } from "../scopes.ts";
5
5
  import { MiddlewareRefs, type ActionCore } from "./core.ts";
6
+ import { Route } from './route.ts';
6
7
  import type { ActionSpec, ContextState } from "./spec.ts";
7
8
  import type { AuthMiddleware, AuthState, HandlerFn, HandlerMeta, HandlerObj, HandlerValue, HintArgs, ImplementedAction } from './types.ts';
8
9
  import { type ResponseTypes } from './writer.ts';
@@ -15,13 +16,13 @@ export type DefineArgs<Term extends string = string, Spec extends ActionSpec = A
15
16
  * after an action is defined.
16
17
  */
17
18
  export declare class HandlerDefinition<State extends ContextState = ContextState, Auth extends AuthState = AuthState, Spec extends ActionSpec = ActionSpec> {
18
- name: string;
19
+ name?: string;
19
20
  contentType: string;
20
21
  handler: HandlerFn | HandlerValue;
21
22
  meta: HandlerMeta;
22
23
  action: ImplementedAction<State, Auth, Spec>;
23
24
  cache: ReadonlyArray<CacheInstanceArgs>;
24
- constructor(name: string, contentType: string, handler: HandlerFn | HandlerValue, meta: HandlerMeta, action: ImplementedAction<State, Auth, Spec>, actionMeta: ActionCore);
25
+ constructor(name: string | undefined, contentType: string, handler: HandlerFn | HandlerValue, meta: HandlerMeta, action: ImplementedAction<State, Auth, Spec>, actionMeta: ActionCore);
25
26
  get [Symbol.toStringTag](): string;
26
27
  }
27
28
  export interface Handleable<State extends ContextState = ContextState, Auth extends AuthState = AuthState, Spec extends ActionSpec = ActionSpec> {
@@ -45,9 +46,9 @@ export declare class FinalizedAction<State extends ContextState = ContextState,
45
46
  get term(): string | undefined;
46
47
  get type(): string | undefined;
47
48
  get typeDef(): TypeDef | undefined;
48
- get name(): string;
49
+ get name(): string | undefined;
49
50
  get template(): string;
50
- get pattern(): URLPattern;
51
+ get route(): Route;
51
52
  get spec(): Spec;
52
53
  get scope(): Scope | undefined;
53
54
  get registry(): Registry;
@@ -84,9 +85,9 @@ export declare class DefinedAction<State extends ContextState = ContextState, Au
84
85
  get term(): string | undefined;
85
86
  get type(): string | undefined;
86
87
  get typeDef(): TypeDef | undefined;
87
- get name(): string;
88
+ get name(): string | undefined;
88
89
  get template(): string;
89
- get pattern(): URLPattern;
90
+ get route(): Route;
90
91
  get path(): string;
91
92
  get spec(): Spec;
92
93
  get scope(): Scope | undefined;
@@ -131,9 +132,9 @@ export declare class Action<State extends ContextState = ContextState, Auth exte
131
132
  get term(): string | undefined;
132
133
  get type(): string | undefined;
133
134
  get typeDef(): TypeDef | undefined;
134
- get name(): string;
135
+ get name(): string | undefined;
135
136
  get template(): string;
136
- get pattern(): URLPattern;
137
+ get route(): Route;
137
138
  get path(): string;
138
139
  get spec(): ActionSpec;
139
140
  get scope(): Scope | undefined;
@@ -31,7 +31,7 @@ export class HandlerDefinition {
31
31
  Object.freeze(this);
32
32
  }
33
33
  get [Symbol.toStringTag]() {
34
- return `name=${this.name} contentType=${this.contentType}`;
34
+ return `name=${this.name ?? 'anon'} contentType=${this.contentType}`;
35
35
  }
36
36
  }
37
37
  export class FinalizedAction {
@@ -65,7 +65,7 @@ export class FinalizedAction {
65
65
  return new FinalizedAction(typeDef, spec, core, arg3);
66
66
  }
67
67
  static async toJSONLD(action, scope) {
68
- if (scope == null || action.typeDef == null) {
68
+ if (scope == null || action.typeDef == null || action.name == null) {
69
69
  return null;
70
70
  }
71
71
  const apiSpec = await getPropertyValueSpecifications(action.spec);
@@ -103,8 +103,8 @@ export class FinalizedAction {
103
103
  get template() {
104
104
  return this.#core.uriTemplate;
105
105
  }
106
- get pattern() {
107
- return this.#core.path.pattern;
106
+ get route() {
107
+ return this.#core.route;
108
108
  }
109
109
  get spec() {
110
110
  return this.#spec;
@@ -129,7 +129,7 @@ export class FinalizedAction {
129
129
  });
130
130
  }
131
131
  url() {
132
- return joinPaths(this.#core.registry.rootIRI, this.#core.path.normalized);
132
+ return joinPaths(this.#core.registry.rootIRI, this.#core.route.normalized);
133
133
  }
134
134
  /**
135
135
  * Retrives the handler configured for the given content type.
@@ -239,11 +239,11 @@ export class DefinedAction {
239
239
  get template() {
240
240
  return this.#core.uriTemplate;
241
241
  }
242
- get pattern() {
243
- return this.#core.path.pattern;
242
+ get route() {
243
+ return this.#core.route;
244
244
  }
245
245
  get path() {
246
- return this.#core.path.normalized;
246
+ return this.#core.route.normalized;
247
247
  }
248
248
  get spec() {
249
249
  return this.#spec;
@@ -364,11 +364,11 @@ export class Action {
364
364
  get template() {
365
365
  return this.#core.uriTemplate;
366
366
  }
367
- get pattern() {
368
- return this.#core.path.pattern;
367
+ get route() {
368
+ return this.#core.route;
369
369
  }
370
370
  get path() {
371
- return this.#core.path.normalized;
371
+ return this.#core.route.normalized;
372
372
  }
373
373
  get spec() {
374
374
  return this.#spec;
@@ -6,6 +6,7 @@ import type { ResponseBody } from "./writer.ts";
6
6
  export type CacheContextArgs<Auth extends AuthState = AuthState> = {
7
7
  req: Request;
8
8
  contentType: string;
9
+ languageTag: string | undefined;
9
10
  public: boolean;
10
11
  authKey?: string;
11
12
  auth: Auth;
@@ -23,7 +24,8 @@ export declare class CacheContext<Auth extends AuthState = AuthState> {
23
24
  req: Request;
24
25
  method: string;
25
26
  url: string;
26
- contentType: string;
27
+ contentType?: string;
28
+ languageTag?: string;
27
29
  public: boolean;
28
30
  authKey: string | null;
29
31
  auth: Auth;
@@ -48,6 +50,7 @@ export declare class CacheContext<Auth extends AuthState = AuthState> {
48
50
  export type ContextArgs<State extends ContextState = ContextState, Auth extends AuthState = AuthState, Spec extends ActionSpec = ActionSpec> = {
49
51
  req: Request;
50
52
  contentType: string;
53
+ languageTag: string | undefined;
51
54
  public: boolean;
52
55
  authKey?: string;
53
56
  auth: Auth;
@@ -67,6 +70,7 @@ export declare class Context<State extends ContextState = ContextState, Auth ext
67
70
  method: string;
68
71
  url: string;
69
72
  contentType: string;
73
+ languageTag?: string;
70
74
  public: boolean;
71
75
  authKey?: string;
72
76
  auth: Auth;
@@ -16,6 +16,7 @@ export class CacheContext {
16
16
  method;
17
17
  url;
18
18
  contentType;
19
+ languageTag;
19
20
  public;
20
21
  authKey;
21
22
  auth;
@@ -30,6 +31,7 @@ export class CacheContext {
30
31
  this.req = args.req;
31
32
  this.url = args.req.url;
32
33
  this.contentType = args.contentType;
34
+ this.languageTag = args.languageTag;
33
35
  this.public = args.public;
34
36
  this.authKey = args.authKey;
35
37
  this.auth = args.auth;
@@ -80,6 +82,7 @@ export class Context {
80
82
  method;
81
83
  url;
82
84
  contentType;
85
+ languageTag;
83
86
  public = false;
84
87
  authKey;
85
88
  auth;
@@ -96,6 +99,7 @@ export class Context {
96
99
  this.req = args.req;
97
100
  this.url = args.req.url;
98
101
  this.contentType = args.contentType;
102
+ this.languageTag = args.languageTag;
99
103
  this.public = args.public;
100
104
  this.authKey = args.authKey;
101
105
  this.auth = args.auth;
@@ -5,7 +5,7 @@ import type { Registry } from '../registry.ts';
5
5
  import type { Scope } from "../scopes.ts";
6
6
  import { HandlerDefinition } from './actions.ts';
7
7
  import { CacheContext, Context } from './context.ts';
8
- import { Path } from "./path.ts";
8
+ import { Route } from "./route.ts";
9
9
  import type { ActionSpec, ContextState, FileValue, NextFn, TransformerFn } from './spec.ts';
10
10
  import type { AuthMiddleware, AuthState, CacheHitHeader, HintArgs, ImplementedAction } from './types.ts';
11
11
  import { type HTTPWriter, type ResponseTypes } from "./writer.ts";
@@ -27,13 +27,14 @@ export declare class MiddlewareRefs<State extends ContextState, Auth extends Aut
27
27
  headers: Headers;
28
28
  handler?: HandlerDefinition<State, Auth, Spec>;
29
29
  contentType: string | null;
30
+ languageTag: string | null;
30
31
  writer: HTTPWriter;
31
32
  req: Request;
32
33
  recordServerTiming: boolean;
33
34
  prevTime: number | null;
34
35
  serverTimes: string[];
35
36
  cacheHitHeader: CacheHitHeader;
36
- constructor(req: Request, writer: HTTPWriter, contentType: string | null, prevTime: number | null);
37
+ constructor(req: Request, writer: HTTPWriter, contentType: string | null, languageTag: string | null, prevTime: number | null);
37
38
  recordServerTime(name: string): void;
38
39
  }
39
40
  /**
@@ -45,11 +46,11 @@ export declare class ActionCore<State extends ContextState = ContextState, Auth
45
46
  rootIRI: string;
46
47
  method: string;
47
48
  isSafe: boolean;
48
- name: string;
49
+ name?: string;
49
50
  uriTemplate: string;
50
51
  public: boolean;
51
52
  authKey?: string;
52
- path: Path;
53
+ route: Route;
53
54
  hints: HintArgs[];
54
55
  transformers: Map<string, TransformerFn<JSONValue | FileValue, State, Spec>>;
55
56
  scope?: Scope;
@@ -61,8 +62,10 @@ export declare class ActionCore<State extends ContextState = ContextState, Auth
61
62
  cacheOccurance: 0 | 1;
62
63
  auth?: AuthMiddleware<Auth>;
63
64
  cache: CacheInstanceArgs[];
65
+ autoLanguageTags: boolean;
66
+ autoFileExtensions: boolean;
64
67
  recordServerTiming: boolean;
65
- constructor(rootIRI: string, method: string, name: string, uriTemplate: string, registry: Registry, writer: HTTPWriter, scope?: Scope);
68
+ constructor(rootIRI: string, method: string, name: string | undefined, uriTemplate: string, registry: Registry, writer: HTTPWriter, scope: Scope | undefined, autoLanguageTags: boolean, autoFileExtensions: boolean, recordServerTiming: boolean | undefined);
66
69
  /**
67
70
  * Called when the API is defined to compute all uncomputed values.
68
71
  */
@@ -3,7 +3,7 @@ import { ProblemDetailsError } from "../errors.js";
3
3
  import { processAction } from "../processAction.js";
4
4
  import { joinPaths } from "../utils/joinPaths.js";
5
5
  import { CacheContext, Context } from "./context.js";
6
- import { Path } from "./path.js";
6
+ import { Route } from "./route.js";
7
7
  const safeMethods = new Set([
8
8
  'OPTIONS',
9
9
  'HEAD',
@@ -29,16 +29,18 @@ export class MiddlewareRefs {
29
29
  headers;
30
30
  handler;
31
31
  contentType;
32
+ languageTag;
32
33
  writer;
33
34
  req;
34
35
  recordServerTiming;
35
36
  prevTime;
36
37
  serverTimes = [];
37
38
  cacheHitHeader;
38
- constructor(req, writer, contentType, prevTime) {
39
+ constructor(req, writer, contentType, languageTag, prevTime) {
39
40
  this.req = req;
40
41
  this.writer = writer;
41
42
  this.contentType = contentType;
43
+ this.languageTag = languageTag;
42
44
  this.prevTime = prevTime;
43
45
  this.headers = new Headers();
44
46
  }
@@ -59,12 +61,12 @@ export class MiddlewareRefs {
59
61
  export class ActionCore {
60
62
  rootIRI;
61
63
  method;
62
- isSafe = false;
64
+ isSafe;
63
65
  name;
64
66
  uriTemplate;
65
67
  public = false;
66
68
  authKey;
67
- path;
69
+ route;
68
70
  hints = [];
69
71
  transformers = new Map();
70
72
  scope;
@@ -76,8 +78,10 @@ export class ActionCore {
76
78
  cacheOccurance = BeforeDefinition;
77
79
  auth;
78
80
  cache = [];
79
- recordServerTiming = false;
80
- constructor(rootIRI, method, name, uriTemplate, registry, writer, scope) {
81
+ autoLanguageTags;
82
+ autoFileExtensions;
83
+ recordServerTiming;
84
+ constructor(rootIRI, method, name, uriTemplate, registry, writer, scope, autoLanguageTags, autoFileExtensions, recordServerTiming) {
81
85
  this.rootIRI = rootIRI;
82
86
  this.method = method.toUpperCase();
83
87
  this.isSafe = safeMethods.has(this.method);
@@ -86,7 +90,10 @@ export class ActionCore {
86
90
  this.registry = registry;
87
91
  this.writer = writer;
88
92
  this.scope = scope;
89
- this.path = new Path(uriTemplate, rootIRI);
93
+ this.route = new Route(uriTemplate, rootIRI, autoLanguageTags, autoFileExtensions);
94
+ this.autoLanguageTags = autoLanguageTags;
95
+ this.autoFileExtensions = autoFileExtensions;
96
+ this.recordServerTiming = recordServerTiming ?? false;
90
97
  }
91
98
  /**
92
99
  * Called when the API is defined to compute all uncomputed values.
@@ -134,7 +141,7 @@ export class ActionCore {
134
141
  found = when(cacheCtx);
135
142
  }
136
143
  if (found) {
137
- return new CacheDescriptor(contentType, this.action, req, this.cache[i]);
144
+ return new CacheDescriptor(contentType, cacheCtx.languageTag, this.action, req, this.cache[i]);
138
145
  }
139
146
  }
140
147
  return null;
@@ -276,6 +283,7 @@ export class ActionCore {
276
283
  refs.handlerCtx = new Context({
277
284
  req: refs.req,
278
285
  contentType: refs.contentType,
286
+ languageTag: refs.languageTag,
279
287
  public: this.public && refs.authKey == null,
280
288
  auth: refs.auth,
281
289
  authKey: refs.authKey,
@@ -307,6 +315,7 @@ export class ActionCore {
307
315
  refs.cacheCtx = new CacheContext({
308
316
  req: refs.req,
309
317
  contentType: refs.contentType,
318
+ languageTag: refs.languageTag,
310
319
  public: this.public && refs.authKey == null,
311
320
  cacheOperation: refs.cacheOperation,
312
321
  auth: refs.auth,
@@ -340,7 +349,9 @@ export class ActionCore {
340
349
  if (this.hints.length !== 0) {
341
350
  const downstream = refs.next;
342
351
  refs.next = async () => {
343
- await Promise.all(this.hints.map((hint) => refs.writer.writeEarlyHints(hint)));
352
+ for (let i = 0; i < this.hints.length; i++) {
353
+ refs.writer.writeEarlyHints(this.hints[i]);
354
+ }
344
355
  await downstream();
345
356
  };
346
357
  }
@@ -0,0 +1,39 @@
1
+ export type RouteMatchResult = {
2
+ path: Record<string, string>;
3
+ query: Record<string, undefined | string | string[]>;
4
+ };
5
+ /**
6
+ * Util class for handling paths defined using URI templates.
7
+ * https://datatracker.ietf.org/doc/html/rfc6570.
8
+ *
9
+ * URI Templates are used instead of URLPattern format to conform to
10
+ * JSON-ld https://schema.org/entryPoint needs. It is possible to add
11
+ * regex syntax which would end up in the generated URLPattern making
12
+ * it in conflict with the URITemplate. To be fixed...
13
+ */
14
+ export declare class Route {
15
+ #private;
16
+ constructor(template: string, rootURL: string, autoLanguageTag: boolean, autoFileExtension: boolean);
17
+ get template(): string;
18
+ get regexp(): RegExp;
19
+ get pattern(): URLPattern;
20
+ /**
21
+ * A normalized form of the url template where arguments are named
22
+ * in order of appearance instead of with the provided names. This
23
+ * allows two paths to be compared based of their ability to match
24
+ * to the same request.
25
+ */
26
+ get normalized(): string;
27
+ /**
28
+ * Matches a URL against this route and returns the
29
+ * path and query values.
30
+ *
31
+ * @param url The URL to match.
32
+ */
33
+ match(url: string | URL): undefined | RouteMatchResult;
34
+ /**
35
+ * Returns the location of the given key if it is present in
36
+ * the path.
37
+ */
38
+ locationOf(key: string): 'path' | 'query' | 'fragment' | null;
39
+ }
@@ -0,0 +1,166 @@
1
+ import { makeURLPattern } from "../utils/makeURLPattern.js";
2
+ const paramsRe = /((?<s>[^\{\}]+)|({(?<t>[\?\#\.])?(?<v>[^}]+)}))/g;
3
+ const languageTagReStr = '(?:\\.(?<languageTag>[a-zA-Z0-9][a-zA-Z0-9\\-]+))';
4
+ const fileExtensionReStr = '(?:\\.(?<fileExtension>[a-z][a-zA-Z0-9\\-]+))';
5
+ /**
6
+ * Util class for handling paths defined using URI templates.
7
+ * https://datatracker.ietf.org/doc/html/rfc6570.
8
+ *
9
+ * URI Templates are used instead of URLPattern format to conform to
10
+ * JSON-ld https://schema.org/entryPoint needs. It is possible to add
11
+ * regex syntax which would end up in the generated URLPattern making
12
+ * it in conflict with the URITemplate. To be fixed...
13
+ */
14
+ export class Route {
15
+ #rootURL;
16
+ #template;
17
+ #regexp;
18
+ #pattern;
19
+ #normalized;
20
+ #pathKeys = new Set();
21
+ #queryKeys = new Set();
22
+ #fragmentKeys = new Set();
23
+ #autoLanguageTag;
24
+ #autoFileExtension;
25
+ constructor(template, rootURL, autoLanguageTag, autoFileExtension) {
26
+ this.#template = template;
27
+ this.#rootURL = rootURL;
28
+ this.#autoLanguageTag = autoLanguageTag;
29
+ this.#autoFileExtension = autoFileExtension;
30
+ [this.#pattern, this.#normalized] = this.#makePatterns();
31
+ }
32
+ get template() {
33
+ return this.#template;
34
+ }
35
+ get regexp() {
36
+ return this.#regexp;
37
+ }
38
+ get pattern() {
39
+ return this.#pattern;
40
+ }
41
+ /**
42
+ * A normalized form of the url template where arguments are named
43
+ * in order of appearance instead of with the provided names. This
44
+ * allows two paths to be compared based of their ability to match
45
+ * to the same request.
46
+ */
47
+ get normalized() {
48
+ return this.#normalized;
49
+ }
50
+ /**
51
+ * Matches a URL against this route and returns the
52
+ * path and query values.
53
+ *
54
+ * @param url The URL to match.
55
+ */
56
+ match(url) {
57
+ if (!url.toString().startsWith(this.#rootURL)) {
58
+ return;
59
+ }
60
+ const url2 = new URL(url);
61
+ const match = this.#regexp.exec(url2.pathname);
62
+ if (match?.groups == null) {
63
+ return;
64
+ }
65
+ const path = Object.create(null);
66
+ const query = Object.create(null);
67
+ for (const [key, value] of Object.entries(match.groups)) {
68
+ path[key] = value;
69
+ }
70
+ for (const key of url2.searchParams.keys()) {
71
+ const value = url2.searchParams.getAll(key);
72
+ if (value.length > 1) {
73
+ query[key] = value;
74
+ }
75
+ else {
76
+ query[key] = value[0];
77
+ }
78
+ }
79
+ return { path, query };
80
+ }
81
+ /**
82
+ * Returns the location of the given key if it is present in
83
+ * the path.
84
+ */
85
+ locationOf(key) {
86
+ if (this.#pathKeys.has(key)) {
87
+ return 'path';
88
+ }
89
+ else if (this.#queryKeys.has(key)) {
90
+ return 'query';
91
+ }
92
+ else if (this.#fragmentKeys.has(key)) {
93
+ return 'fragment';
94
+ }
95
+ return null;
96
+ }
97
+ #makePatterns() {
98
+ let pattern = '';
99
+ let normalized = '';
100
+ // assign values to key location sets for quick querying
101
+ let match;
102
+ let foundQueryOrFragment = false;
103
+ let index = 0;
104
+ let template = '';
105
+ let regexpStr = '^';
106
+ while ((match = paramsRe.exec(this.#template))) {
107
+ const segment = match.groups?.s;
108
+ const type = match.groups?.t;
109
+ const value = match.groups?.v;
110
+ if (type != null && type !== '.' && !foundQueryOrFragment) {
111
+ foundQueryOrFragment = true;
112
+ if (this.#autoLanguageTag && this.#autoFileExtension) {
113
+ regexpStr += `(${languageTagReStr}?${fileExtensionReStr})?`;
114
+ template += '{.languageTag,fileExtension}';
115
+ }
116
+ else if (this.#autoFileExtension) {
117
+ regexpStr += fileExtensionReStr + '?';
118
+ template += '{.fileExtension}';
119
+ }
120
+ }
121
+ template += match[0];
122
+ if (!foundQueryOrFragment && segment != null && type == null) {
123
+ regexpStr += segment;
124
+ normalized += segment;
125
+ pattern += segment;
126
+ }
127
+ if (value == null) {
128
+ continue;
129
+ }
130
+ if (type === '.' && !foundQueryOrFragment && value != null) {
131
+ index++;
132
+ regexpStr += `(\\.(?<${value}>[^\\/\\.]+))`;
133
+ pattern += `.:${value}`;
134
+ normalized += `.:value${index}`;
135
+ this.#pathKeys.add(value);
136
+ }
137
+ else if (!foundQueryOrFragment && value != null) {
138
+ index++;
139
+ regexpStr += `(?<${value}>[^\\/\\.]+)`;
140
+ pattern += `:${value}`;
141
+ normalized += `:value${index}`;
142
+ this.#pathKeys.add(value);
143
+ }
144
+ }
145
+ if (!foundQueryOrFragment) {
146
+ if (this.#autoLanguageTag && this.#autoFileExtension) {
147
+ regexpStr += `(${languageTagReStr}?${fileExtensionReStr})`;
148
+ template += '{.languageTag,fileExtension}';
149
+ this.#pathKeys.add('languageTag');
150
+ this.#pathKeys.add('fileExtension');
151
+ }
152
+ else if (this.#autoFileExtension) {
153
+ regexpStr += fileExtensionReStr;
154
+ template += '{.fileExtension}';
155
+ this.#pathKeys.add('fileExtension');
156
+ }
157
+ }
158
+ regexpStr += '$';
159
+ this.#template = template;
160
+ this.#regexp = new RegExp(regexpStr);
161
+ return [
162
+ makeURLPattern(pattern, this.#rootURL),
163
+ normalized,
164
+ ];
165
+ }
166
+ }