-
Notifications
You must be signed in to change notification settings - Fork 127
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
3 changed files
with
194 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,89 @@ | ||
import arrayifyStream from 'arrayify-stream'; | ||
import { BinaryRepresentation } from '../ldp/representation/BinaryRepresentation'; | ||
import { NotFoundHttpError } from '../util/errors/NotFoundHttpError'; | ||
import { Quad } from 'rdf-js'; | ||
import { QuadRepresentation } from '../ldp/representation/QuadRepresentation'; | ||
import { Readable } from 'stream'; | ||
import { Representation } from '../ldp/representation/Representation'; | ||
import { RepresentationPreferences } from '../ldp/representation/RepresentationPreferences'; | ||
import { ResourceIdentifier } from '../ldp/representation/ResourceIdentifier'; | ||
import { ResourceStore } from './ResourceStore'; | ||
import streamifyArray from 'streamify-array'; | ||
import { StreamWriter } from 'n3'; | ||
import { UnsupportedMediaTypeHttpError } from '../util/errors/UnsupportedMediaTypeHttpError'; | ||
|
||
export class SimpleResourceStore implements ResourceStore { | ||
private readonly store: { [id: string]: Quad[] } = { '': []}; | ||
private readonly base: string; | ||
private index = 0; | ||
|
||
public constructor(base: string) { | ||
this.base = base; | ||
} | ||
|
||
public async addResource(container: ResourceIdentifier, representation: Representation): Promise<ResourceIdentifier> { | ||
const containerPath = this.parseIdentifier(container); | ||
const newPath = `${containerPath}/${this.index}`; | ||
this.index += 1; | ||
this.store[newPath] = await this.parseRepresentation(representation); | ||
return { path: `${this.base}${newPath}` }; | ||
} | ||
|
||
public async deleteResource(identifier: ResourceIdentifier): Promise<void> { | ||
const path = this.parseIdentifier(identifier); | ||
// eslint-disable-next-line @typescript-eslint/no-dynamic-delete | ||
delete this.store[path]; | ||
} | ||
|
||
public async getRepresentation(identifier: ResourceIdentifier, preferences: RepresentationPreferences): Promise<Representation> { | ||
const path = this.parseIdentifier(identifier); | ||
return this.generateRepresentation(this.store[path], preferences); | ||
} | ||
|
||
public async modifyResource(): Promise<void> { | ||
throw new Error('Not supported.'); | ||
} | ||
|
||
public async setRepresentation(identifier: ResourceIdentifier, representation: Representation): Promise<void> { | ||
const path = this.parseIdentifier(identifier); | ||
this.store[path] = await this.parseRepresentation(representation); | ||
} | ||
|
||
private parseIdentifier(identifier: ResourceIdentifier): string { | ||
const path = identifier.path.slice(this.base.length); | ||
if (!this.store[path] || !identifier.path.startsWith(this.base)) { | ||
throw new NotFoundHttpError(); | ||
} | ||
return path; | ||
} | ||
|
||
private async parseRepresentation(representation: Representation): Promise<Quad[]> { | ||
if (representation.dataType !== 'quad') { | ||
throw new UnsupportedMediaTypeHttpError('SimpleResourceStore only supports quad representations.'); | ||
} | ||
return arrayifyStream(representation.data); | ||
} | ||
|
||
private generateRepresentation(data: Quad[], preferences: RepresentationPreferences): Representation { | ||
if (preferences.type && preferences.type.some((preference): boolean => preference.value.includes('text/turtle'))) { | ||
return this.generateBinaryRepresentation(data); | ||
} | ||
return this.generateQuadRepresentation(data); | ||
} | ||
|
||
private generateBinaryRepresentation(data: Quad[]): BinaryRepresentation { | ||
return { | ||
dataType: 'binary', | ||
data: streamifyArray(data).pipe(new StreamWriter({ format: 'text/turtle' })) as unknown as Readable, | ||
metadata: { raw: [], profiles: [], contentType: 'text/turtle' }, | ||
}; | ||
} | ||
|
||
private generateQuadRepresentation(data: Quad[]): QuadRepresentation { | ||
return { | ||
dataType: 'quad', | ||
data: streamifyArray(data), | ||
metadata: { raw: [], profiles: []}, | ||
}; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,7 @@ | ||
import { HttpError } from './HttpError'; | ||
|
||
export class NotFoundHttpError extends HttpError { | ||
public constructor(message?: string) { | ||
super(404, 'NotFoundHttpError', message); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,98 @@ | ||
import arrayifyStream from 'arrayify-stream'; | ||
import { NotFoundHttpError } from '../../../src/util/errors/NotFoundHttpError'; | ||
import { QuadRepresentation } from '../../../src/ldp/representation/QuadRepresentation'; | ||
import { Readable } from 'stream'; | ||
import { SimpleResourceStore } from '../../../src/storage/SimpleResourceStore'; | ||
import streamifyArray from 'streamify-array'; | ||
import { UnsupportedMediaTypeHttpError } from '../../../src/util/errors/UnsupportedMediaTypeHttpError'; | ||
import { namedNode, triple } from '@rdfjs/data-model'; | ||
|
||
const base = 'http://test.com/'; | ||
|
||
describe('A SimpleResourceStore', (): void => { | ||
let store: SimpleResourceStore; | ||
let representation: QuadRepresentation; | ||
const quad = triple( | ||
namedNode('http://test.com/s'), | ||
namedNode('http://test.com/p'), | ||
namedNode('http://test.com/o'), | ||
); | ||
|
||
beforeEach(async(): Promise<void> => { | ||
store = new SimpleResourceStore(base); | ||
|
||
representation = { | ||
data: streamifyArray([ quad ]), | ||
dataType: 'quad', | ||
metadata: null, | ||
}; | ||
}); | ||
|
||
it('errors if a resource was not found.', async(): Promise<void> => { | ||
await expect(store.getRepresentation({ path: `${base}wrong` }, {})).rejects.toThrow(NotFoundHttpError); | ||
await expect(store.addResource({ path: 'http://wrong.com/wrong' }, null)).rejects.toThrow(NotFoundHttpError); | ||
await expect(store.deleteResource({ path: 'wrong' })).rejects.toThrow(NotFoundHttpError); | ||
await expect(store.setRepresentation({ path: 'http://wrong.com/' }, null)).rejects.toThrow(NotFoundHttpError); | ||
}); | ||
|
||
it('errors when modifying resources.', async(): Promise<void> => { | ||
await expect(store.modifyResource()).rejects.toThrow(Error); | ||
}); | ||
|
||
it('errors for wrong input data types.', async(): Promise<void> => { | ||
(representation as any).dataType = 'binary'; | ||
await expect(store.addResource({ path: base }, representation)).rejects.toThrow(UnsupportedMediaTypeHttpError); | ||
}); | ||
|
||
it('can write and read data.', async(): Promise<void> => { | ||
const identifier = await store.addResource({ path: base }, representation); | ||
expect(identifier.path.startsWith(base)).toBeTruthy(); | ||
const result = await store.getRepresentation(identifier, {}); | ||
expect(result).toEqual({ | ||
dataType: 'quad', | ||
data: expect.any(Readable), | ||
metadata: { | ||
profiles: [], | ||
raw: [], | ||
}, | ||
}); | ||
await expect(arrayifyStream(result.data)).resolves.toEqualRdfQuadArray([ quad ]); | ||
}); | ||
|
||
it('can read binary data.', async(): Promise<void> => { | ||
const identifier = await store.addResource({ path: base }, representation); | ||
expect(identifier.path.startsWith(base)).toBeTruthy(); | ||
const result = await store.getRepresentation(identifier, { type: [{ value: 'text/turtle', weight: 1 }]}); | ||
expect(result).toEqual({ | ||
dataType: 'binary', | ||
data: expect.any(Readable), | ||
metadata: { | ||
profiles: [], | ||
raw: [], | ||
contentType: 'text/turtle', | ||
}, | ||
}); | ||
await expect(arrayifyStream(result.data)).resolves.toContain( | ||
`<${quad.subject.value}> <${quad.predicate.value}> <${quad.object.value}>`, | ||
); | ||
}); | ||
|
||
it('can set data.', async(): Promise<void> => { | ||
await store.setRepresentation({ path: base }, representation); | ||
const result = await store.getRepresentation({ path: base }, {}); | ||
expect(result).toEqual({ | ||
dataType: 'quad', | ||
data: expect.any(Readable), | ||
metadata: { | ||
profiles: [], | ||
raw: [], | ||
}, | ||
}); | ||
await expect(arrayifyStream(result.data)).resolves.toEqualRdfQuadArray([ quad ]); | ||
}); | ||
|
||
it('can delete data.', async(): Promise<void> => { | ||
await store.deleteResource({ path: base }); | ||
await expect(store.getRepresentation({ path: base }, {})).rejects.toThrow(NotFoundHttpError); | ||
}); | ||
}); |