Skip to content

Commit

Permalink
Introduce a collections helper property on IHypermediaContainer (#27)
Browse files Browse the repository at this point in the history
* Introduced a collections helper property on IHypermediaContainer
as mentioned in HydraCG/Specifications#155.
* Added support for hydra:collection predicate
* Expanded support for hydra:collection to other that hydra:EntryPoint resources.
* Minor changes after code review
* Minor changes after code review
* Typo change
* Merge branch 'master' of https://github.com/HydraCG/Heracles.ts into Specification/issue-155_EntryPoint_question
  • Loading branch information
alien-mcl authored and lanthaler committed Apr 1, 2018
1 parent af85b10 commit 4c92457
Show file tree
Hide file tree
Showing 13 changed files with 180 additions and 77 deletions.
8 changes: 3 additions & 5 deletions integration-tests/HydraClient.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,9 +35,7 @@ describe("Having a Hydra client", () => {
});

it("should obtain a collection of events", () => {
const collection = this.entryPoint.hypermedia
.where(item => item.iri.match("/api/events$") && item.type.contains(hydra.Collection))
.first();
const collection = this.entryPoint.hypermedia.collections.where(item => item.iri.match("/api/events$")).first();
expect(collection).toBeDefined();
expect(collection).not.toBeNull();
});
Expand Down Expand Up @@ -108,8 +106,8 @@ describe("Having a Hydra client", () => {
);

it("should obtain matching events", () => {
const matchingEvents = this.searchResult.hypermedia
.where(item => item.iri.match("/api/events$") && item.type.contains(hydra.Collection))
const matchingEvents = this.searchResult.hypermedia.collections
.where(item => item.iri.match("/api/events$"))
.first();
expect(matchingEvents).toBeDefined();
expect(matchingEvents).not.toBeNull();
Expand Down
4 changes: 4 additions & 0 deletions integration-tests/server/api/context.jsonld
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,10 @@
"@type": "@vocab"
},
"required": "hydra:required",
"collection": {
"@id": "hydra:collection",
"@type": "@id"
},
"member": {
"@id": "hydra:member",
"@type": "@id"
Expand Down
13 changes: 13 additions & 0 deletions src/DataModel/Collections/FilterableCollection.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,19 @@ export default abstract class FilterableCollection<T> {
return null;
}

/**
* Gets the last item of the collection or null if there are no items matching the criteria.
* @returns {T}
*/
public last(): T {
let result: T = null;
for (const item of this) {
result = item;
}

return result;
}

/**
* Filters the collection with a generic match evaluator.
* @param matchEvaluator {Function} Match evaluation delegate.
Expand Down
20 changes: 19 additions & 1 deletion src/DataModel/HypermediaContainer.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
import { hydra } from "../namespaces";
import LinksCollection from "./Collections/LinksCollection";
import OperationsCollection from "./Collections/OperationsCollection";
import ResourceFilterableCollection from "./Collections/ResourceFilterableCollection";
import { ICollection } from "./ICollection";
import { IHydraResource } from "./IHydraResource";
import { IHypermediaContainer } from "./IHypermediaContainer";
import { IResource } from "./IResource";

Expand All @@ -12,6 +15,8 @@ export default class HypermediaContainer extends ResourceFilterableCollection<IR
implements IHypermediaContainer {
public readonly members?: ResourceFilterableCollection<IResource>;

public readonly collections: ResourceFilterableCollection<ICollection>;

public readonly operations: OperationsCollection;

public readonly links: LinksCollection;
Expand All @@ -21,7 +26,8 @@ export default class HypermediaContainer extends ResourceFilterableCollection<IR
* @param items {Iterable<IResource>} Hypermedia controls to be stored within this container.
* @param operations {OperationsCollection} Operations available on the container.
* @param links {LinksCollection} Links available on the container.
* @param members {Iterable<IResource>} Optional Hydra collection members in case container is a collection.
* @param members {ResourceFilterableCollection<IResource>} Optional Hydra collection members in case
* container is a collection.
*/
public constructor(
items: Iterable<IResource>,
Expand All @@ -30,7 +36,19 @@ export default class HypermediaContainer extends ResourceFilterableCollection<IR
members?: ResourceFilterableCollection<IResource>
) {
super(items);
const itemsArray = Array.from(items);
const explicitelyTypedCollections: ICollection[] = itemsArray
.filter(control => control.type.contains(hydra.Collection))
.map(control => control as ICollection);
const linkedCollections: ICollection[] = Array.prototype.concat(
...itemsArray
.filter((control: any) => !!control.collections)
.map((control: IHydraResource) => Array.from(control.collections))
);
this.operations = operations;
this.collections = new ResourceFilterableCollection<ICollection>(
explicitelyTypedCollections.concat(linkedCollections)
);
this.links = links;
this.members =
members instanceof ResourceFilterableCollection ? members : new ResourceFilterableCollection<IResource>(members);
Expand Down
9 changes: 9 additions & 0 deletions src/DataModel/IHydraResource.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,21 @@
import LinksCollection from "./Collections/LinksCollection";
import OperationsCollection from "./Collections/OperationsCollection";
import ResourceFilterableCollection from "./Collections/ResourceFilterableCollection";
import { ICollection } from "./ICollection";
import { IResource } from "./IResource";

/**
* Describes an abstract Hydra resource.
* @interface
*/
export interface IHydraResource extends IResource {
/**
* Gets collections exposed by that resource.
* @readonly
* @returns {ResourceFilterableCollection<ICollection>}
*/
readonly collections: ResourceFilterableCollection<ICollection>;

/**
* Gets operations that can be performed on that resource.
* @readonly
Expand Down
6 changes: 6 additions & 0 deletions src/DataModel/IHypermediaContainer.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import OperationsCollection from "./Collections/OperationsCollection";
import ResourceFilterableCollection from "./Collections/ResourceFilterableCollection";
import { ICollection } from "./ICollection";
import { IResource } from "./IResource";

/**
Expand All @@ -21,4 +22,9 @@ export interface IHypermediaContainer extends ResourceFilterableCollection<IReso
* Gets possible operations.
*/
readonly operations: OperationsCollection;

/**
* Gets discovered collections.
*/
readonly collections: ResourceFilterableCollection<ICollection>;
}
6 changes: 6 additions & 0 deletions src/DataModel/TemplatedResource.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,9 @@ import { hydra } from "../namespaces";
import LinksCollection from "./Collections/LinksCollection";
import MappingsCollection from "./Collections/MappingsCollection";
import OperationsCollection from "./Collections/OperationsCollection";
import ResourceFilterableCollection from "./Collections/ResourceFilterableCollection";
import TypesCollection from "./Collections/TypesCollection";
import { ICollection } from "./ICollection";
import { IIriTemplate } from "./IIriTemplate";
import { IPointingResource } from "./IPointingResource";
import { IResource } from "./IResource";
Expand All @@ -27,6 +29,8 @@ export default abstract class TemplatedResource<T extends IPointingResource> imp

public readonly links: LinksCollection;

public readonly collections: ResourceFilterableCollection<ICollection>;

private readonly template: string;

private readonly mappings: MappingsCollection;
Expand All @@ -42,6 +46,7 @@ export default abstract class TemplatedResource<T extends IPointingResource> imp
this.baseUrl = resource.baseUrl;
this.iri = resource.iri;
this.links = new LinksCollection([]);
this.collections = new ResourceFilterableCollection<ICollection>([]);
this.type = new TypesCollection(types.filter((item, index) => types.indexOf(item) === index));
this.target = null;
this.template = template.template;
Expand All @@ -63,6 +68,7 @@ export default abstract class TemplatedResource<T extends IPointingResource> imp
const target = targetUri.match(/^[a-zA-Z][a-zA-Z0-9_]*:/) ? targetUri : new URL(targetUri, this.baseUrl).toString();
return this.createInstance({
baseUrl: this.baseUrl,
collections: this.collections,
iri: this.getNextIri(),
links: this.links,
operations: this.operations,
Expand Down
1 change: 1 addition & 0 deletions src/JsonLd/JsonLdHypermediaProcessor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { promises as jsonLd } from "jsonld";
import ApiDocumentation from "../DataModel/ApiDocumentation";
import LinksCollection from "../DataModel/Collections/LinksCollection";
import OperationsCollection from "../DataModel/Collections/OperationsCollection";
import ResourceFilterableCollection from "../DataModel/Collections/ResourceFilterableCollection";
import TypesCollection from "../DataModel/Collections/TypesCollection";
import HypermediaContainer from "../DataModel/HypermediaContainer";
import { IApiDocumentation } from "../DataModel/IApiDocumentation";
Expand Down
3 changes: 3 additions & 0 deletions src/JsonLd/linksExtractor.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import LinksCollection from "../DataModel/Collections/LinksCollection";
import OperationsCollection from "../DataModel/Collections/OperationsCollection";
import ResourceFilterableCollection from "../DataModel/Collections/ResourceFilterableCollection";
import TypesCollection from "../DataModel/Collections/TypesCollection";
import { ICollection } from "../DataModel/ICollection";
import TemplatedLink from "../DataModel/TemplatedLink";
import { hydra } from "../namespaces";

Expand Down Expand Up @@ -33,6 +35,7 @@ export const linksExtractor = (resources, processingState) => {
processingState.forbiddenHypermedia.push(predicate);
let link = {
baseUrl: processingState.baseUrl,
collections: new ResourceFilterableCollection<ICollection>([]),
iri: predicate,
links: new LinksCollection([]),
operations: new OperationsCollection([]),
Expand Down
5 changes: 5 additions & 0 deletions src/JsonLd/mappings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,11 @@ mappings[hydra.entrypoint] = {
required: true,
type: [hydra.ApiDocumentation as string]
};
mappings[hydra.collection] = {
default: (collections, processingState) => new ResourceFilterableCollection(collections),
propertyName: "collections",
required: true
};
mappings[hydra.template] = {
default: "",
propertyName: "template",
Expand Down
1 change: 1 addition & 0 deletions src/namespaces.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ export let hydra = {
TemplatedLink: hydraNamespace + "TemplatedLink",
Link: hydraNamespace + "Link",

collection: hydraNamespace + "collection",
member: hydraNamespace + "member",
memberTemplate: hydraNamespace + "memberTemplate",
totalItems: hydraNamespace + "totalItems",
Expand Down
136 changes: 82 additions & 54 deletions tests/JsonLd/JsonLdMetadataProvider.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ describe("Given instance of the JsonLdHypermediaProcessor class", () => {
describe("when parsing", () => {
beforeEach(
run(async () => {
this.response = returnOk("http://temp.uri/", inputJsonLd);
this.response = returnOk("http://temp.uri/api", inputJsonLd);
this.result = await this.hypermediaProcessor.process(this.response, false);
})
);
Expand All @@ -36,77 +36,105 @@ describe("Given instance of the JsonLdHypermediaProcessor class", () => {
expect(this.result).toEqual(inputJsonLd);
});

it("should discover all collections", () => {
expect(this.result.hypermedia.collections.length).toBe(2);
});

it("should discover people collection", () => {
expect(this.result.hypermedia.collections.first().iri).toMatch("/api/people$");
});

it("should discover events collection", () => {
expect(this.result.hypermedia.collections.last().iri).toMatch("/api/events$");
});

it("should separate hypermedia", () => {
expect(this.result.hypermedia).toBeLike([
{
iri: "http://temp.uri/api/events",
links: [
{
baseUrl: "http://temp.uri/",
iri: "http://temp.uri/vocab/closed-events",
links: [],
operations: [],
relation: "http://temp.uri/vocab/closed-events",
target: { iri: "http://temp.uri/api/events/closed", type: [] },
type: [hydra.Link]
},
collections: [
{
baseUrl: "http://temp.uri/",
iri: "http://www.w3.org/ns/hydra/core#first",
collections: [],
iri: "http://temp.uri/api/people",
links: [],
operations: [],
relation: "http://www.w3.org/ns/hydra/core#first",
target: { iri: "http://temp.uri/api/events?page=1", type: [] },
type: [hydra.Link]
},
{
baseUrl: "http://temp.uri/",
iri: "http://www.w3.org/ns/hydra/core#last",
links: [],
operations: [],
relation: "http://www.w3.org/ns/hydra/core#last",
target: { iri: "http://temp.uri/api/events?page=9", type: [] },
type: [hydra.Link]
type: []
},
{
baseUrl: "http://temp.uri/",
iri: "http://www.w3.org/ns/hydra/core#search",
links: [],
mappings: [
collections: [],
iri: "http://temp.uri/api/events",
links: [
{
baseUrl: "http://temp.uri/api",
collections: [],
iri: "http://temp.uri/vocab/closed-events",
links: [],
operations: [],
relation: "http://temp.uri/vocab/closed-events",
target: { iri: "http://temp.uri/api/events/closed", type: [] },
type: [hydra.Link]
},
{
iri: "_:b1",
baseUrl: "http://temp.uri/api",
collections: [],
iri: "http://www.w3.org/ns/hydra/core#first",
links: [],
operations: [],
property: { iri: "http://www.w3.org/ns/hydra/core#freetextQuery", type: [] },
required: false,
type: [],
variable: "searchPhrase"
relation: "http://www.w3.org/ns/hydra/core#first",
target: { iri: "http://temp.uri/api/events?page=1", type: [] },
type: [hydra.Link]
},
{
baseUrl: "http://temp.uri/api",
collections: [],
iri: "http://www.w3.org/ns/hydra/core#last",
links: [],
operations: [],
relation: "http://www.w3.org/ns/hydra/core#last",
target: { iri: "http://temp.uri/api/events?page=9", type: [] },
type: [hydra.Link]
},
{
baseUrl: "http://temp.uri/api",
collections: [],
iri: "http://www.w3.org/ns/hydra/core#search",
links: [],
mappings: [
{
collections: [],
iri: "_:b1",
links: [],
operations: [],
property: { iri: "http://www.w3.org/ns/hydra/core#freetextQuery", type: [] },
required: false,
type: [],
variable: "searchPhrase"
}
],
operations: [],
relation: "http://www.w3.org/ns/hydra/core#search",
target: null,
template: "http://temp.uri/api/events{?searchPhrase}",
type: [hydra.TemplatedLink]
}
],
members: [
{
collections: [],
iri: "http://temp.uri/api/events/1",
links: [],
operations: [],
type: [hydra.Collection]
}
],
operations: [],
relation: "http://www.w3.org/ns/hydra/core#search",
target: null,
template: "http://temp.uri/api/events{?searchPhrase}",
type: [hydra.TemplatedLink]
}
],
members: [
{
iri: "http://temp.uri/api/events/1",
links: [],
operations: [],
type: []
totalItems: 1,
type: [hydra.Collection]
}
],
operations: [],
totalItems: 1,
type: [hydra.Collection]
},
{
iri: "http://temp.uri/",
iri: "http://temp.uri/api",
links: [],
operations: [],
type: []
type: [hydra.EntryPoint]
}
]);
});
Expand Down
Loading

0 comments on commit 4c92457

Please sign in to comment.