diff --git a/README.md b/README.md index 967d42b83..9e693958f 100644 --- a/README.md +++ b/README.md @@ -68,6 +68,12 @@ $ curl -H "Accept: text/turtle" \ http://localhost:3000/myfile.ttl ``` +Retrieve a turtle file in a different serialization: +```bash +$ curl -H "Accept: application/ld+json" \ + http://localhost:3000/myfile.ttl +``` + ### `DELETE`: Deleting resources ```bash diff --git a/config/presets/storage_wrapper.json b/config/presets/storage_wrapper.json index 6236350ab..dce8d1f24 100644 --- a/config/presets/storage_wrapper.json +++ b/config/presets/storage_wrapper.json @@ -37,7 +37,7 @@ "@type": "RdfToQuadConverter" }, { - "@type": "QuadToTurtleConverter" + "@type": "QuadToRdfConverter" } ] } diff --git a/index.ts b/index.ts index 0083490b1..64ef4c048 100644 --- a/index.ts +++ b/index.ts @@ -65,6 +65,7 @@ export * from './src/server/HttpRequest'; export * from './src/server/HttpResponse'; // Storage/Conversion +export * from './src/storage/conversion/QuadToRdfConverter'; export * from './src/storage/conversion/RdfToQuadConverter'; export * from './src/storage/conversion/QuadToTurtleConverter'; export * from './src/storage/conversion/RepresentationConverter'; diff --git a/package-lock.json b/package-lock.json index 28e77fb85..fa68d62cd 100644 --- a/package-lock.json +++ b/package-lock.json @@ -507,6 +507,25 @@ "rdfa-streaming-parser": "^1.1.1" } }, + "@comunica/actor-rdf-serialize-jsonld": { + "version": "1.15.0", + "resolved": "https://registry.npmjs.org/@comunica/actor-rdf-serialize-jsonld/-/actor-rdf-serialize-jsonld-1.15.0.tgz", + "integrity": "sha512-+QeLhBWY9Ce0sNW6yDm7GoEdFNlMsQ01k71yBhaBRPhe/gYEbJc0chZAUoByCY0dJRqtfZK1Wc5gjfTrG/ctdQ==", + "requires": { + "jsonld-streaming-serializer": "^1.1.0" + } + }, + "@comunica/actor-rdf-serialize-n3": { + "version": "1.15.0", + "resolved": "https://registry.npmjs.org/@comunica/actor-rdf-serialize-n3/-/actor-rdf-serialize-n3-1.15.0.tgz", + "integrity": "sha512-/9wY7o875w103A9a/SNpk65rFcp+bT3mSOnjV1bUnMVhvy73AsRG88uiwGUbS6GDFBPzA2j/l8OD+I4U3j6I7Q==", + "requires": { + "@types/n3": "^1.4.2", + "@types/rdf-js": "^3.0.0", + "n3": "^1.0.0", + "rdf-string": "^1.4.2" + } + }, "@comunica/bus-http": { "version": "1.16.0", "resolved": "https://registry.npmjs.org/@comunica/bus-http/-/bus-http-1.16.0.tgz", @@ -545,6 +564,15 @@ "@types/rdf-js": "^3.0.0" } }, + "@comunica/bus-rdf-serialize": { + "version": "1.15.0", + "resolved": "https://registry.npmjs.org/@comunica/bus-rdf-serialize/-/bus-rdf-serialize-1.15.0.tgz", + "integrity": "sha512-c1uJF1LkJ96zscMCe+CB2fLbXhlJ0o8PPVRMm3Jk7/rc8WY5bUxSxf1SFbA/jkOZtcZy59wFHDvPf/NM74ADBg==", + "requires": { + "@comunica/actor-abstract-mediatyped": "^1.15.0", + "@types/rdf-js": "^3.0.0" + } + }, "@comunica/core": { "version": "1.15.0", "resolved": "https://registry.npmjs.org/@comunica/core/-/core-1.15.0.tgz", @@ -6636,6 +6664,25 @@ "jsonparse": "^1.3.1" } }, + "jsonld-streaming-serializer": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/jsonld-streaming-serializer/-/jsonld-streaming-serializer-1.1.0.tgz", + "integrity": "sha512-C+cs913C3XDScZIqUL8fg5crHQtPTQSZstzvFmhA9/r0QBCRw88BR4TYHvLNhJhzB45GOpoF5/Fx4I4xfKGpOQ==", + "requires": { + "@types/rdf-js": "^2.0.1", + "jsonld-context-parser": "^2.0.0" + }, + "dependencies": { + "@types/rdf-js": { + "version": "2.0.12", + "resolved": "https://registry.npmjs.org/@types/rdf-js/-/rdf-js-2.0.12.tgz", + "integrity": "sha512-NBzHFHp2vHOJkPlSqzsOrkEsD9grKn+PdFjZieIw59pc0FlRG6WEQAjQZvHzFxJlYzC6ZDCFyTA1wBvUnnzUQw==", + "requires": { + "@types/node": "*" + } + } + } + }, "jsonparse": { "version": "1.3.1", "resolved": "https://registry.npmjs.org/jsonparse/-/jsonparse-1.3.1.tgz", @@ -7811,6 +7858,23 @@ "stream-to-string": "^1.2.0" } }, + "rdf-serialize": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/rdf-serialize/-/rdf-serialize-1.0.0.tgz", + "integrity": "sha512-5OZ7qAKKosAumdVMY7EccR9ICqBq90z78E5PuUMX2ThZ1ezkjQCKmqrJC/h3EfpPCdRbTsqwNGM+i6lYI3wQ3A==", + "requires": { + "@comunica/actor-rdf-serialize-jsonld": "~1.15.0", + "@comunica/actor-rdf-serialize-n3": "~1.15.0", + "@comunica/bus-init": "~1.15.0", + "@comunica/bus-rdf-serialize": "~1.15.0", + "@comunica/core": "~1.15.0", + "@comunica/mediator-combine-union": "~1.15.0", + "@comunica/mediator-number": "~1.15.0", + "@comunica/mediator-race": "~1.15.0", + "@types/rdf-js": "*", + "stream-to-string": "^1.2.0" + } + }, "rdf-string": { "version": "1.4.2", "resolved": "https://registry.npmjs.org/rdf-string/-/rdf-string-1.4.2.tgz", diff --git a/package.json b/package.json index 4684769ff..2908b90fe 100644 --- a/package.json +++ b/package.json @@ -59,6 +59,7 @@ "mime-types": "^2.1.27", "n3": "^1.4.0", "rdf-parse": "^1.5.0", + "rdf-serialize": "^1.0.0", "rdf-terms": "^1.5.1", "sparqlalgebrajs": "^2.3.1", "uuid": "^8.3.0", @@ -86,6 +87,7 @@ "node-mocks-http": "^1.8.1", "nodemon": "^2.0.4", "streamify-array": "^1.0.1", + "stream-to-string": "^1.1.0", "supertest": "^4.0.2", "ts-jest": "^26.0.0", "typescript": "^3.9.2" diff --git a/src/storage/conversion/QuadToRdfConverter.ts b/src/storage/conversion/QuadToRdfConverter.ts new file mode 100644 index 000000000..7ac5eb238 --- /dev/null +++ b/src/storage/conversion/QuadToRdfConverter.ts @@ -0,0 +1,31 @@ +import { Readable } from 'stream'; +import rdfSerializer from 'rdf-serialize'; +import { Representation } from '../../ldp/representation/Representation'; +import { RepresentationMetadata } from '../../ldp/representation/RepresentationMetadata'; +import { RepresentationPreferences } from '../../ldp/representation/RepresentationPreferences'; +import { CONTENT_TYPE_QUADS, DATA_TYPE_BINARY } from '../../util/ContentTypes'; +import { checkRequest, matchingTypes } from './ConversionUtil'; +import { RepresentationConverter, RepresentationConverterArgs } from './RepresentationConverter'; + +/** + * Converts `internal/quads` to most major RDF serializations. + */ +export class QuadToRdfConverter extends RepresentationConverter { + public async canHandle(input: RepresentationConverterArgs): Promise { + checkRequest(input, [ CONTENT_TYPE_QUADS ], await rdfSerializer.getContentTypes()); + } + + public async handle(input: RepresentationConverterArgs): Promise { + return this.quadsToRdf(input.representation, input.preferences); + } + + private async quadsToRdf(quads: Representation, preferences: RepresentationPreferences): Promise { + const contentType = matchingTypes(preferences, await rdfSerializer.getContentTypes())[0].value; + const metadata: RepresentationMetadata = { ...quads.metadata, contentType }; + return { + dataType: DATA_TYPE_BINARY, + data: rdfSerializer.serialize(quads.data, { contentType }) as Readable, + metadata, + }; + } +} diff --git a/test/unit/storage/conversion/QuadToRdfConverter.test.ts b/test/unit/storage/conversion/QuadToRdfConverter.test.ts new file mode 100644 index 000000000..324d2d0f2 --- /dev/null +++ b/test/unit/storage/conversion/QuadToRdfConverter.test.ts @@ -0,0 +1,80 @@ +import { namedNode, triple } from '@rdfjs/data-model'; +import stringifyStream from 'stream-to-string'; +import streamifyArray from 'streamify-array'; +import { Representation } from '../../../../src/ldp/representation/Representation'; +import { RepresentationPreferences } from '../../../../src/ldp/representation/RepresentationPreferences'; +import { ResourceIdentifier } from '../../../../src/ldp/representation/ResourceIdentifier'; +import { QuadToRdfConverter } from '../../../../src/storage/conversion/QuadToRdfConverter'; +import { CONTENT_TYPE_QUADS, DATA_TYPE_BINARY } from '../../../../src/util/ContentTypes'; + +describe('A QuadToRdfConverter', (): void => { + const converter = new QuadToRdfConverter(); + const identifier: ResourceIdentifier = { path: 'path' }; + + it('can handle quad to turtle conversions.', async(): Promise => { + const representation = { metadata: { contentType: CONTENT_TYPE_QUADS }} as Representation; + const preferences: RepresentationPreferences = { type: [{ value: 'text/turtle', weight: 1 }]}; + await expect(converter.canHandle({ identifier, representation, preferences })).resolves.toBeUndefined(); + }); + + it('can handle quad to JSON-LD conversions.', async(): Promise => { + const representation = { metadata: { contentType: CONTENT_TYPE_QUADS }} as Representation; + const preferences: RepresentationPreferences = { type: [{ value: 'application/ld+json', weight: 1 }]}; + await expect(converter.canHandle({ identifier, representation, preferences })).resolves.toBeUndefined(); + }); + + it('converts quads to turtle.', async(): Promise => { + const representation = { + data: streamifyArray([ triple( + namedNode('http://test.com/s'), + namedNode('http://test.com/p'), + namedNode('http://test.com/o'), + ) ]), + metadata: { contentType: CONTENT_TYPE_QUADS }, + } as Representation; + const preferences: RepresentationPreferences = { type: [{ value: 'text/turtle', weight: 1 }]}; + const result = await converter.handle({ identifier, representation, preferences }); + expect(result).toMatchObject({ + dataType: DATA_TYPE_BINARY, + metadata: { + contentType: 'text/turtle', + }, + }); + await expect(stringifyStream(result.data)).resolves.toEqual( + ` . +`, + ); + }); + + it('converts quads to JSON-LD.', async(): Promise => { + const representation = { + data: streamifyArray([ triple( + namedNode('http://test.com/s'), + namedNode('http://test.com/p'), + namedNode('http://test.com/o'), + ) ]), + metadata: { contentType: CONTENT_TYPE_QUADS }, + } as Representation; + const preferences: RepresentationPreferences = { type: [{ value: 'application/ld+json', weight: 1 }]}; + const result = await converter.handle({ identifier, representation, preferences }); + expect(result).toMatchObject({ + dataType: DATA_TYPE_BINARY, + metadata: { + contentType: 'application/ld+json', + }, + }); + await expect(stringifyStream(result.data)).resolves.toEqual( + `[ + { + "@id": "http://test.com/s", + "http://test.com/p": [ + { + "@id": "http://test.com/o" + } + ] + } +] +`, + ); + }); +});