From 5399e75ae4917f88b0909bf04849d51039ab5189 Mon Sep 17 00:00:00 2001 From: Thomas Dupont Date: Fri, 13 May 2022 15:49:47 +0200 Subject: [PATCH] feat: regexRoutes stored as ordered array --- RELEASE_NOTES.md | 5 +++- config/sparql-file-storage.json | 12 ++++++---- config/storage/backend/regex.json | 22 ++++++++++------- src/storage/routing/RegexRouterRule.ts | 24 ++++++++++++++----- .../storage/routing/RegexRouterRule.test.ts | 17 +++++++++---- 5 files changed, 54 insertions(+), 26 deletions(-) diff --git a/RELEASE_NOTES.md b/RELEASE_NOTES.md index 758a84b66..5fec6a695 100644 --- a/RELEASE_NOTES.md +++ b/RELEASE_NOTES.md @@ -24,7 +24,7 @@ The following changes pertain to the imports in the default configs: - All default configurations with a file-based backend now use a file-based locker instead of a memory-based one, making them threadsafe. -The following changes are relevant for v3 custom configs that replaced certain features. +The following changes are relevant for v4 custom configs that replaced certain features. - `config/app/variables/cli.json` was changed to support the new `YargsCliExtractor` format. - `config/util/resource-locker/memory.json` had the locker @type changed from `SingleThreadedResourceLocker` to `MemoryResourceLocker`. - The content-length parser has been moved from the default configuration to the quota configurations. @@ -33,6 +33,9 @@ The following changes are relevant for v3 custom configs that replaced certain f - `/storage/backend/quota/quota-file.json` - The structure of the init configs has changed significantly to support worker threads. - `/app/init/*` +- RegexPathRouting has changed from a map datastructure to an array datastructure, allowing for fallthrough regex parsing. The change is reflected in the following default configs: + - `/storage/backend/regex.json` + - `/sparql-file-storage.json` ### Interface changes These changes are relevant if you wrote custom modules for the server that depend on existing interfaces. diff --git a/config/sparql-file-storage.json b/config/sparql-file-storage.json index 1196306ed..6a22698d2 100644 --- a/config/sparql-file-storage.json +++ b/config/sparql-file-storage.json @@ -51,16 +51,18 @@ "@id": "urn:solid-server:default:RouterRule", "@type": "RegexRouterRule", "base": { "@id": "urn:solid-server:default:variable:baseUrl" }, - "storeMap": [ + "rules": [ { "comment": "Internal storage data", - "RegexRouterRule:_storeMap_key": "^/\\.internal/", - "RegexRouterRule:_storeMap_value": { "@id": "urn:solid-server:default:FileResourceStore" } + "@type": "RegexRule", + "regex": "^/\\.internal/", + "store": { "@id": "urn:solid-server:default:FileResourceStore" } }, { "comment": "Send everything else to the SPARQL store.", - "RegexRouterRule:_storeMap_key": "^/(?!\\.internal/).*", - "RegexRouterRule:_storeMap_value": { "@id": "urn:solid-server:default:SparqlResourceStore" } + "@type": "RegexRule", + "regex": ".*", + "store": { "@id": "urn:solid-server:default:SparqlResourceStore" } } ] }, diff --git a/config/storage/backend/regex.json b/config/storage/backend/regex.json index 3a96faee8..b4699f8ee 100644 --- a/config/storage/backend/regex.json +++ b/config/storage/backend/regex.json @@ -21,22 +21,26 @@ "@id": "urn:solid-server:default:RouterRule", "@type": "RegexRouterRule", "base": { "@id": "urn:solid-server:default:variable:baseUrl" }, - "storeMap": [ + "rules": [ { - "RegexRouterRule:_storeMap_key": "^/(\\.acl)?$", - "RegexRouterRule:_storeMap_value": { "@id": "urn:solid-server:default:SparqlResourceStore" } + "@type": "RegexRule", + "regex": "^/(\\.acl)?$", + "store": { "@id": "urn:solid-server:default:SparqlResourceStore" } }, { - "RegexRouterRule:_storeMap_key": "/file/", - "RegexRouterRule:_storeMap_value": { "@id": "urn:solid-server:default:FileResourceStore" } + "@type": "RegexRule", + "regex": "/file/", + "store": { "@id": "urn:solid-server:default:FileResourceStore" } }, { - "RegexRouterRule:_storeMap_key": "/memory/", - "RegexRouterRule:_storeMap_value": { "@id": "urn:solid-server:default:MemoryResourceStore" } + "@type": "RegexRule", + "regex": "/memory/", + "store": { "@id": "urn:solid-server:default:MemoryResourceStore" } }, { - "RegexRouterRule:_storeMap_key": "/sparql/", - "RegexRouterRule:_storeMap_value": { "@id": "urn:solid-server:default:SparqlResourceStore" } + "@type": "RegexRule", + "regex": "/sparql/", + "store": { "@id": "urn:solid-server:default:SparqlResourceStore" } } ] }, diff --git a/src/storage/routing/RegexRouterRule.ts b/src/storage/routing/RegexRouterRule.ts index 4f8a58a8f..3f50b0e3d 100644 --- a/src/storage/routing/RegexRouterRule.ts +++ b/src/storage/routing/RegexRouterRule.ts @@ -6,6 +6,19 @@ import { trimTrailingSlashes } from '../../util/PathUtil'; import type { ResourceStore } from '../ResourceStore'; import { RouterRule } from './RouterRule'; +/** + * Utility class to easily configure Regex to ResourceStore mappings in the config files. + */ +export class RegexRule { + public readonly regex: RegExp; + public readonly store: ResourceStore; + + public constructor(regex: string, store: ResourceStore) { + this.regex = new RegExp(regex, 'u'); + this.store = store; + } +} + /** * Routes requests to a store based on the path of the identifier. * The identifier will be stripped of the base URI after which regexes will be used to find the correct store. @@ -16,16 +29,15 @@ import { RouterRule } from './RouterRule'; */ export class RegexRouterRule extends RouterRule { private readonly base: string; - private readonly regexes: Map; + private readonly rules: RegexRule[]; /** * The keys of the `storeMap` will be converted into actual RegExp objects that will be used for testing. */ - public constructor(base: string, storeMap: Record) { + public constructor(base: string, rules: RegexRule[]) { super(); this.base = trimTrailingSlashes(base); - this.regexes = new Map(Object.keys(storeMap).map((regex): [ RegExp, ResourceStore ] => - [ new RegExp(regex, 'u'), storeMap[regex] ])); + this.rules = rules; } public async canHandle(input: { identifier: ResourceIdentifier; representation?: Representation }): Promise { @@ -42,9 +54,9 @@ export class RegexRouterRule extends RouterRule { */ private matchStore(identifier: ResourceIdentifier): ResourceStore { const path = this.toRelative(identifier); - for (const regex of this.regexes.keys()) { + for (const { regex, store } of this.rules) { if (regex.test(path)) { - return this.regexes.get(regex)!; + return store; } } throw new NotImplementedHttpError(`No stored regexes match ${identifier.path}`); diff --git a/test/unit/storage/routing/RegexRouterRule.test.ts b/test/unit/storage/routing/RegexRouterRule.test.ts index 0f1705071..d8be0b7e1 100644 --- a/test/unit/storage/routing/RegexRouterRule.test.ts +++ b/test/unit/storage/routing/RegexRouterRule.test.ts @@ -1,5 +1,5 @@ import type { ResourceStore } from '../../../../src/storage/ResourceStore'; -import { RegexRouterRule } from '../../../../src/storage/routing/RegexRouterRule'; +import { RegexRouterRule, RegexRule } from '../../../../src/storage/routing/RegexRouterRule'; import { BadRequestHttpError } from '../../../../src/util/errors/BadRequestHttpError'; import { NotImplementedHttpError } from '../../../../src/util/errors/NotImplementedHttpError'; @@ -7,29 +7,36 @@ describe('A RegexRouterRule', (): void => { const base = 'http://test.com/'; const store: ResourceStore = 'resourceStore' as any; + it('can construct a RegexRule utility object.', (): void => { + const regex = '/myPath/'; + const rule = new RegexRule(regex, store); + expect(rule.regex).toEqual(new RegExp(regex, 'u')); + expect(rule.store).toEqual(store); + }); + it('rejects identifiers not containing the base.', async(): Promise => { - const router = new RegexRouterRule(base, {}); + const router = new RegexRouterRule(base, []); const result = router.canHandle({ identifier: { path: 'http://notTest.com/apple' }}); await expect(result).rejects.toThrow(BadRequestHttpError); await expect(result).rejects.toThrow('Identifiers need to start with http://test.com'); }); it('rejects identifiers not matching any regex.', async(): Promise => { - const router = new RegexRouterRule(base, { pear: store }); + const router = new RegexRouterRule(base, [ new RegexRule('pear', store) ]); const result = router.canHandle({ identifier: { path: `${base}apple/` }}); await expect(result).rejects.toThrow(NotImplementedHttpError); await expect(result).rejects.toThrow('No stored regexes match http://test.com/apple/'); }); it('accepts identifiers matching any regex.', async(): Promise => { - const router = new RegexRouterRule(base, { '^/apple': store }); + const router = new RegexRouterRule(base, [ new RegexRule('^/apple', store) ]); await expect(router.canHandle({ identifier: { path: `${base}apple/` }})) .resolves.toBeUndefined(); }); it('returns the corresponding store.', async(): Promise => { const store2: ResourceStore = 'resourceStore2' as any; - const router = new RegexRouterRule(base, { '^/apple': store2, '/pear/': store }); + const router = new RegexRouterRule(base, [ new RegexRule('^/apple', store2), new RegexRule('/pear/', store) ]); await expect(router.handle({ identifier: { path: `${base}apple/` }})).resolves.toBe(store2); }); });