From 11c0d1d6cf2898efab80c8e3f171f92b3d3b5aaa Mon Sep 17 00:00:00 2001 From: Joachim Van Herwegen Date: Fri, 8 Jul 2022 11:50:19 +0200 Subject: [PATCH] feat: Add contains function to IdentifierStrategy --- .../identifiers/BaseIdentifierStrategy.ts | 24 ++++++++- src/util/identifiers/IdentifierStrategy.ts | 10 ++++ .../BaseIdentifierStrategy.test.ts | 49 ++++++++++++++----- 3 files changed, 70 insertions(+), 13 deletions(-) diff --git a/src/util/identifiers/BaseIdentifierStrategy.ts b/src/util/identifiers/BaseIdentifierStrategy.ts index 9d71543a7..c5c355ca9 100644 --- a/src/util/identifiers/BaseIdentifierStrategy.ts +++ b/src/util/identifiers/BaseIdentifierStrategy.ts @@ -1,12 +1,16 @@ +import { URL } from 'url'; import type { ResourceIdentifier } from '../../http/representation/ResourceIdentifier'; import { InternalServerError } from '../errors/InternalServerError'; -import { ensureTrailingSlash } from '../PathUtil'; +import { ensureTrailingSlash, isContainerIdentifier } from '../PathUtil'; import type { IdentifierStrategy } from './IdentifierStrategy'; /** * Provides a default implementation for `getParentContainer` * which checks if the identifier is supported and not a root container. * If not, the last part before the first relevant slash will be removed to find the parent. + * + * Provides a default implementation for `contains` + * which does standard slash-semantics based string comparison. */ export abstract class BaseIdentifierStrategy implements IdentifierStrategy { public abstract supportsIdentifier(identifier: ResourceIdentifier): boolean; @@ -27,4 +31,22 @@ export abstract class BaseIdentifierStrategy implements IdentifierStrategy { } public abstract isRootContainer(identifier: ResourceIdentifier): boolean; + + public contains(container: ResourceIdentifier, identifier: ResourceIdentifier, transitive: boolean): boolean { + if (!isContainerIdentifier(container)) { + return false; + } + + if (!identifier.path.startsWith(container.path)) { + return false; + } + + if (transitive) { + return true; + } + + const tail = identifier.path.slice(container.path.length); + // If there is at least one `/` followed by a char this is not a direct parent container + return !/\/./u.test(tail); + } } diff --git a/src/util/identifiers/IdentifierStrategy.ts b/src/util/identifiers/IdentifierStrategy.ts index c0907194a..073b05e74 100644 --- a/src/util/identifiers/IdentifierStrategy.ts +++ b/src/util/identifiers/IdentifierStrategy.ts @@ -23,4 +23,14 @@ export interface IdentifierStrategy { * This does not check if this identifier actually exists. */ isRootContainer: (identifier: ResourceIdentifier) => boolean; + + /** + * Checks if the given container would contain the given identifier. + * This does not check that either of these identifiers actually exist. + * This is similar to calling {@link getParentContainer} on an identifier + * and comparing the result. + * + * If `transitive` is `false` this only checks if `container` is the direct parent container of `identifier`. + */ + contains: (container: ResourceIdentifier, identifier: ResourceIdentifier, transitive: boolean) => boolean; } diff --git a/test/unit/util/identifiers/BaseIdentifierStrategy.test.ts b/test/unit/util/identifiers/BaseIdentifierStrategy.test.ts index ccb5a1ca4..7b6539dfe 100644 --- a/test/unit/util/identifiers/BaseIdentifierStrategy.test.ts +++ b/test/unit/util/identifiers/BaseIdentifierStrategy.test.ts @@ -14,20 +14,45 @@ class DummyStrategy extends BaseIdentifierStrategy { describe('A BaseIdentifierStrategy', (): void => { const strategy = new DummyStrategy(); - it('returns the parent identifier.', async(): Promise => { - expect(strategy.getParentContainer({ path: 'http://test.com/foo/bar' })).toEqual({ path: 'http://test.com/foo/' }); - expect(strategy.getParentContainer({ path: 'http://test.com/foo/bar/' })).toEqual({ path: 'http://test.com/foo/' }); + describe('getParentContainer', (): void => { + it('returns the parent identifier.', async(): Promise => { + expect(strategy.getParentContainer({ path: 'http://example.com/foo/bar' })).toEqual({ path: 'http://example.com/foo/' }); + expect(strategy.getParentContainer({ path: 'http://example.com/foo/bar/' })).toEqual({ path: 'http://example.com/foo/' }); + }); + + it('errors when attempting to get the parent of an unsupported identifier.', async(): Promise => { + expect((): any => strategy.getParentContainer({ path: '/unsupported' })) + .toThrow('The identifier /unsupported is outside the configured identifier space.'); + expect((): any => strategy.getParentContainer({ path: '/unsupported' })) + .toThrow(expect.objectContaining({ errorCode: 'E0001', details: { path: '/unsupported' }})); + }); + + it('errors when attempting to get the parent of a root container.', async(): Promise => { + expect((): any => strategy.getParentContainer({ path: 'http://example.com/root' })) + .toThrow('Cannot obtain the parent of http://example.com/root because it is a root container.'); + }); }); - it('errors when attempting to get the parent of an unsupported identifier.', async(): Promise => { - expect((): any => strategy.getParentContainer({ path: '/unsupported' })) - .toThrow('The identifier /unsupported is outside the configured identifier space.'); - expect((): any => strategy.getParentContainer({ path: '/unsupported' })) - .toThrow(expect.objectContaining({ errorCode: 'E0001', details: { path: '/unsupported' }})); - }); + describe('contains', (): void => { + it('returns false if container parameter is not a container identifier.', async(): Promise => { + expect(strategy.contains({ path: 'http://example.com' }, { path: 'http://example.com/foo' }, false)).toBe(false); + }); - it('errors when attempting to get the parent of a root container.', async(): Promise => { - expect((): any => strategy.getParentContainer({ path: 'http://test.com/root' })) - .toThrow('Cannot obtain the parent of http://test.com/root because it is a root container.'); + it('returns false if container parameter is longer.', async(): Promise => { + expect(strategy.contains({ path: 'http://example.com/foo/' }, { path: 'http://example.com' }, false)).toBe(false); + }); + + it('returns false if container parameter is not the direct container.', async(): Promise => { + expect(strategy.contains({ path: 'http://example.com/' }, { path: 'http://example.com/foo/bar' }, false)).toBe(false); + }); + + it('returns true if the container parameter is the direct container.', async(): Promise => { + expect(strategy.contains({ path: 'http://example.com/' }, { path: 'http://example.com/foo/' }, false)).toBe(true); + expect(strategy.contains({ path: 'http://example.com/' }, { path: 'http://example.com/foo' }, false)).toBe(true); + }); + + it('returns true for transtive calls if container parameter is a grandparent.', async(): Promise => { + expect(strategy.contains({ path: 'http://example.com/' }, { path: 'http://example.com/foo/bar' }, true)).toBe(true); + }); }); });