feat: regexRoutes stored as ordered array

This commit is contained in:
Thomas Dupont 2022-05-13 15:49:47 +02:00 committed by Joachim Van Herwegen
parent afed963a23
commit 5399e75ae4
5 changed files with 54 additions and 26 deletions

View File

@ -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, - All default configurations with a file-based backend now use a file-based locker instead of a memory-based one,
making them threadsafe. 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/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`. - `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. - 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` - `/storage/backend/quota/quota-file.json`
- The structure of the init configs has changed significantly to support worker threads. - The structure of the init configs has changed significantly to support worker threads.
- `/app/init/*` - `/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 ### Interface changes
These changes are relevant if you wrote custom modules for the server that depend on existing interfaces. These changes are relevant if you wrote custom modules for the server that depend on existing interfaces.

View File

@ -51,16 +51,18 @@
"@id": "urn:solid-server:default:RouterRule", "@id": "urn:solid-server:default:RouterRule",
"@type": "RegexRouterRule", "@type": "RegexRouterRule",
"base": { "@id": "urn:solid-server:default:variable:baseUrl" }, "base": { "@id": "urn:solid-server:default:variable:baseUrl" },
"storeMap": [ "rules": [
{ {
"comment": "Internal storage data", "comment": "Internal storage data",
"RegexRouterRule:_storeMap_key": "^/\\.internal/", "@type": "RegexRule",
"RegexRouterRule:_storeMap_value": { "@id": "urn:solid-server:default:FileResourceStore" } "regex": "^/\\.internal/",
"store": { "@id": "urn:solid-server:default:FileResourceStore" }
}, },
{ {
"comment": "Send everything else to the SPARQL store.", "comment": "Send everything else to the SPARQL store.",
"RegexRouterRule:_storeMap_key": "^/(?!\\.internal/).*", "@type": "RegexRule",
"RegexRouterRule:_storeMap_value": { "@id": "urn:solid-server:default:SparqlResourceStore" } "regex": ".*",
"store": { "@id": "urn:solid-server:default:SparqlResourceStore" }
} }
] ]
}, },

View File

@ -21,22 +21,26 @@
"@id": "urn:solid-server:default:RouterRule", "@id": "urn:solid-server:default:RouterRule",
"@type": "RegexRouterRule", "@type": "RegexRouterRule",
"base": { "@id": "urn:solid-server:default:variable:baseUrl" }, "base": { "@id": "urn:solid-server:default:variable:baseUrl" },
"storeMap": [ "rules": [
{ {
"RegexRouterRule:_storeMap_key": "^/(\\.acl)?$", "@type": "RegexRule",
"RegexRouterRule:_storeMap_value": { "@id": "urn:solid-server:default:SparqlResourceStore" } "regex": "^/(\\.acl)?$",
"store": { "@id": "urn:solid-server:default:SparqlResourceStore" }
}, },
{ {
"RegexRouterRule:_storeMap_key": "/file/", "@type": "RegexRule",
"RegexRouterRule:_storeMap_value": { "@id": "urn:solid-server:default:FileResourceStore" } "regex": "/file/",
"store": { "@id": "urn:solid-server:default:FileResourceStore" }
}, },
{ {
"RegexRouterRule:_storeMap_key": "/memory/", "@type": "RegexRule",
"RegexRouterRule:_storeMap_value": { "@id": "urn:solid-server:default:MemoryResourceStore" } "regex": "/memory/",
"store": { "@id": "urn:solid-server:default:MemoryResourceStore" }
}, },
{ {
"RegexRouterRule:_storeMap_key": "/sparql/", "@type": "RegexRule",
"RegexRouterRule:_storeMap_value": { "@id": "urn:solid-server:default:SparqlResourceStore" } "regex": "/sparql/",
"store": { "@id": "urn:solid-server:default:SparqlResourceStore" }
} }
] ]
}, },

View File

@ -6,6 +6,19 @@ import { trimTrailingSlashes } from '../../util/PathUtil';
import type { ResourceStore } from '../ResourceStore'; import type { ResourceStore } from '../ResourceStore';
import { RouterRule } from './RouterRule'; 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. * 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. * 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 { export class RegexRouterRule extends RouterRule {
private readonly base: string; private readonly base: string;
private readonly regexes: Map<RegExp, ResourceStore>; private readonly rules: RegexRule[];
/** /**
* The keys of the `storeMap` will be converted into actual RegExp objects that will be used for testing. * The keys of the `storeMap` will be converted into actual RegExp objects that will be used for testing.
*/ */
public constructor(base: string, storeMap: Record<string, ResourceStore>) { public constructor(base: string, rules: RegexRule[]) {
super(); super();
this.base = trimTrailingSlashes(base); this.base = trimTrailingSlashes(base);
this.regexes = new Map(Object.keys(storeMap).map((regex): [ RegExp, ResourceStore ] => this.rules = rules;
[ new RegExp(regex, 'u'), storeMap[regex] ]));
} }
public async canHandle(input: { identifier: ResourceIdentifier; representation?: Representation }): Promise<void> { public async canHandle(input: { identifier: ResourceIdentifier; representation?: Representation }): Promise<void> {
@ -42,9 +54,9 @@ export class RegexRouterRule extends RouterRule {
*/ */
private matchStore(identifier: ResourceIdentifier): ResourceStore { private matchStore(identifier: ResourceIdentifier): ResourceStore {
const path = this.toRelative(identifier); const path = this.toRelative(identifier);
for (const regex of this.regexes.keys()) { for (const { regex, store } of this.rules) {
if (regex.test(path)) { if (regex.test(path)) {
return this.regexes.get(regex)!; return store;
} }
} }
throw new NotImplementedHttpError(`No stored regexes match ${identifier.path}`); throw new NotImplementedHttpError(`No stored regexes match ${identifier.path}`);

View File

@ -1,5 +1,5 @@
import type { ResourceStore } from '../../../../src/storage/ResourceStore'; 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 { BadRequestHttpError } from '../../../../src/util/errors/BadRequestHttpError';
import { NotImplementedHttpError } from '../../../../src/util/errors/NotImplementedHttpError'; import { NotImplementedHttpError } from '../../../../src/util/errors/NotImplementedHttpError';
@ -7,29 +7,36 @@ describe('A RegexRouterRule', (): void => {
const base = 'http://test.com/'; const base = 'http://test.com/';
const store: ResourceStore = 'resourceStore' as any; 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<void> => { it('rejects identifiers not containing the base.', async(): Promise<void> => {
const router = new RegexRouterRule(base, {}); const router = new RegexRouterRule(base, []);
const result = router.canHandle({ identifier: { path: 'http://notTest.com/apple' }}); const result = router.canHandle({ identifier: { path: 'http://notTest.com/apple' }});
await expect(result).rejects.toThrow(BadRequestHttpError); await expect(result).rejects.toThrow(BadRequestHttpError);
await expect(result).rejects.toThrow('Identifiers need to start with http://test.com'); await expect(result).rejects.toThrow('Identifiers need to start with http://test.com');
}); });
it('rejects identifiers not matching any regex.', async(): Promise<void> => { it('rejects identifiers not matching any regex.', async(): Promise<void> => {
const router = new RegexRouterRule(base, { pear: store }); const router = new RegexRouterRule(base, [ new RegexRule('pear', store) ]);
const result = router.canHandle({ identifier: { path: `${base}apple/` }}); const result = router.canHandle({ identifier: { path: `${base}apple/` }});
await expect(result).rejects.toThrow(NotImplementedHttpError); await expect(result).rejects.toThrow(NotImplementedHttpError);
await expect(result).rejects.toThrow('No stored regexes match http://test.com/apple/'); await expect(result).rejects.toThrow('No stored regexes match http://test.com/apple/');
}); });
it('accepts identifiers matching any regex.', async(): Promise<void> => { it('accepts identifiers matching any regex.', async(): Promise<void> => {
const router = new RegexRouterRule(base, { '^/apple': store }); const router = new RegexRouterRule(base, [ new RegexRule('^/apple', store) ]);
await expect(router.canHandle({ identifier: { path: `${base}apple/` }})) await expect(router.canHandle({ identifier: { path: `${base}apple/` }}))
.resolves.toBeUndefined(); .resolves.toBeUndefined();
}); });
it('returns the corresponding store.', async(): Promise<void> => { it('returns the corresponding store.', async(): Promise<void> => {
const store2: ResourceStore = 'resourceStore2' as any; 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); await expect(router.handle({ identifier: { path: `${base}apple/` }})).resolves.toBe(store2);
}); });
}); });