feat: Add contains function to IdentifierStrategy

This commit is contained in:
Joachim Van Herwegen 2022-07-08 11:50:19 +02:00
parent 18391ec414
commit 11c0d1d6cf
3 changed files with 70 additions and 13 deletions

View File

@ -1,12 +1,16 @@
import { URL } from 'url';
import type { ResourceIdentifier } from '../../http/representation/ResourceIdentifier'; import type { ResourceIdentifier } from '../../http/representation/ResourceIdentifier';
import { InternalServerError } from '../errors/InternalServerError'; import { InternalServerError } from '../errors/InternalServerError';
import { ensureTrailingSlash } from '../PathUtil'; import { ensureTrailingSlash, isContainerIdentifier } from '../PathUtil';
import type { IdentifierStrategy } from './IdentifierStrategy'; import type { IdentifierStrategy } from './IdentifierStrategy';
/** /**
* Provides a default implementation for `getParentContainer` * Provides a default implementation for `getParentContainer`
* which checks if the identifier is supported and not a root container. * 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. * 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 { export abstract class BaseIdentifierStrategy implements IdentifierStrategy {
public abstract supportsIdentifier(identifier: ResourceIdentifier): boolean; public abstract supportsIdentifier(identifier: ResourceIdentifier): boolean;
@ -27,4 +31,22 @@ export abstract class BaseIdentifierStrategy implements IdentifierStrategy {
} }
public abstract isRootContainer(identifier: ResourceIdentifier): boolean; 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);
}
} }

View File

@ -23,4 +23,14 @@ export interface IdentifierStrategy {
* This does not check if this identifier actually exists. * This does not check if this identifier actually exists.
*/ */
isRootContainer: (identifier: ResourceIdentifier) => boolean; 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;
} }

View File

@ -14,20 +14,45 @@ class DummyStrategy extends BaseIdentifierStrategy {
describe('A BaseIdentifierStrategy', (): void => { describe('A BaseIdentifierStrategy', (): void => {
const strategy = new DummyStrategy(); const strategy = new DummyStrategy();
it('returns the parent identifier.', async(): Promise<void> => { describe('getParentContainer', (): void => {
expect(strategy.getParentContainer({ path: 'http://test.com/foo/bar' })).toEqual({ path: 'http://test.com/foo/' }); it('returns the parent identifier.', async(): Promise<void> => {
expect(strategy.getParentContainer({ path: 'http://test.com/foo/bar/' })).toEqual({ path: 'http://test.com/foo/' }); 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<void> => {
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<void> => {
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<void> => { describe('contains', (): void => {
expect((): any => strategy.getParentContainer({ path: '/unsupported' })) it('returns false if container parameter is not a container identifier.', async(): Promise<void> => {
.toThrow('The identifier /unsupported is outside the configured identifier space.'); expect(strategy.contains({ path: 'http://example.com' }, { path: 'http://example.com/foo' }, false)).toBe(false);
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<void> => { it('returns false if container parameter is longer.', async(): Promise<void> => {
expect((): any => strategy.getParentContainer({ path: 'http://test.com/root' })) expect(strategy.contains({ path: 'http://example.com/foo/' }, { path: 'http://example.com' }, false)).toBe(false);
.toThrow('Cannot obtain the parent of http://test.com/root because it is a root container.'); });
it('returns false if container parameter is not the direct container.', async(): Promise<void> => {
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<void> => {
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<void> => {
expect(strategy.contains({ path: 'http://example.com/' }, { path: 'http://example.com/foo/bar' }, true)).toBe(true);
});
}); });
}); });