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

Introduced a collections helper property on IHypermediaContainer #27

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
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