Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implementation necessary to achieve use case 3 - obtaining events #11

Merged
merged 18 commits into from
Sep 18, 2017
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions integration-tests/HydraClient.spec.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import HydraClient from "../src/HydraClient";
import {run} from "../testing/AsyncHelper";
import {hydra} from "../src/namespaces";

describe("Having a Hydra client", function() {
beforeEach(function() {
Expand Down Expand Up @@ -29,6 +30,17 @@ describe("Having a Hydra client", function() {
expect(this.entryPoint.hypermedia.find(item => item.iri.match("\/api\/events$") && item.isA === "Colletion"))
.not.toBeNull();
});

describe("and then obtaining events as in use case 3.obtaining-events", function() {
beforeEach(run(async function () {
this.events = await this.client.getResource(this.url + "api/events");
this.members = this.events.hypermedia.members;
}));

it("should obtain a collection of events", function () {
expect(this.members.filter(member => member.isA.indexOf("http://schema.org/Event") !== -1).length).toBe(3);
});
});
});

describe("and obtaining it's API documentation as in use case 2.api-documentation", function() {
Expand Down
19 changes: 19 additions & 0 deletions integration-tests/server/api/events.jsonld
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
{
"@context": "/api/context.jsonld",
"@id": "/api/events",
"@type": "hydra:Collection",
"member": [
{
"@id": "/api/events/1",
"@type": "schema:Event"
},
{
"@id": "/api/events/2",
"@type": "schema:Event"
},
{
"@id": "/api/events/3",
"@type": "schema:Event"
}
]
}
3 changes: 2 additions & 1 deletion src/DataModel/IHydraResource.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import {IOperation} from "./IOperation";
import {IResource} from "./IResource";
import {IHypermedia} from "./IHypermedia";
/**
* @interface Describes an abstract Hydra resource.
*/
export interface IHydraResource extends IResource
export interface IHydraResource extends IResource, IHypermedia
{
/**
* @readonly Gets classes a given resource is of.
Expand Down
12 changes: 12 additions & 0 deletions src/DataModel/IHypermediaContainer.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import {IHypermedia} from "./IHypermedia";
import {IResource} from "./IResource";
/**
* @interface Provides an abstraction layer over hypermedia container.
*/
export interface IHypermediaContainer extends Array<IHypermedia>
{
/**
* @readonly Gets a collection members. This may be null if the resource owning this container is not a hydra:Collection.
*/
readonly members: Array<IResource>;
}
4 changes: 2 additions & 2 deletions src/DataModel/IWebResource.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import {IHypermedia} from "./IHypermedia";
import {IHypermediaContainer} from "./IHypermediaContainer";

/**
* @interface Describes an abstract web resource.
Expand All @@ -8,5 +8,5 @@ export interface IWebResource extends Object
/**
* @readonly Gets a collection of hypermedia controls.
*/
readonly hypermedia: Array<IHypermedia>;
readonly hypermedia: IHypermediaContainer;
}
44 changes: 38 additions & 6 deletions src/DataModel/JsonLd/JsonLdHypermediaProcessor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ export default class JsonLdHypermediaProcessor implements IHypermediaProcessor
if (!removeFromPayload)
{
hypermedia = await jsonLd.frame(payload, context, { embed: "@link" });
hypermedia = hypermedia["@graph"];
hypermedia = JsonLdHypermediaProcessor.fixType(hypermedia["@graph"]);
}
else
{
Expand All @@ -46,10 +46,16 @@ export default class JsonLdHypermediaProcessor implements IHypermediaProcessor
{
for (let index = result.length - 1; index >= 0; index--)
{
if ((Object.keys(result[index]).length == 1) && (result[index].iri))
let keys = ["iri", "isA"].concat(Object.keys(result[index]));
keys = keys.filter((key, index) => keys.indexOf(key) === index);
if (keys.length == 2)
{
result.splice(index, 1);
}
else
{
JsonLdHypermediaProcessor.fixTypeOf(result[index]);
}
}

return result;
Expand Down Expand Up @@ -102,15 +108,19 @@ export default class JsonLdHypermediaProcessor implements IHypermediaProcessor
private static processResource(resource: any, result: Array<any> & { [key: string]: any }, removeFromPayload: boolean): any
{
let targetResource;
if ((resource["@id"]) && (targetResource = result[resource["@id"]]))
if (resource["@id"])
{
targetResource["@id"] = resource["@id"];
targetResource = result[resource["@id"]];
}

if (!targetResource)
{
targetResource = {};
Object.defineProperty(result, JsonLdHypermediaProcessor.generateBlankNodeId(), { enumerable: false, value: targetResource });
targetResource =
{
"@id": resource["@id"] || JsonLdHypermediaProcessor.generateBlankNodeId(),
"@type": resource["@type"] || new Array<string>()
};
Object.defineProperty(result, targetResource["@id"], { enumerable: false, value: targetResource });
result.push(targetResource);
}

Expand All @@ -128,6 +138,28 @@ export default class JsonLdHypermediaProcessor implements IHypermediaProcessor

return result;
}

private static fixType(result: Array<any> & { [key: string]: any })
{
for (let resource of result)
{
JsonLdHypermediaProcessor.fixTypeOf(resource);
}

return result;
}

private static fixTypeOf(resource: any)
{
if (!resource.isA)
{
resource.isA = [];
}
else if (!(resource.isA instanceof Array))
{
resource.isA = [resource.isA];
}
}
}

JsonLdHypermediaProcessor.initialize();
4 changes: 3 additions & 1 deletion src/DataModel/JsonLd/context.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@
"members": {
"@id": "hydra:member",
"@container": "@set"
}
},
"http://www.w3.org/ns/hydra/core#Collection": "http://www.w3.org/ns/hydra/core#Collection",
"http://www.w3.org/ns/hydra/core#Resource": "http://www.w3.org/ns/hydra/core#Resource"
}
}
21 changes: 19 additions & 2 deletions src/HydraClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,9 @@ import {hydra} from "./namespaces";
import {IHypermediaProcessor} from "./DataModel/IHypermediaProcessor";
import {IApiDocumentation} from "./DataModel/IApiDocumentation";
import {IWebResource} from "./DataModel/IWebResource";
import ApiDocumentation from "./ApiDocumentation";
import {IResource} from "./DataModel/IResource";
import ApiDocumentation from "./ApiDocumentation";
import ResourceEnrichmentProvider from "./ResourceEnrichmentProvider";
const jsonld = require("jsonld");
require("isomorphic-fetch");

Expand All @@ -14,6 +15,8 @@ require("isomorphic-fetch");
export default class HydraClient
{
private static _hypermediaProcessors = new Array<IHypermediaProcessor>();
private static _resourceEnrichmentProvider: { enrichHypermedia(resource: IWebResource): IWebResource } =
new ResourceEnrichmentProvider();
private _removeHypermediaFromPayload;

public static noUrlProvided = "There was no Url provided.";
Expand All @@ -34,6 +37,19 @@ export default class HydraClient
this._removeHypermediaFromPayload = removeHypermediaFromPayload;
}

/**
* Registers a custom resource enrichment provider.
* @param resourceEnrichmentProvider Component to be registered.
*/
public static registerResourceEnrichmentProvider(
resourceEnrichmentProvider: { enrichHypermedia(resource: IWebResource): IWebResource })
{
if (resourceEnrichmentProvider)
{
HydraClient._resourceEnrichmentProvider = resourceEnrichmentProvider;
}
}

/**
* Registers a hypermedia processor.
* @param {IHypermediaProcessor} hypermediaProcessor Hypermedia processor to be registered.
Expand Down Expand Up @@ -100,7 +116,8 @@ export default class HydraClient
throw new Error(HydraClient.responseFormatNotSupported);
}

return await hypermediaProcessor.process(response, this._removeHypermediaFromPayload);
let result = await hypermediaProcessor.process(response, this._removeHypermediaFromPayload);
return HydraClient._resourceEnrichmentProvider.enrichHypermedia(result);
}

private async getApiDocumentationUrl(url: string): Promise<string>
Expand Down
55 changes: 55 additions & 0 deletions src/ResourceEnrichmentProvider.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import {IWebResource} from "./DataModel/IWebResource";
import {IHypermedia} from "./DataModel/IHypermedia";
import {IHydraResource} from "./DataModel/IHydraResource";
import {hydra} from "./namespaces";
import {IResource} from "./DataModel/IResource";

/**
* @class @name ResourceEnrichmentProvider
* Provides IWebResource enrichment routines.
*/
export default class ResourceEnrichmentProvider
{
private static properties =
{
members: { type: hydra.Collection, propertyName: "members" }
};

/**
* Enriches a given resource with IHypermediaContainer specific properties.
* @param resource Resource to be enriched.
* @returns {IWebResource}
*/
public enrichHypermedia(resource: IWebResource): IWebResource
{
if (!resource)
{
return resource;
}

if (!resource.hypermedia)
{
Object.defineProperty(resource, "hypermedia", { value: new Array<IHypermedia>(), enumerable: false });
}

for (let propertyName of Object.keys(ResourceEnrichmentProvider.properties))
{
let propertyDefinition = ResourceEnrichmentProvider.properties[propertyName];
let value = null;
let collections = resource.hypermedia
.filter(item =>
(<IHydraResource>item).isA &&
(<IHydraResource>item).isA.find(type => type === propertyDefinition.type));
if (collections.length > 0)
{
value = Array.prototype.concat.apply(
new Array<IResource>(),
collections.map(collection => (<any>collection)[propertyDefinition.propertyName]));
}

Object.defineProperty(resource.hypermedia, propertyName, { value: value, enumerable: false });
}

return resource;
}
}
5 changes: 4 additions & 1 deletion src/namespaces.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
export let hydra: any = new String("http://www.w3.org/ns/hydra/core#");
hydra.namespace = hydra.toString();
hydra.apiDocumentation = hydra + "apiDocumentation";
hydra.member = hydra + "member";
hydra.ApiDocumentation = hydra + "ApiDocumentation";
hydra.EntryPoint = hydra + "EntryPoint";
hydra.EntryPoint = hydra + "EntryPoint";
hydra.Collection = hydra + "Collection";
hydra.Resource = hydra + "Resource";
26 changes: 15 additions & 11 deletions tests/DataModel/JsonLd/JsonLdMetadataProvider.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import HydraClient from "../../../src/HydraClient";
import {returnOk} from "../../../testing/ResponseHelper";
import JsonLdHypermediaProcessor from "../../../src/DataModel/JsonLd/JsonLdHypermediaProcessor";
import {run} from "../../../testing/AsyncHelper";
import {hydra} from "../../../src/namespaces";
const inputJsonLd = require("./input.json");

describe("Given instance of the JsonLdHypermediaProcessor class", function() {
Expand All @@ -26,23 +27,24 @@ describe("Given instance of the JsonLdHypermediaProcessor class", function() {
});

describe("without removing hypermedia controls", function() {
it("should process data", run(async function() {
let result = await this.hypermediaProcessor.process(this.response, false);

expect(result).toEqual(inputJsonLd);
beforeEach(run(async function() {
this.result = await this.hypermediaProcessor.process(this.response, false);
}));

it("should separate hypermedia", run(async function() {
let result = await this.hypermediaProcessor.process(this.response, false);
it("should process data", function() {
expect(this.result).toEqual(inputJsonLd);
});

expect(result.hypermedia).toEqual([
it("should separate hypermedia", function() {
expect(this.result.hypermedia).toEqual([
{
iri: "http://temp.uri/api/events",
isA: "Collection",
isA: [hydra.Collection],
totalItems: 1,
members: [
{
iri: "http://temp.uri/api/events/1",
isA: [],
"http://schema.org/endDate": "2017-04-19",
"http://schema.org/eventDescription": "Some event 1",
"http://schema.org/eventName": "Event 1",
Expand All @@ -51,15 +53,17 @@ describe("Given instance of the JsonLdHypermediaProcessor class", function() {
]
}, {
iri: "http://temp.uri/api/events/1",
isA: [],
"http://schema.org/endDate": "2017-04-19",
"http://schema.org/eventDescription": "Some event 1",
"http://schema.org/eventName": "Event 1",
"http://schema.org/startDate": "2017-04-19"
}, {
iri: 'some:named.graph'
iri: 'some:named.graph',
isA: []
}
]);
}));
});
});

describe("and removing hypermedia controls", function() {
Expand Down Expand Up @@ -88,7 +92,7 @@ describe("Given instance of the JsonLdHypermediaProcessor class", function() {
expect(result.hypermedia).toEqual([
{
"iri": "http://temp.uri/api/events",
"isA": "Collection",
"isA": [hydra.Collection],
"totalItems": 1,
"members": [
{ "iri": "http://temp.uri/api/events/1" }
Expand Down
Loading