From 978d8498b079ff8973d838ebdda6c233e9188fe3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Karol=20Szczepa=C5=84ski?= Date: Mon, 9 Mar 2020 22:27:52 +0100 Subject: [PATCH] Issue/16 should we keep using the hypermedia interface (#100) * Added support for N3 and derivatives with some minor hydra client factory changes * Added setup for separate N3 module publishing * Some fixes and minor refactorization leading to better compliance with Heracles.net * Removed hypermedia property * Added some tweaks to internal API and displayName and textDescription properties --- index.ts | 1 - .../HydraClient.ApiDocumentation.spec.ts | 27 +++- .../HydraClient.EntryPoint.spec.ts | 60 ++++----- .../HydraClient.Resource.spec.ts | 8 +- package.json | 2 +- src/DataModel/HypermediaContainer.ts | 90 +++++++++++-- src/DataModel/IApiDocumentation.ts | 12 +- src/DataModel/IClass.ts | 4 +- src/DataModel/IHypermediaContainer.ts | 60 ++++++--- src/DataModel/IPartialCollectionView.ts | 12 +- src/DataModel/ISupportedProperty.ts | 4 +- src/HydraClient.ts | 10 +- src/HydraClientFactory.ts | 6 +- src/IHydraClient.ts | 8 +- src/IHypermediaProcessor.ts | 10 +- .../CompoundGraphTransformer.ts | 6 +- .../EntryPointCorrectingGraphTransformer.ts | 11 +- .../FlatteningGraphTransformer.ts | 4 +- .../GraphTransformations/IGraphTransformer.ts | 4 +- src/JsonLd/JsonLdHelper.ts | 2 +- src/JsonLd/JsonLdHypermediaProcessor.ts | 14 +- src/JsonLd/ProcessingState.ts | 2 +- src/JsonLd/apiDocumentation.ts | 2 +- src/JsonLd/collection.ts | 8 +- src/JsonLd/factories.ts | 9 ++ src/JsonLd/linksExtractor.ts | 36 ++++- src/JsonLd/mappings.ts | 14 +- .../partialCollectionIteratorFactory.ts | 62 +++++---- src/N3/N3HypermediaProcessor.ts | 14 +- src/N3/package.json | 2 +- src/PartialCollectionCrawler.ts | 6 +- .../Collections/LinksCollection.spec.ts | 22 +-- .../Collections/MappingsCollection.spec.ts | 15 +-- .../Collections/OperationsCollection.spec.ts | 74 +++------- .../ResourceFilterableCollection.spec.ts | 37 ++--- .../Collections/TypesCollection.spec.ts | 18 +-- tests/DataModel/IApiDocumentation.spec.ts | 25 ++-- tests/DataModel/ICollection.spec.ts | 58 +++----- tests/DataModel/TemplatedLink.spec.ts | 7 +- tests/DataModel/TemplatedOperation.spec.ts | 6 +- tests/HydraClient.spec.ts | 23 ++-- tests/HydraClientFactory.spec.ts | 2 +- ...tryPointCorrectingGraphTransformer.spec.ts | 12 +- tests/JsonLd/IndirectTypingProvider.spec.ts | 2 +- .../JsonLd/JsonLdHypermediaProcessor.spec.ts | 127 +++--------------- tests/JsonLd/linksExtractor.spec.ts | 56 ++++---- tests/JsonLd/operationInput.json | 24 +++- tests/N3/N3HypermediaProcessor.spec.ts | 10 +- tests/PartialCollectionCrawler.spec.ts | 20 +-- 49 files changed, 502 insertions(+), 546 deletions(-) diff --git a/index.ts b/index.ts index bbd1f032..09184e41 100644 --- a/index.ts +++ b/index.ts @@ -28,7 +28,6 @@ export { ISupportedProperty } from "./src/DataModel/ISupportedProperty"; export { ITemplatedLink } from "./src/DataModel/ITemplatedLink"; export { ITemplatedOperation } from "./src/DataModel/ITemplatedOperation"; export { ITemplatedResource } from "./src/DataModel/ITemplatedResource"; -export { IWebResource } from "./src/DataModel/IWebResource"; export { default as FilterableCollection } from "./src/DataModel/Collections/FilterableCollection"; export { default as LinksCollection } from "./src/DataModel/Collections/LinksCollection"; export { default as MappingsCollection } from "./src/DataModel/Collections/MappingsCollection"; diff --git a/integration-tests/HydraClient.ApiDocumentation.spec.ts b/integration-tests/HydraClient.ApiDocumentation.spec.ts index 3d8c794b..d1b848bf 100644 --- a/integration-tests/HydraClient.ApiDocumentation.spec.ts +++ b/integration-tests/HydraClient.ApiDocumentation.spec.ts @@ -12,14 +12,15 @@ describe("Having a Hydra client", () => { .andCreate(); }); - describe("while browsing the test website", () => { + describe("while browsing a website", () => { beforeEach( run(async () => { this.apiDocumentation = await this.client.getApiDocumentation(this.url); }) ); - describe("and obtaining it's API documentation as in use case 2.api-documentation", () => { + /*Use case 2. API documentation.*/ + describe("and obtaining it's API documentation", () => { it("should obtain an API documentation", () => { expect(this.apiDocumentation.getEntryPoint).toEqual(jasmine.any(Function)); }); @@ -28,7 +29,8 @@ describe("Having a Hydra client", () => { expect(this.apiDocumentation.entryPoint).toMatch(".*/api$"); }); - it("should provide class of schema:Event as in use case 2.1.api-documentation-data-structures", () => { + /*Use case 2.1. API documentation data structures.*/ + it("should provide class of schema:Event", () => { expect(this.apiDocumentation.supportedClasses.ofIri("http://schema.org/Event")).toBeLike([ { collections: [], @@ -41,6 +43,8 @@ describe("Having a Hydra client", () => { supportedProperties: [ { collections: [], + description: "", + displayName: "", iri: "_:b0", links: [], operations: [], @@ -56,11 +60,15 @@ describe("Having a Hydra client", () => { }, readable: false, required: false, + textDescription: "", + title: "", type: [hydra.SupportedProperty], writable: false }, { collections: [], + description: "", + displayName: "", iri: "_:b1", links: [], operations: [], @@ -76,11 +84,15 @@ describe("Having a Hydra client", () => { }, readable: false, required: false, + textDescription: "", + title: "", type: [hydra.SupportedProperty], writable: false }, { collections: [], + description: "", + displayName: "", iri: "_:b2", links: [], operations: [], @@ -99,11 +111,15 @@ describe("Having a Hydra client", () => { }, readable: false, required: false, + textDescription: "", + title: "", type: [hydra.SupportedProperty], writable: false }, { collections: [], + description: "", + displayName: "", iri: "_:b3", links: [], operations: [], @@ -122,6 +138,8 @@ describe("Having a Hydra client", () => { }, readable: false, required: false, + textDescription: "", + title: "", type: [hydra.SupportedProperty], writable: false } @@ -155,7 +173,8 @@ describe("Having a Hydra client", () => { } }); - it("should provide all details for the user guide as in use case 2.2.api-documentation-user-document", () => { + /*Use case 2.2. API documentation user document.*/ + it("should provide all details for the user guide", () => { expect(this.userGuide.classes["http://schema.org/Event"]).toBe( "##Class Event (http://schema.org/Event)\n\n" + "An event happening at a certain time and location, such as a concert, lecture, or festival.\n\n" + diff --git a/integration-tests/HydraClient.EntryPoint.spec.ts b/integration-tests/HydraClient.EntryPoint.spec.ts index 45df28ef..af6b4ecf 100644 --- a/integration-tests/HydraClient.EntryPoint.spec.ts +++ b/integration-tests/HydraClient.EntryPoint.spec.ts @@ -1,4 +1,5 @@ import * as md5 from "js-md5"; +import { IResource } from "../src/DataModel/IResource"; import HydraClientFactory from "../src/HydraClientFactory"; import { hydra } from "../src/namespaces"; import PartialCollectionCrawler from "../src/PartialCollectionCrawler"; @@ -12,14 +13,15 @@ describe("Having a Hydra client", () => { .andCreate(); }); - describe("while browsing the test website", () => { + describe("while browsing a website", () => { beforeEach( run(async () => { this.apiDocumentation = await this.client.getApiDocumentation(this.url); }) ); - describe("and obtaining it's entry point as in use case 1.entry-point", () => { + /*Use case 1. Entry-point.*/ + describe("and obtaining it's entry point", () => { beforeEach( run(async () => { this.entryPoint = await this.apiDocumentation.getEntryPoint(); @@ -27,23 +29,22 @@ describe("Having a Hydra client", () => { ); it("should obtain three hypermedia controls", () => { - expect(this.entryPoint.hypermedia.length).toBe(3); + expect(this.entryPoint.length).toBe(3); }); it("should obtain a schema:AddAction operations", () => { - const operations = this.entryPoint.hypermedia.where(_ => - _.operations.ofType("http://schema.org/AddAction").any() - ); + const operations = this.entryPoint.where(_ => _.operations.ofType("http://schema.org/AddAction").any()); expect(operations.length).toBe(2); }); it("should obtain a collection of events", () => { - const collection = this.entryPoint.hypermedia.collections.where(item => item.iri.match("/api/events$")).first(); + const collection = this.entryPoint.collections.where(item => item.iri.match("/api/events$")).first(); expect(collection).toBeDefined(); expect(collection).not.toBeNull(); }); - describe("and then obtaining events as in use case 3.obtaining-events", () => { + /*Use case 3. Obtaining events.*/ + describe("and then obtaining events", () => { beforeEach( run(async () => { this.events = await this.client.getResource(this.url + "api/events"); @@ -51,17 +52,18 @@ describe("Having a Hydra client", () => { ); it("should obtain a collection of events", () => { - expect(this.events.hypermedia.members.ofType("http://schema.org/Event").length).toBe(3); + expect(this.events.members.ofType("http://schema.org/Event").length).toBe(3); }); - describe("and then adding a new event to that collection as in use case 5.creating-event", () => { + /*Use case 5. Creating an event*/ + describe("and then adding a new event to that collection", () => { beforeEach( run(async () => { try { this.body = { "@type": "http://schema.org/Event" }; - const operation = this.events.hypermedia.operations + const operation = this.events.operations .ofType("http://schema.org/CreateAction") .expecting("http://schema.org/Event") .first(); @@ -96,10 +98,11 @@ describe("Having a Hydra client", () => { ); }); - describe("and then searching for events as in use case 7.searching-events", () => { + /*Use case 7. Searching events*/ + describe("and then searching for events", () => { beforeEach( run(async () => { - const link = this.events.hypermedia.links + const link = this.events.links .withRelationOf(hydra.search) .withTemplate() .first() @@ -109,18 +112,16 @@ describe("Having a Hydra client", () => { ); it("should obtain matching events", () => { - const matchingEvents = this.searchResult.hypermedia.collections - .where(item => item.iri.match("/api/events?")) - .first(); + const matchingEvents = this.searchResult.collections.where(item => item.iri.match("/api/events?")).first(); expect(matchingEvents).toBeDefined(); expect(matchingEvents).not.toBeNull(); }); }); - describe("and then using a custom arbitrarily pointed templated operation", () => { + describe("and then using some templated operation", () => { beforeEach( run(async () => { - const link = this.events.hypermedia.links + const link = this.events.links .withRelationOf("http://example.com/vocab#filter") .withTemplate() .first() @@ -135,7 +136,7 @@ describe("Having a Hydra client", () => { ); it("should obtain matching events", () => { - const matchingEvents = this.filteringResult.hypermedia + const matchingEvents = this.filteringResult .where(item => item.iri.match("/api/events?") && item.type.contains(hydra.Collection)) .first(); expect(matchingEvents).toBeDefined(); @@ -152,20 +153,15 @@ describe("Having a Hydra client", () => { ); it("should obtain a collection of people", () => { - expect(this.people.hypermedia.members.ofType("http://schema.org/Person").length).toBe(1); + expect(this.people.members.ofType("http://schema.org/Person").length).toBe(1); }); - describe("and then obtaining all people collection members", () => { - beforeEach( - run(async () => { - this.allPeople = await PartialCollectionCrawler.from(this.people.hypermedia).getMembers(); - }) - ); - - it("should follow all links and gather all members", () => { - expect(this.allPeople.length).toBe(2); - }); - }); + it( + "should follow all links and gather all members", + run(async () => { + expect(((await PartialCollectionCrawler.from(this.people).getMembers()) as IResource[]).length).toBe(2); + }) + ); describe("and then adding a new person to that collection", () => { beforeEach( @@ -175,7 +171,7 @@ describe("Having a Hydra client", () => { "@type": "http://schema.org/Person", "http://schema.org/name": "test-name" }; - const operation = this.people.hypermedia.operations + const operation = this.people.operations .ofType("http://schema.org/UpdateAction") .expecting("http://schema.org/Person") .first(); diff --git a/integration-tests/HydraClient.Resource.spec.ts b/integration-tests/HydraClient.Resource.spec.ts index e873064e..7d3f6992 100644 --- a/integration-tests/HydraClient.Resource.spec.ts +++ b/integration-tests/HydraClient.Resource.spec.ts @@ -20,18 +20,16 @@ describe("Having a Hydra client", () => { ); it("should obtain four hypermedia controls", () => { - expect(this.resource.hypermedia.length).toBe(4); + expect(this.resource.length).toBe(4); }); it("should obtain a schema:AddAction operations", () => { - const operations = this.resource.hypermedia.where(_ => - _.operations.ofType("http://schema.org/AddAction").any() - ); + const operations = this.resource.where(_ => _.operations.ofType("http://schema.org/AddAction").any()); expect(operations.length).toBe(2); }); it("should obtain a collection of events", () => { - const collection = this.resource.hypermedia.collections.where(item => item.iri.match("/api/events$")).first(); + const collection = this.resource.collections.where(item => item.iri.match("/api/events$")).first(); expect(collection).toBeDefined(); expect(collection).not.toBeNull(); }); diff --git a/package.json b/package.json index 3b53cdff..f52f8fcd 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@hydra-cg/heracles.ts", - "version": "0.2.0", + "version": "0.5.0", "contributors": [ { "name": "Karol Szczepanski" diff --git a/src/DataModel/HypermediaContainer.ts b/src/DataModel/HypermediaContainer.ts index 81dbc9bc..f3941ee8 100644 --- a/src/DataModel/HypermediaContainer.ts +++ b/src/DataModel/HypermediaContainer.ts @@ -8,7 +8,9 @@ import { IHeaders } from "./IHeaders"; import { IHydraResource } from "./IHydraResource"; import { IHypermediaContainer } from "./IHypermediaContainer"; import { IPartialCollectionIterator } from "./IPartialCollectionIterator"; +import { IPartialCollectionView } from "./IPartialCollectionView"; import { IResource } from "./IResource"; +import { IStatement } from "./IStatement"; function addTo(collection: ICollection[], hashList: string[], item: ICollection): void { if (hashList.indexOf(item.iri) === -1) { @@ -39,8 +41,8 @@ function discoverCollectionsFrom(hypermedia: Iterable): ICollection[] */ export default class HypermediaContainer extends ResourceFilterableCollection implements IHypermediaContainer { - /** @inheritDoc */ - public readonly headers: IHeaders; + private readonly response: Response; + private readonly collection: ICollection; /** @inheritDoc */ public readonly iri: string; @@ -49,41 +51,101 @@ export default class HypermediaContainer extends ResourceFilterableCollection; + public readonly operations: OperationsCollection; /** @inheritDoc */ - public readonly collections: CollectionsCollection; + public readonly links: LinksCollection; /** @inheritDoc */ - public readonly operations: OperationsCollection; + public get headers(): IHeaders { + return this.response.headers; + } /** @inheritDoc */ - public readonly links: LinksCollection; + public get url(): string { + return this.response.url; + } + + /** @inheritDoc */ + public get status(): number { + return this.response.status; + } + + /** @inheritDoc */ + public get body(): ReadableStream | null { + return this.response.body; + } + + /** @inheritDoc */ + public get bodyUsed(): boolean { + return this.response.bodyUsed; + } + + /** @inheritDoc */ + public get view(): IPartialCollectionView | null { + return !!this.collection ? this.collection.view : null; + } + + /** @inheritDoc */ + public get members(): ResourceFilterableCollection { + return !!this.collection ? this.collection.members : null; + } + + /** @inheritDoc */ + public get manages(): ResourceFilterableCollection { + return !!this.collection ? this.collection.manages : null; + } + + /** @inheritDoc */ + public get totalItems(): number { + return !!this.collection ? this.collection.totalItems : 0; + } /** * Initializes a new instance of the {@link HypermediaContainer} class. + * @param {Response} response Raw response. * @param {IResource} rootResource Main resource associated with the requested Url. * @param {Iterable} hypermedia Hypermedia controls to be stored within this container. */ - public constructor(headers: IHeaders, rootResource: IResource, hypermedia: Iterable) { + public constructor(response: Response, rootResource: IResource, hypermedia: Iterable) { super(hypermedia); - this.headers = headers; + this.response = response; this.iri = rootResource.iri; this.type = rootResource.type; this.operations = (rootResource as IHydraResource).operations; this.collections = new CollectionsCollection(discoverCollectionsFrom(hypermedia)); this.links = (rootResource as IHydraResource).links; const collection = rootResource as ICollection; - if (collection != null) { - this.members = collection.members; - this.view = collection.view; - Object.defineProperty(this, "getIterator", { value: collection.getIterator, writable: false }); + if (!!collection.members) { + this.collection = collection; } } /** @inheritDoc */ - public getIterator?(): IPartialCollectionIterator; + public getIterator(): IPartialCollectionIterator { + return !!this.collection ? this.collection.getIterator() : null; + } + + /** @inheritDoc */ + public arrayBuffer(): Promise { + return this.response.arrayBuffer(); + } + + /** @inheritDoc */ + public blob(): Promise { + return this.response.blob(); + } + + /** @inheritDoc */ + public json(): Promise { + return this.response.json(); + } + + /** @inheritDoc */ + public text(): Promise { + return this.response.text(); + } } diff --git a/src/DataModel/IApiDocumentation.ts b/src/DataModel/IApiDocumentation.ts index 1b006eb6..b2cbd132 100644 --- a/src/DataModel/IApiDocumentation.ts +++ b/src/DataModel/IApiDocumentation.ts @@ -1,19 +1,19 @@ import ResourceFilterableCollection from "./Collections/ResourceFilterableCollection"; import { IClass } from "./IClass"; -import { IResource } from "./IResource"; -import { IWebResource } from "./IWebResource"; +import { IHydraResource } from "./IHydraResource"; +import { IHypermediaContainer } from "./IHypermediaContainer"; /** * Represents an abstract API documentation. * @interface */ -export interface IApiDocumentation extends IResource { +export interface IApiDocumentation extends IHydraResource { /** * Gets a title of this API documentation. * @readonly * @returns {string} */ - readonly title?: string; + readonly displayName?: string; /** * Gets a description of this API documentation. @@ -39,7 +39,7 @@ export interface IApiDocumentation extends IResource { /** * Retrieves an API's entry point resource. * @readonly - * @returns {Promise} + * @returns {Promise} */ - getEntryPoint(): Promise; + getEntryPoint(): Promise; } diff --git a/src/DataModel/IClass.ts b/src/DataModel/IClass.ts index 7d072327..2498fd04 100644 --- a/src/DataModel/IClass.ts +++ b/src/DataModel/IClass.ts @@ -1,13 +1,13 @@ import OperationsCollection from "./Collections/OperationsCollection"; import ResourceFilterableCollection from "./Collections/ResourceFilterableCollection"; -import { IResource } from "./IResource"; +import { IHydraResource } from "./IHydraResource"; import { ISupportedProperty } from "./ISupportedProperty"; /** * Represents a Hydra class. * @interface */ -export interface IClass extends IResource { +export interface IClass extends IHydraResource { /** * Gets the class' display name. * @readonly diff --git a/src/DataModel/IHypermediaContainer.ts b/src/DataModel/IHypermediaContainer.ts index d913d909..19d33786 100644 --- a/src/DataModel/IHypermediaContainer.ts +++ b/src/DataModel/IHypermediaContainer.ts @@ -1,41 +1,61 @@ import ResourceFilterableCollection from "./Collections/ResourceFilterableCollection"; +import { ICollection } from "./ICollection"; import { IHeaders } from "./IHeaders"; -import { IHydraResource } from "./IHydraResource"; -import { IPartialCollectionIterator } from "./IPartialCollectionIterator"; import { IResource } from "./IResource"; /** * Provides an abstraction layer over hypermedia container. * @interface */ -export interface IHypermediaContainer extends ResourceFilterableCollection, IHydraResource { +export interface IHypermediaContainer extends ResourceFilterableCollection, ICollection { /** - * Gets a collection members. - * This may be null if the resource owning this container is not a hydra:Collection. + * Gets response headers. * @readonly - * @returns {ResourceFilterableCollection} + * @returns {IHeaders} */ - readonly members?: ResourceFilterableCollection; + readonly headers: IHeaders; /** - * Gets a partial collection view. - * This may be null if the resource owning this container is not a hydra:Collection with hydra:view. - * @readonly - * @returns {IHydraResource} + * Gets a response status. */ - readonly view?: IHydraResource; + readonly status: number; /** - * Gets response headers. - * @readonly - * @returns {IHeaders} + * Gets a response's URL. */ - readonly headers: IHeaders; + readonly url: string; + + /** + * Gets a raw response body. + */ + readonly body: ReadableStream | null; + + /** + * Gets a value indicating whether the body was consumed. + */ + readonly bodyUsed: boolean; + + /** + * Gets a raw response body as an array of bytes. + * @returns {Promise} + */ + arrayBuffer(): Promise; + + /** + * Gets a raw response body as a blob. + * @returns {Promise} + */ + blob(): Promise; + + /** + * Gets a raw response body as a JSON. + * @returns {Promise} + */ + json(): Promise; /** - * Gets a part iterator associated with the collection. - * This may be null if the resource owning this container is not a hydra:Collection with hydra:view. - * @returns {IPartialCollectionIterator} + * Gets a raw response body as a text. + * @returns {Promise} */ - getIterator?(): IPartialCollectionIterator; + text(): Promise; } diff --git a/src/DataModel/IPartialCollectionView.ts b/src/DataModel/IPartialCollectionView.ts index 966acdb3..fcae4706 100644 --- a/src/DataModel/IPartialCollectionView.ts +++ b/src/DataModel/IPartialCollectionView.ts @@ -1,35 +1,35 @@ -import { ILink } from "./ILink"; +import { IResource } from "./IResource"; /** * Describes an abstract partial collection view with links to other collection parts. * @interface */ -export interface IPartialCollectionView extends ILink { +export interface IPartialCollectionView extends IResource { /** * Gets the link to the first part of the collection, if any. * @readonly * @returns {ILink} */ - readonly first?: ILink; + readonly first?: IResource; /** * Gets the link to the next part of the collection, if any. * @readonly * @returns {ILink} */ - readonly next?: ILink; + readonly next?: IResource; /** * Gets the link to the previous part of the collection, if any. * @readonly * @returns {ILink} */ - readonly previous?: ILink; + readonly previous?: IResource; /** * Gets the link to the last part of the collection, if any. * @readonly * @returns {ILink} */ - readonly last?: ILink; + readonly last?: IResource; } diff --git a/src/DataModel/ISupportedProperty.ts b/src/DataModel/ISupportedProperty.ts index 1686e1f7..a8c46baf 100644 --- a/src/DataModel/ISupportedProperty.ts +++ b/src/DataModel/ISupportedProperty.ts @@ -1,11 +1,11 @@ +import { IHydraResource } from "./IHydraResource"; import { IProperty } from "./IProperty"; -import { IResource } from "./IResource"; /** * Describes an abstract Hydra property. * @interface */ -export interface ISupportedProperty extends IResource { +export interface ISupportedProperty extends IHydraResource { /** * Gets the actual property. * @readonly diff --git a/src/HydraClient.ts b/src/HydraClient.ts index f79a3ca2..9ed7f426 100644 --- a/src/HydraClient.ts +++ b/src/HydraClient.ts @@ -3,10 +3,10 @@ import * as jsonld from "jsonld"; import * as parseLinkHeader from "parse-link-header"; import FilterableCollection from "./DataModel/Collections/FilterableCollection"; import { IApiDocumentation } from "./DataModel/IApiDocumentation"; +import { IHypermediaContainer } from "./DataModel/IHypermediaContainer"; import { ILink } from "./DataModel/ILink"; import { IOperation } from "./DataModel/IOperation"; import { IResource } from "./DataModel/IResource"; -import { IWebResource } from "./DataModel/IWebResource"; import { HttpCallFacility } from "./HydraClientFactory"; import { IHydraClient } from "./IHydraClient"; import { IHypermediaProcessor } from "./IHypermediaProcessor"; @@ -87,7 +87,7 @@ export default class HydraClient implements IHydraClient { const apiDocumentation = await this.getApiDocumentationUrl(url); const options = { auxiliaryResponse: apiDocumentation.response, auxiliaryOriginalUrl: url }; const resource = await this.getResourceFrom(apiDocumentation.url, options); - const result = resource.hypermedia.ofType(hydra.ApiDocumentation).first() as IApiDocumentation; + const result = resource.ofType(hydra.ApiDocumentation).first() as IApiDocumentation; if (!result) { throw new Error(HydraClient.noEntryPointDefined); } @@ -96,12 +96,12 @@ export default class HydraClient implements IHydraClient { } /** @inheritDoc */ - public async getResource(urlOrResource: string | IResource | ILink): Promise { + public async getResource(urlOrResource: string | IResource | ILink): Promise { return await this.getResourceFrom(HydraClient.getUrl(urlOrResource), {}); } /** @inheritDoc */ - public async invoke(operation: IOperation, body?: IWebResource, parameters?: object): Promise { + public async invoke(operation: IOperation, body?: IResource, parameters?: object): Promise { if (!operation) { throw new Error(HydraClient.noOperationProvided); } @@ -116,7 +116,7 @@ export default class HydraClient implements IHydraClient { }); } - private async getResourceFrom(url: string, options: any): Promise { + private async getResourceFrom(url: string, options: any): Promise { const response = await this.makeRequestTo(url); if (response.status !== 200) { throw new Error(HydraClient.invalidResponse + response.status); diff --git a/src/HydraClientFactory.ts b/src/HydraClientFactory.ts index 71f52c01..9400d04d 100644 --- a/src/HydraClientFactory.ts +++ b/src/HydraClientFactory.ts @@ -142,7 +142,7 @@ export default class HydraClientFactory implements IHydraClientFactory { /** * Adds an another {@link IHypermediaProcessor} component via it's factory method. - * @param {HypermediaProcessorFactory} hypermediaProcessorFactory Hypermedia processor facvtory to be passed + * @param {HypermediaProcessorFactory} hypermediaProcessorFactory Hypermedia processor factory to be passed * to future {@link HydraClient} instances. * @returns {HydraClientFactory} */ @@ -162,14 +162,14 @@ export default class HydraClientFactory implements IHydraClientFactory { /** * Sets a {@link IIriTemplateExpansionStrategy} component. * @param {IIriTemplateExpansionStrategy} iriTemplateExpansionStrategy IRI template expansion strategy to be used - * when an IRI template is encountered. + * when an IRI template is encountered. * @returns {HydraClientFactory} */ /* tslint:disable-next-line:unified-signatures */ public with(iriTemplateExpansionStrategy: IIriTemplateExpansionStrategy): HydraClientFactory; /** - * Adds HTTP requests facility component. + * Adds an HTTP requests facility component. * @param {HttpCallFacility} httpCall HTTP call facility to be used for remote server calls. * @returns {HydraClientFactory} */ diff --git a/src/IHydraClient.ts b/src/IHydraClient.ts index b417b383..5e5b1aa4 100644 --- a/src/IHydraClient.ts +++ b/src/IHydraClient.ts @@ -1,8 +1,8 @@ import { IApiDocumentation } from "./DataModel/IApiDocumentation"; +import { IHypermediaContainer } from "./DataModel/IHypermediaContainer"; import { ILink } from "./DataModel/ILink"; import { IOperation } from "./DataModel/IOperation"; import { IResource } from "./DataModel/IResource"; -import { IWebResource } from "./DataModel/IWebResource"; import { IHypermediaProcessor } from "./IHypermediaProcessor"; /** @@ -29,14 +29,14 @@ export interface IHydraClient { * @param urlOrResource {string | IResource | ILink } Either URL, {@link IResource} pr {@link ILink} * carrying an IRI of the resource to be obtained. */ - getResource(urlOrResource: string | IResource | ILink): Promise; + getResource(urlOrResource: string | IResource | ILink): Promise; /** * Invokes a given operation. * @param {IOperation} operation Operation descriptor to be invoked. - * @param {IWebResource} body Optional resource to be used as a body of the operation. + * @param {IResource} body Optional resource to be used as a body of the operation. * @param {object} parameters Optional auxiliary parameters. * @returns {Promise} */ - invoke(operation: IOperation, body?: IWebResource, parameters?: object): Promise; + invoke(operation: IOperation, body?: IResource, parameters?: object): Promise; } diff --git a/src/IHypermediaProcessor.ts b/src/IHypermediaProcessor.ts index 715e8fd3..ffc46f2c 100644 --- a/src/IHypermediaProcessor.ts +++ b/src/IHypermediaProcessor.ts @@ -1,4 +1,4 @@ -import { IWebResource } from "./DataModel/IWebResource"; +import { IHypermediaContainer } from "./DataModel/IHypermediaContainer"; import { IHydraClient } from "./IHydraClient"; import { IHypermediaProcessingOptions } from "./IHypermediaProcessingOptions"; import { Level } from "./Level"; @@ -28,7 +28,11 @@ export interface IHypermediaProcessor { * @param {Response} response Raw fetch response holding data to be parsed. * @param {IHydraClient} client Hydra client. * @param {IHypermediaProcessingOptions} options Optional additional processing options. - * @returns {Promise} + * @returns {Promise} */ - process(response: Response, client: IHydraClient, options?: IHypermediaProcessingOptions): Promise; + process( + response: Response, + client: IHydraClient, + options?: IHypermediaProcessingOptions + ): Promise; } diff --git a/src/JsonLd/GraphTransformations/CompoundGraphTransformer.ts b/src/JsonLd/GraphTransformations/CompoundGraphTransformer.ts index abd90de6..dea2b310 100644 --- a/src/JsonLd/GraphTransformations/CompoundGraphTransformer.ts +++ b/src/JsonLd/GraphTransformations/CompoundGraphTransformer.ts @@ -17,14 +17,14 @@ export default class CompoundGraphTransformer implements IGraphTransformer { } /** @inheritDoc */ - public async transform( + public transform( graph: object[], processor: IHypermediaProcessor, options?: IHypermediaProcessingOptions - ): Promise { + ): object[] { let result = graph; for (const graphTransformer of this.graphTransformers) { - result = await graphTransformer.transform(result, processor, options); + result = graphTransformer.transform(result, processor, options); } return result; diff --git a/src/JsonLd/GraphTransformations/EntryPointCorrectingGraphTransformer.ts b/src/JsonLd/GraphTransformations/EntryPointCorrectingGraphTransformer.ts index 5a516f88..4218c29b 100644 --- a/src/JsonLd/GraphTransformations/EntryPointCorrectingGraphTransformer.ts +++ b/src/JsonLd/GraphTransformations/EntryPointCorrectingGraphTransformer.ts @@ -1,5 +1,6 @@ import { IHypermediaProcessingOptions } from "../../../src/IHypermediaProcessingOptions"; import { IHypermediaProcessor } from "../../IHypermediaProcessor"; +import { Level } from "../../Level"; import { hydra } from "../../namespaces"; import { IGraphTransformer } from "./IGraphTransformer"; @@ -8,17 +9,19 @@ import { IGraphTransformer } from "./IGraphTransformer"; */ export default class EntryPointCorrectingGraphTransformer implements IGraphTransformer { /** @inheritDoc */ - public async transform( + public transform( graph: object[], processor: IHypermediaProcessor, options?: IHypermediaProcessingOptions - ): Promise { - const apiDocumentation = graph.find(_ => !!_["@type"] && _["@type"].indexOf(hydra.ApiDocumentation) !== -1); + ): object[] { + const apiDocumentation = !!graph + ? graph.find(_ => !!_["@type"] && _["@type"].indexOf(hydra.ApiDocumentation) !== -1) + : null; if ( !!apiDocumentation && !apiDocumentation[hydra.entrypoint] && !!options && - processor.supports(options.auxiliaryResponse) && + processor.supports(options.auxiliaryResponse) !== Level.None && options.auxiliaryOriginalUrl.match(/.+:\/\/[^\/]+\/?/) ) { apiDocumentation[hydra.entrypoint] = [{ "@id": options.auxiliaryOriginalUrl }]; diff --git a/src/JsonLd/GraphTransformations/FlatteningGraphTransformer.ts b/src/JsonLd/GraphTransformations/FlatteningGraphTransformer.ts index d2485547..42aadf44 100644 --- a/src/JsonLd/GraphTransformations/FlatteningGraphTransformer.ts +++ b/src/JsonLd/GraphTransformations/FlatteningGraphTransformer.ts @@ -7,11 +7,11 @@ import { IGraphTransformer } from "./IGraphTransformer"; */ export default class FlatteningGraphTransformer implements IGraphTransformer { /** @inheritDoc */ - public async transform( + public transform( graph: object[], processor: IHypermediaProcessor, options?: IHypermediaProcessingOptions - ): Promise { + ): object[] { let result = graph; if (graph.find(_ => _["@graph"])) { result = [].concat.apply( diff --git a/src/JsonLd/GraphTransformations/IGraphTransformer.ts b/src/JsonLd/GraphTransformations/IGraphTransformer.ts index 98bb46f5..becbe30c 100644 --- a/src/JsonLd/GraphTransformations/IGraphTransformer.ts +++ b/src/JsonLd/GraphTransformations/IGraphTransformer.ts @@ -11,11 +11,11 @@ export interface IGraphTransformer { * @param {object[]} graph Graph to be transformed. * @param {IHypermediaProcessor} processor Hypermedia processor requesting a graph transformation. * @param {IHypermediaProcessingOptions} options Additional processing options. - * @returns {Promise} + * @returns {object[]} */ transform( graph: object[], processor: IHypermediaProcessor, options?: IHypermediaProcessingOptions - ): Promise; + ): object[]; } diff --git a/src/JsonLd/JsonLdHelper.ts b/src/JsonLd/JsonLdHelper.ts index 214f9379..08bb9587 100644 --- a/src/JsonLd/JsonLdHelper.ts +++ b/src/JsonLd/JsonLdHelper.ts @@ -1,7 +1,7 @@ import { hydra } from "../namespaces"; export const JsonLdHelper = { - validKeys(instance: object, nonHydra = false): Iterable { + validKeys(instance: object, nonHydra = false): string[] { return Object.keys(instance).filter( _ => _.length > 0 && _.charAt(0) !== "@" && (!nonHydra || Object.keys(hydra).indexOf(_) === -1) ); diff --git a/src/JsonLd/JsonLdHypermediaProcessor.ts b/src/JsonLd/JsonLdHypermediaProcessor.ts index d751acf2..9ac04c62 100644 --- a/src/JsonLd/JsonLdHypermediaProcessor.ts +++ b/src/JsonLd/JsonLdHypermediaProcessor.ts @@ -5,7 +5,7 @@ import OperationsCollection from "../DataModel/Collections/OperationsCollection" import TypesCollection from "../DataModel/Collections/TypesCollection"; import HypermediaContainer from "../DataModel/HypermediaContainer"; import { IHydraResource } from "../DataModel/IHydraResource"; -import { IWebResource } from "../DataModel/IWebResource"; +import { IHypermediaContainer } from "../DataModel/IHypermediaContainer"; import { HttpCallFacility } from "../HydraClientFactory"; import { IHydraClient } from "../IHydraClient"; import { IHypermediaProcessingOptions } from "../IHypermediaProcessingOptions"; @@ -106,11 +106,11 @@ export default class JsonLdHypermediaProcessor implements IHypermediaProcessor { response: Response, client: IHydraClient, options?: IHypermediaProcessingOptions - ): Promise { + ): Promise { options = { ...{ linksPolicy: LinksPolicy.Strict, originalUrl: response.url }, ...(options || {}) }; const result = await this.ensureJsonLd(response); let flattenPayload = await jsonld.promises.flatten(result, null, { base: response.url, embed: "@link" }); - flattenPayload = await this.graphTransformer.transform(flattenPayload, this, options); + flattenPayload = this.graphTransformer.transform(flattenPayload, this, options); const context = await this.processHypermedia( new ProcessingState(flattenPayload, response.url, client, options.linksPolicy) ); @@ -131,11 +131,9 @@ export default class JsonLdHypermediaProcessor implements IHypermediaProcessor { ); } - const hypermediaContainer = new HypermediaContainer(response.headers, rootResource, hypermedia); - Object.defineProperty(result, "hypermedia", { enumerable: false, value: hypermediaContainer }); - Object.defineProperty(result, "iri", { enumerable: false, value: rootResource.iri }); - Object.defineProperty(result, "type", { enumerable: false, value: rootResource.type }); - return result; + const hypermediaContainer = new HypermediaContainer(response, rootResource, hypermedia); + (hypermediaContainer as any).json = () => result; + return hypermediaContainer; } private static tryRemoveReferenceFrom(graph: object[], index: number): boolean { diff --git a/src/JsonLd/ProcessingState.ts b/src/JsonLd/ProcessingState.ts index 22eaa95d..fc948497 100644 --- a/src/JsonLd/ProcessingState.ts +++ b/src/JsonLd/ProcessingState.ts @@ -271,7 +271,7 @@ export default class ProcessingState { }; for (const expectedType of Object.keys(factories)) { - if (result.type.contains(expectedType)) { + if (expectedType === "any" || result.type.contains(expectedType)) { result = factories[expectedType](result, this.client, this); } } diff --git a/src/JsonLd/apiDocumentation.ts b/src/JsonLd/apiDocumentation.ts index d5043e98..ce91f038 100644 --- a/src/JsonLd/apiDocumentation.ts +++ b/src/JsonLd/apiDocumentation.ts @@ -11,7 +11,7 @@ export function apiDocumentation(mappings: IDictionary): IDict type: [hydra.ApiDocumentation as string] }; mappings[hydra.title] = { - propertyName: "title", + propertyName: "displayName", type: [hydra.ApiDocumentation as string] }; mappings[hydra.supportedClass] = { diff --git a/src/JsonLd/collection.ts b/src/JsonLd/collection.ts index 950de8ae..a6485778 100644 --- a/src/JsonLd/collection.ts +++ b/src/JsonLd/collection.ts @@ -51,22 +51,22 @@ export function collection(mappings: IDictionary): IDictionary mappings[hydra.first] = { default: (first, processingState) => convertToResource(first[0], processingState), propertyName: "first", - type: [hydra.PartialCollectionView as string] + type: [hydra.Resource as string] }; mappings[hydra.last] = { default: (last, processingState) => convertToResource(last[0], processingState), propertyName: "last", - type: [hydra.PartialCollectionView as string] + type: [hydra.Resource as string] }; mappings[hydra.next] = { default: (next, processingState) => convertToResource(next[0], processingState), propertyName: "next", - type: [hydra.PartialCollectionView as string] + type: [hydra.Resource as string] }; mappings[hydra.previous] = { default: (previous, processingState) => convertToResource(previous[0], processingState), propertyName: "previous", - type: [hydra.PartialCollectionView as string] + type: [hydra.Resource as string] }; return mappings; } diff --git a/src/JsonLd/factories.ts b/src/JsonLd/factories.ts index 10b89bd2..bf5b466f 100644 --- a/src/JsonLd/factories.ts +++ b/src/JsonLd/factories.ts @@ -21,3 +21,12 @@ factories[hydra.ApiDocumentation] = (resource, client) => { }; factories[hydra.Collection] = partialCollectionIteratorFactory; + +factories.any = resource => { + if (!!(resource as any).displayName) { + Object.defineProperty(resource, "displayName", { get: () => (this.title || this.label) || "" }); + Object.defineProperty(resource, "textDescription", { get: () => (this.description || this.comments) || "" }); + } + + return resource; +}; \ No newline at end of file diff --git a/src/JsonLd/linksExtractor.ts b/src/JsonLd/linksExtractor.ts index 6b4c8320..f381d04a 100644 --- a/src/JsonLd/linksExtractor.ts +++ b/src/JsonLd/linksExtractor.ts @@ -17,12 +17,39 @@ hydraLinks[hydra.previous] = hydra.Link; hydraLinks[hydra.next] = hydra.Link; hydraLinks[hydra.view] = hydra.Link; hydraLinks[hydra.collection] = hydra.Link; +hydraLinks[hydra.freetextQuery] = hydra.TemplatedLink; hydraLinks[hydra.search] = hydra.TemplatedLink; +const standaloneControls = {}; +standaloneControls[hydra.view] = true; +standaloneControls[hydra.collection] = true; +standaloneControls[hydra.supportedClass] = true; +standaloneControls[hydra.supportedOperation] = true; +standaloneControls[hydra.supportedProperty] = true; +standaloneControls[hydra.entrypoint] = true; +standaloneControls[hydra.manages] = true; +standaloneControls[hydra.member] = true; +standaloneControls[hydra.first] = true; +standaloneControls[hydra.last] = true; +standaloneControls[hydra.next] = true; +standaloneControls[hydra.previous] = true; +standaloneControls[hydra.mapping] = true; +standaloneControls[hydra.variableRepresentation] = true; +standaloneControls[hydra.expects] = true; +standaloneControls[hydra.returns] = true; +standaloneControls[hydra.subject] = true; +standaloneControls[hydra.property] = true; +standaloneControls[hydra.object] = true; +standaloneControls[hydra.operation] = true; + function isLink(type) { return type === hydra.Link || type === hydra.TemplatedLink; } +function isStandaloneControl(predicate: string): boolean { + return !!standaloneControls[predicate]; +} + function tryGetPredicateLinkType(predicate: string, processingState: ProcessingState): string { let result = hydraLinks[predicate] || null; if (!result) { @@ -38,7 +65,7 @@ function tryGetPredicateLinkType(predicate: string, processingState: ProcessingS function tryGetResourceLinkType(iri: string, type: string[], processingState: ProcessingState): string { let result = null; if (!!type) { - result = type.find(isLink) || null; + result = !!type.find(_ => _ === hydra.IriTemplate) ? hydra.TemplatedLink : null; } if (!result && iri.charAt(0) !== "_") { @@ -65,7 +92,10 @@ function tryGetResourceLinkType(iri: string, type: string[], processingState: Pr function internalLinksExtractor(resources: any[], processingState: ProcessingState): ILink[] { const links = []; const originalResource = processingState.processedObject; - for (const predicate of JsonLd.validKeys(originalResource)) { + const possiblePredicates = JsonLd.validKeys(originalResource).filter( + _ => originalResource[_] instanceof Object && !isStandaloneControl(_) + ); + for (const predicate of possiblePredicates) { const linkType = tryGetPredicateLinkType(predicate, processingState); const possibleLinkedResources = originalResource[predicate].filter(_ => !!_["@id"]); for (const targetResource of possibleLinkedResources) { @@ -82,7 +112,7 @@ function internalLinksExtractor(resources: any[], processingState: ProcessingSta let link = { baseUrl: processingState.baseUrl, collections: new CollectionsCollection(), - iri: targetResource["@id"], + iri: predicate, links: LinksCollection.empty, operations: OperationsCollection.empty, relation: predicate, diff --git a/src/JsonLd/mappings.ts b/src/JsonLd/mappings.ts index 6e30d1df..6dec4e17 100644 --- a/src/JsonLd/mappings.ts +++ b/src/JsonLd/mappings.ts @@ -46,13 +46,23 @@ mappings[hydra.title] = { default: "", propertyName: "displayName", required: true, - type: [hydra.Class as string, hydra.ApiDocumentation as string] + type: [ + hydra.Class as string, + hydra.ApiDocumentation as string, + hydra.Link as string, + hydra.Operation as string, + hydra.SupportedProperty as string] }; mappings[hydra.description] = { default: "", propertyName: "description", required: true, - type: [hydra.Class as string, hydra.ApiDocumentation as string] + type: [ + hydra.Class as string, + hydra.ApiDocumentation as string, + hydra.Link as string, + hydra.Operation as string, + hydra.SupportedProperty as string] }; mappings.links = { default: linksExtractor, diff --git a/src/JsonLd/partialCollectionIteratorFactory.ts b/src/JsonLd/partialCollectionIteratorFactory.ts index 82e42b98..15a9321c 100644 --- a/src/JsonLd/partialCollectionIteratorFactory.ts +++ b/src/JsonLd/partialCollectionIteratorFactory.ts @@ -1,47 +1,51 @@ -import LinksCollection from "../DataModel/Collections/LinksCollection"; +import ResourceFilterableCollection from "../DataModel/Collections/ResourceFilterableCollection"; import TypesCollection from "../DataModel/Collections/TypesCollection"; import { ICollection } from "../DataModel/ICollection"; -import { ILink } from "../DataModel/ILink"; +import { IPartialCollectionView } from "../DataModel/IPartialCollectionView"; import { IResource } from "../DataModel/IResource"; import { IHydraClient } from "../IHydraClient"; import { hydra } from "../namespaces"; import ProcessingState from "./ProcessingState"; +interface IBrowsableCollection { + members?: ResourceFilterableCollection; + view?: IPartialCollectionView; +} + interface IState { current?: string; - first?: ILink; - next?: ILink; - prev?: ILink; - last?: ILink; + first?: IResource; + next?: IResource; + prev?: IResource; + last?: IResource; } -function createStateFrom(iri: string, links: LinksCollection): IState { - return update({}, iri, links); +function createStateFrom(iri: string, view: IPartialCollectionView): IState { + return update({}, iri, view); } -function update(state: IState, iri: string, links: LinksCollection): IState { +function update(state: IState, iri: string, view: IPartialCollectionView): IState { state.current = iri; - state.first = links.withRelationOf(hydra.first).first(); - state.next = links.withRelationOf(hydra.next).first(); - state.prev = links.withRelationOf(hydra.previous).first(); - state.last = links.withRelationOf(hydra.last).first(); + state.first = view.first; + state.next = view.next; + state.prev = view.previous; + state.last = view.last; return state; } -async function getPart( - state: IState, - link: ILink, - client: IHydraClient, - collectionIri: string -): Promise> { - const collectionPart = await client.getResource(link.target); - const page = collectionPart.hypermedia.collections.ofIri(collectionIri).first(); - update(state, page.view.iri, page.view.links); +async function getPart(state: IState, link: IResource, client: IHydraClient): Promise> { + const collectionPart = await client.getResource(link); + let page: IBrowsableCollection = collectionPart; + if (!page.view) { + page = collectionPart.where(_ => !!(_ as ICollection).view).first() as ICollection; + } + + update(state, page.view.iri, page.view); return page.members; } -function getTargetOf(link: ILink) { - return link != null ? link.target.iri : null; +function getTargetOf(link: IResource) { + return !!link ? link.iri : null; } export function partialCollectionIteratorFactory( @@ -55,12 +59,12 @@ export function partialCollectionIteratorFactory( ? processingState.processedObject[hydra.view][0]["@id"] : null; const getIterator = () => { - const state = createStateFrom(collection.iri, collection.view.links); + const state = createStateFrom(collection.iri, collection.view); const result = { - getFirstPart: () => getPart(state, state.first, client, collection.iri), - getLastPart: () => getPart(state, state.last, client, collection.iri), - getNextPart: () => getPart(state, state.next, client, collection.iri), - getPreviousPart: () => getPart(state, state.prev, client, collection.iri), + getFirstPart: () => getPart(state, state.first, client), + getLastPart: () => getPart(state, state.last, client), + getNextPart: () => getPart(state, state.next, client), + getPreviousPart: () => getPart(state, state.prev, client), type: new TypesCollection([hydra.PartialCollectionView]) }; Object.defineProperty(result, "currentPartIri", { get: () => state.current }); diff --git a/src/N3/N3HypermediaProcessor.ts b/src/N3/N3HypermediaProcessor.ts index 04ad6dc7..be4241e3 100644 --- a/src/N3/N3HypermediaProcessor.ts +++ b/src/N3/N3HypermediaProcessor.ts @@ -2,7 +2,8 @@ import * as N3Parser from "@rdfjs/parser-n3"; import * as SerializerJsonLd from "@rdfjs/serializer-jsonld"; import { Readable } from "stream"; import { IHypermediaProcessingOptions } from "../../src/IHypermediaProcessingOptions"; -import { IWebResource } from "../DataModel/IWebResource"; +import HypermediaContainer from "../DataModel/HypermediaContainer"; +import { IHypermediaContainer } from "../DataModel/IHypermediaContainer"; import { IHydraClient } from "../IHydraClient"; import { IHypermediaProcessor } from "../IHypermediaProcessor"; import { Level } from "../Level"; @@ -48,9 +49,9 @@ export default class N3HypermediaProcessor implements IHypermediaProcessor { response: Response, client: IHydraClient, options?: IHypermediaProcessingOptions - ): Promise { + ): Promise { const result = []; - const json = await new Promise((success, error) => { + const json = await new Promise((success, error) => { const transformationOptions = { baseIRI: options.originalUrl }; const reader = response.body.getReader(); const stream = new Readable({ @@ -72,10 +73,9 @@ export default class N3HypermediaProcessor implements IHypermediaProcessor { }); const jsonLdResponse = { json: () => json, headers: response.headers, url: response.url } as any; const jsonLdResult = await this.jsonLdProcessor.process(jsonLdResponse, client, options); - Object.defineProperty(result, "hypermedia", { value: jsonLdResult.hypermedia, enumerable: false }); - Object.defineProperty(result, "iri", { value: jsonLdResult.iri, enumerable: false }); - Object.defineProperty(result, "type", { value: jsonLdResult.type, enumerable: false }); - return (result as any) as IWebResource; + const hypermediaContainer = new HypermediaContainer(response, jsonLdResult, jsonLdResult); + (hypermediaContainer as any).dataset = () => result; + return hypermediaContainer; } private static getSupportedMediaTypeFrom(response: Response) { diff --git a/src/N3/package.json b/src/N3/package.json index 44e86053..2e073aaf 100644 --- a/src/N3/package.json +++ b/src/N3/package.json @@ -1,6 +1,6 @@ { "name": "@hydra-cg/heracles.n3", - "version": "0.2.0", + "version": "0.5.0", "contributors": [ { "name": "Karol Szczepanski" diff --git a/src/PartialCollectionCrawler.ts b/src/PartialCollectionCrawler.ts index eb94f5eb..7ec783d8 100644 --- a/src/PartialCollectionCrawler.ts +++ b/src/PartialCollectionCrawler.ts @@ -80,12 +80,8 @@ export default class PartialCollectionCrawler { options = options || {}; const result = []; const memberLimit = options.memberLimit || Number.MAX_SAFE_INTEGER; - if (this.addWithLimitReached(result, this.collection.members, memberLimit)) { - return result; - } - const iterator = this.collection.getIterator(); - if (iterator === null || result.length >= options.memberLimit) { + if (this.addWithLimitReached(result, this.collection.members, memberLimit) || iterator === null) { return result; } diff --git a/tests/DataModel/Collections/LinksCollection.spec.ts b/tests/DataModel/Collections/LinksCollection.spec.ts index 8fabaaa3..c6336443 100644 --- a/tests/DataModel/Collections/LinksCollection.spec.ts +++ b/tests/DataModel/Collections/LinksCollection.spec.ts @@ -3,7 +3,7 @@ import { hydra } from "../../../src/namespaces"; describe("Given instance of the LinksCollection", () => { beforeEach(() => { - const target = "some:resource"; + const target = { iri: "some:resource" }; this.link1 = { relation: "some:resource-url", target, type: [hydra.Link] }; this.link2 = { relation: "some:other-url", target, type: [hydra.TemplatedLink] }; this.link3 = { relation: "yet:another-url", target, type: [hydra.Link] }; @@ -16,23 +16,11 @@ describe("Given instance of the LinksCollection", () => { expect([...this.links]).toEqual(this.allLinks); }); - describe("when narrowing filters with relation type", () => { - beforeEach(() => { - this.relationTypeNorrowedOperations = this.links.withRelationOf("yet:another-url"); - }); - - it("should provide only type matching links", () => { - expect([...this.relationTypeNorrowedOperations]).toEqual([this.link3]); - }); + it("should provide only links matching required relation", () => { + expect([...this.links.withRelationOf("yet:another-url")]).toEqual([this.link3]); }); - describe("when narrowing filters with template", () => { - beforeEach(() => { - this.templateNorrowedOperations = this.links.withTemplate(); - }); - - it("should provide only type matching links", () => { - expect([...this.templateNorrowedOperations]).toEqual([this.link2]); - }); + it("should provide only links with template", () => { + expect([...this.links.withTemplate()]).toEqual([this.link2]); }); }); diff --git a/tests/DataModel/Collections/MappingsCollection.spec.ts b/tests/DataModel/Collections/MappingsCollection.spec.ts index f9532010..bf9089d8 100644 --- a/tests/DataModel/Collections/MappingsCollection.spec.ts +++ b/tests/DataModel/Collections/MappingsCollection.spec.ts @@ -1,9 +1,10 @@ import MappingsCollection from "../../../src/DataModel/Collections/MappingsCollection"; +import { hydra } from "../../../src/namespaces"; describe("Given instance of the MappingsCollection", () => { beforeEach(() => { - this.mapping1 = { is: ["IriTemplateMapping"], variable: "variable1" }; - this.mapping2 = { is: ["IriTemplateMapping"], variable: "variable2" }; + this.mapping1 = { type: [hydra.IriTemplate], variable: "variable1" }; + this.mapping2 = { type: [hydra.IriTemplate], variable: "variable2" }; this.allMappings = [this.mapping1, this.mapping2]; this.mappings = new MappingsCollection(this.allMappings); }); @@ -12,13 +13,7 @@ describe("Given instance of the MappingsCollection", () => { expect([...this.mappings]).toEqual(this.allMappings); }); - describe("when narrowing filters with variable name", () => { - beforeEach(() => { - this.variableNameNorrowedOperations = this.mappings.ofVariableName("variable1"); - }); - - it("should provide only variable name matching mappings", () => { - expect([...this.variableNameNorrowedOperations]).toEqual([this.mapping1]); - }); + it("should provide only variable name matching mappings", () => { + expect([...this.mappings.ofVariableName("variable1")]).toEqual([this.mapping1]); }); }); diff --git a/tests/DataModel/Collections/OperationsCollection.spec.ts b/tests/DataModel/Collections/OperationsCollection.spec.ts index 5010c91d..48899304 100644 --- a/tests/DataModel/Collections/OperationsCollection.spec.ts +++ b/tests/DataModel/Collections/OperationsCollection.spec.ts @@ -34,73 +34,31 @@ describe("Given instance of the OperationsCollection", () => { expect([...this.operations]).toEqual(this.allOperations); }); - describe("when narrowing filters with type", () => { - beforeEach(() => { - this.typeNorrowedOperations = this.operations.ofType("OperationType1"); - }); - - it("should provide only type matching operations", () => { - expect([...this.typeNorrowedOperations]).toEqual([this.operation1, this.operation2]); - }); - - describe("and narrowing filters with expected type", () => { - beforeEach(() => { - this.typeAndExpectationNarrowedOperations = this.typeNorrowedOperations.expecting("ExpectedType2"); - }); - - it("should provide only type and expected type matching operations", () => { - expect([...this.typeAndExpectationNarrowedOperations]).toEqual([this.operation2]); - }); - }); - - describe("and narrowing filters with returned type", () => { - beforeEach(() => { - this.typeAndReturnedNarrowedOperations = this.typeNorrowedOperations.returning("ReturnedType2"); - }); - - it("should provide only type and expected type matching operations", () => { - expect([...this.typeAndReturnedNarrowedOperations]).toEqual([this.operation2]); - }); - }); + it("should provide only type matching operations", () => { + expect([...this.operations.ofType("OperationType1")]).toEqual([this.operation1, this.operation2]); }); - describe("when narrowing filters with expected headers", () => { - beforeEach(() => { - this.expectedHeaderNorrowedOperations = this.operations.expectingHeader("InHeader1"); - }); - - it("should provide only type matching operations", () => { - expect([...this.expectedHeaderNorrowedOperations]).toEqual([this.operation1]); - }); + it("should provide only type and expected type matching operations", () => { + expect([...this.operations.ofType("OperationType1").expecting("ExpectedType2")]).toEqual([this.operation2]); }); - describe("when narrowing filters with returned headers", () => { - beforeEach(() => { - this.returnedHeaderNorrowedOperations = this.operations.returningHeader("OutHeader1"); - }); - - it("should provide only type matching operations", () => { - expect([...this.returnedHeaderNorrowedOperations]).toEqual([this.operation1]); - }); + it("should provide only type and returned type matching operations", () => { + expect([...this.operations.ofType("OperationType1").returning("ReturnedType2")]).toEqual([this.operation2]); }); - describe("when narrowing filters with method", () => { - beforeEach(() => { - this.methodNorrowedOperations = this.operations.ofMethod("SOME"); - }); + it("should provide only expected headers matching operations", () => { + expect([...this.operations.expectingHeader("InHeader1")]).toEqual([this.operation1]); + }); - it("should provide only type matching operations", () => { - expect([...this.methodNorrowedOperations]).toEqual([this.operation1]); - }); + it("should provide only returned headers matching operations", () => { + expect([...this.operations.returningHeader("OutHeader1")]).toEqual([this.operation1]); }); - describe("when narrowing filters with template", () => { - beforeEach(() => { - this.templateNorrowedOperations = this.operations.withTemplate(); - }); + it("should provide only method matching operations", () => { + expect([...this.operations.ofMethod("SOME")]).toEqual([this.operation1]); + }); - it("should provide only type matching operations", () => { - expect([...this.templateNorrowedOperations]).toEqual([this.operation4]); - }); + it("should provide only templated operations", () => { + expect([...this.operations.withTemplate()]).toEqual([this.operation4]); }); }); diff --git a/tests/DataModel/Collections/ResourceFilterableCollection.spec.ts b/tests/DataModel/Collections/ResourceFilterableCollection.spec.ts index c732bdb6..174ac01b 100644 --- a/tests/DataModel/Collections/ResourceFilterableCollection.spec.ts +++ b/tests/DataModel/Collections/ResourceFilterableCollection.spec.ts @@ -19,33 +19,20 @@ describe("Given instance of the ResourceFilterableCollection", () => { expect([...this.resources]).toEqual(this.allResources); }); - describe("when narrowing filters to those of a specific types", () => { - beforeEach(() => { - this.typedResources = this.resources.ofType("http://temp.uri/vocab#Class"); - }); - - it("should provide only resources of that type specified", () => { - expect([...this.typedResources]).toEqual([this.resource1]); - }); - - describe("and when narrowing filters with non-blank resources", () => { - beforeEach(() => { - this.nonBlankResources = this.typedResources.nonBlank(); - }); - - it("should provide an empty collection", () => { - expect(this.nonBlankResources.any()).toBeFalsy(); - }); - }); + it("should provide only resources of type specified", () => { + expect([...this.resources.ofType("http://temp.uri/vocab#Class")]).toEqual([this.resource1]); }); - describe("when narrowing filters with non-blank resources", () => { - beforeEach(() => { - this.nonBlankResources = this.resources.nonBlank(); - }); + it("should provide only non-blank resources of type specified", () => { + expect( + this.resources + .ofType("http://temp.uri/vocab#Class") + .nonBlank() + .any() + ).toBeFalsy(); + }); - it("should provide only non-blank resources", () => { - expect([...this.nonBlankResources]).toEqual([this.resource2]); - }); + it("should provide only non-blank resources", () => { + expect([...this.resources.nonBlank()]).toEqual([this.resource2]); }); }); diff --git a/tests/DataModel/Collections/TypesCollection.spec.ts b/tests/DataModel/Collections/TypesCollection.spec.ts index 9f0aab2f..00561aee 100644 --- a/tests/DataModel/Collections/TypesCollection.spec.ts +++ b/tests/DataModel/Collections/TypesCollection.spec.ts @@ -16,19 +16,15 @@ describe("Given instance of the TypesCollection", () => { expect([...this.type]).toEqual(this.allTypes); }); - describe("when checking whether a collection has a given type", () => { - it("should confirm an existing type", () => { - expect(this.type.contains(this.type1)).toBeTruthy(); - }); + it("should confirm a contained type", () => { + expect(this.type.contains(this.type1)).toBeTruthy(); + }); - it("should not confirm an existing type", () => { - expect(this.type.contains("whatever type")).toBeFalsy(); - }); + it("should not confirm not contained type", () => { + expect(this.type.contains("whatever type")).toBeFalsy(); }); - describe("when excluding a type", () => { - it("should actually exclude that type", () => { - expect(this.type.except(this.type1).toArray()).toEqual([this.type2]); - }); + it("should exclude required type", () => { + expect(this.type.except(this.type1).toArray()).toEqual([this.type2]); }); }); diff --git a/tests/DataModel/IApiDocumentation.spec.ts b/tests/DataModel/IApiDocumentation.spec.ts index a9df62a8..35a77057 100644 --- a/tests/DataModel/IApiDocumentation.spec.ts +++ b/tests/DataModel/IApiDocumentation.spec.ts @@ -17,24 +17,19 @@ describe("Given an instance of the IApiDocumentation interface", () => { }); describe("when obtaining an entry point", () => { - beforeEach(() => { - this.client.getResource.returns((this.entryPoint = returnOk())); - }); - - it( - "should call the client", + beforeEach( run(async () => { - await this.apiDocumentation.getEntryPoint(); - - expect(this.client.getResource).toHaveBeenCalledWith(this.apiDocumentation.entryPoint); + this.client.getResource.returns((this.entryPoint = returnOk())); + this.result = await this.apiDocumentation.getEntryPoint(); }) ); - it( - "should provide a correct result", - run(async () => { - expect(await this.apiDocumentation.getEntryPoint()).toBe(this.entryPoint); - }) - ); + it("should call the client", () => { + expect(this.client.getResource).toHaveBeenCalledWith(this.apiDocumentation.entryPoint); + }); + + it("should provide a correct result", () => { + expect(this.result).toBe(this.entryPoint); + }); }); }); diff --git a/tests/DataModel/ICollection.spec.ts b/tests/DataModel/ICollection.spec.ts index 63c6a1bc..eed34f88 100644 --- a/tests/DataModel/ICollection.spec.ts +++ b/tests/DataModel/ICollection.spec.ts @@ -2,23 +2,12 @@ import * as sinon from "sinon"; import LinksCollection from "../../src/DataModel/Collections/LinksCollection"; import ResourceFilterableCollection from "../../src/DataModel/Collections/ResourceFilterableCollection"; import TypesCollection from "../../src/DataModel/Collections/TypesCollection"; -import { ICollection } from "../../src/DataModel/ICollection"; -import { ILink } from "../../src/DataModel/ILink"; import { IResource } from "../../src/DataModel/IResource"; import { factories } from "../../src/JsonLd/factories"; import { hydra } from "../../src/namespaces"; import { run } from "../../testing/AsyncHelper"; function collectionOf(iri: string, next: IResource, previous: IResource, ...iris: string[]) { - const links: ILink[] = []; - if (next) { - links.push({ relation: hydra.next, target: next } as any); - } - - if (previous) { - links.push({ relation: hydra.previous, target: previous } as any); - } - const members: IResource[] = []; for (const item of iris) { members.push({ iri: item } as any); @@ -26,18 +15,18 @@ function collectionOf(iri: string, next: IResource, previous: IResource, ...iris const collection = { members: new ResourceFilterableCollection(members), - view: { - iri, - links: new LinksCollection(links) - } - }; - const result = { - hypermedia: { - collections: new ResourceFilterableCollection([collection as ICollection]) - } + view: { iri } }; - return result; + if (!!next) { + (collection.view as any).next = next; + } + + if (!!previous) { + (collection.view as any).previous = previous; + } + + return collection; } describe("Given an instance of the ICollection interface", () => { @@ -47,8 +36,6 @@ describe("Given an instance of the ICollection interface", () => { describe("which has no view associated", () => { beforeEach(() => { - this.members = []; - this.client.getResource.returns(this.members); const setup: any = { links: LinksCollection.empty, members: this.members, @@ -70,7 +57,7 @@ describe("Given an instance of the ICollection interface", () => { this.firstBatch = collectionOf("view:1", this.secondPage, null, "some:item"); this.secondBatch = collectionOf("view:2", this.lastPage, this.firstPage, "some:another-item"); this.lastBatch = collectionOf("view:3", null, this.secondPage, "yet:another-item"); - this.initialLink = { relation: null, target: this.secondPage }; + this.initialLink = this.secondPage; this.initialMembers = []; const collectionResource = {}; collectionResource[hydra.view] = [{ "@id": "view:1" }]; @@ -79,7 +66,8 @@ describe("Given an instance of the ICollection interface", () => { type: new TypesCollection([hydra.Collection]), view: { iri: collectionResource[hydra.view][0]["@id"], - links: new LinksCollection([this.initialLink as any]) + next: this.initialLink, + previous: this.initialLink } }; this.result = []; @@ -91,10 +79,7 @@ describe("Given an instance of the ICollection interface", () => { describe("by following next links", () => { beforeEach( run(async () => { - this.initialLink.relation = hydra.next; - Array.from(this.firstBatch.hypermedia.collections.first().members).forEach(item => - this.initialMembers.push(item) - ); + Array.from(this.firstBatch.members).forEach(item => this.initialMembers.push(item)); this.client.getResource .onFirstCall() .returns(this.secondBatch) @@ -123,20 +108,14 @@ describe("Given an instance of the ICollection interface", () => { }); it("should provide a correct result", () => { - expect(this.result).toEqual([ - this.secondBatch.hypermedia.collections.first().members.first(), - this.lastBatch.hypermedia.collections.first().members.first() - ]); + expect(this.result).toEqual([this.secondBatch.members.first(), this.lastBatch.members.first()]); }); }); describe("by following previous links", () => { beforeEach( run(async () => { - this.initialLink.relation = hydra.previous; - Array.from(this.lastBatch.hypermedia.collections.first().members).forEach(item => - this.initialMembers.push(item) - ); + Array.from(this.lastBatch.members).forEach(item => this.initialMembers.push(item)); this.client.getResource .onFirstCall() .returns(this.secondBatch) @@ -165,10 +144,7 @@ describe("Given an instance of the ICollection interface", () => { }); it("should provide a correct result", () => { - expect(this.result).toEqual([ - this.secondBatch.hypermedia.collections.first().members.first(), - this.firstBatch.hypermedia.collections.first().members.first() - ]); + expect(this.result).toEqual([this.secondBatch.members.first(), this.firstBatch.members.first()]); }); }); }); diff --git a/tests/DataModel/TemplatedLink.spec.ts b/tests/DataModel/TemplatedLink.spec.ts index a59401b3..631c7960 100644 --- a/tests/DataModel/TemplatedLink.spec.ts +++ b/tests/DataModel/TemplatedLink.spec.ts @@ -7,11 +7,10 @@ describe("Given instance of the TemplatedLink", () => { beforeEach(() => { jasmine.addMatchers({ toBeLike: () => new HydraResourceMatcher() }); this.template = { - template: "some-uri{?with-variable}" + template: "some-uri{?with_variable}" }; this.originalLink = { baseUrl: "http://temp.uri/", - method: "GET", target: { iri: "test-url" }, type: new TypesCollection([hydra.Link]) }; @@ -24,11 +23,11 @@ describe("Given instance of the TemplatedLink", () => { describe("when expanding URI with variable values", () => { beforeEach(() => { - this.result = this.link.expandTarget({ "with-variable": "test-value" }); + this.result = this.link.expandTarget({ with_variable: "test-value" }); }); it("should provide an expanded URL", () => { - expect(this.result.target).toBeLike({ iri: "http://temp.uri/some-uri?with-variable=test-value", type: [] }); + expect(this.result.target).toBeLike({ iri: "http://temp.uri/some-uri?with_variable=test-value", type: [] }); }); it("should copy original operation's types", () => { diff --git a/tests/DataModel/TemplatedOperation.spec.ts b/tests/DataModel/TemplatedOperation.spec.ts index 5cd6a2c1..b9f77b74 100644 --- a/tests/DataModel/TemplatedOperation.spec.ts +++ b/tests/DataModel/TemplatedOperation.spec.ts @@ -9,7 +9,7 @@ describe("Given instance of the TemplatedOperation", () => { beforeEach(() => { jasmine.addMatchers({ toBeLike: () => new HydraResourceMatcher() }); this.template = { - template: "some-uri{?with-variable}" + template: "some-uri{?with_variable}" }; this.originalOperation = { baseUrl: "http://temp.uri/", @@ -23,11 +23,11 @@ describe("Given instance of the TemplatedOperation", () => { describe("when expanding URI with variable values", () => { beforeEach(() => { - this.result = this.operation.expandTarget({ "with-variable": "test-value" }); + this.result = this.operation.expandTarget({ with_variable: "test-value" }); }); it("should provide an expanded URL", () => { - expect(this.result.target).toBeLike({ iri: "http://temp.uri/some-uri?with-variable=test-value", type: [] }); + expect(this.result.target).toBeLike({ iri: "http://temp.uri/some-uri?with_variable=test-value", type: [] }); }); it("should pass a correct method", () => { diff --git a/tests/HydraClient.spec.ts b/tests/HydraClient.spec.ts index 361bb83a..0850782b 100644 --- a/tests/HydraClient.spec.ts +++ b/tests/HydraClient.spec.ts @@ -1,4 +1,5 @@ import * as sinon from "sinon"; +import TypesCollection from "../src/DataModel/Collections/TypesCollection"; import HydraClient from "../src/HydraClient"; import { Level } from "../src/Level"; import { LinksPolicy } from "../src/LinksPolicy"; @@ -24,10 +25,6 @@ describe("Given an instance of the HydraClient class", () => { ); }); - it("should create an instance", () => { - expect(this.client).toEqual(jasmine.any(HydraClient)); - }); - it("should register a hypermedia processor", () => { expect(this.client.getHypermediaProcessor(returnOk())).toBe(this.hypermediaProcessor); }); @@ -38,7 +35,7 @@ describe("Given an instance of the HydraClient class", () => { "should throw", run(async () => { try { - await this.client.getApiDocumentation({ iri: null }); + await this.client.getApiDocumentation({ iri: null, type: TypesCollection.empty }); } catch (e) { expect(e.message).toBe(HydraClient.noUrlProvided); } @@ -120,7 +117,7 @@ describe("Given an instance of the HydraClient class", () => { "should throw", run(async () => { try { - await this.client.getApiDocumentation({ iri: this.baseUrl }); + await this.client.getApiDocumentation({ iri: this.baseUrl, type: TypesCollection.empty }); } catch (e) { expect(e.message).toMatch(HydraClient.invalidResponse); } @@ -139,7 +136,7 @@ describe("Given an instance of the HydraClient class", () => { "should throw", run(async () => { try { - await this.client.getApiDocumentation({ iri: this.baseUrl }); + await this.client.getApiDocumentation({ iri: this.baseUrl, type: TypesCollection.empty }); } catch (e) { expect(e.message).toBe(HydraClient.responseFormatNotSupported); } @@ -151,14 +148,14 @@ describe("Given an instance of the HydraClient class", () => { beforeEach(() => { this.apiDocumentationResponse = returnOk(); this.httpCall.withArgs(`${this.baseUrl}api/documentation`).returns(this.apiDocumentationResponse); - this.hypermediaProcessor.process.returns({ hypermedia: { ofType: () => ({ first: () => null }) } }); + this.hypermediaProcessor.process.returns({ ofType: () => ({ first: () => null }) }); }); it( "should throw", run(async () => { try { - await this.client.getApiDocumentation({ iri: this.baseUrl }); + await this.client.getApiDocumentation({ iri: this.baseUrl, type: TypesCollection.empty }); } catch (e) { expect(e.message).toBe(HydraClient.noEntryPointDefined); } @@ -175,7 +172,7 @@ describe("Given an instance of the HydraClient class", () => { (this.data as any).ofType = sinon.stub().returns({ first: sinon.stub().returns(this.apiDocumentation) }); this.apiDocumentationResponse = returnOk(this.apiDocumentationUrl, this.data); this.httpCall.withArgs(this.apiDocumentationUrl).returns(this.apiDocumentationResponse); - this.hypermediaProcessor.process.returns(Promise.resolve({ hypermedia: this.data })); + this.hypermediaProcessor.process.returns(Promise.resolve(this.data)); await this.client.getApiDocumentation(this.baseUrl); }) ); @@ -214,7 +211,7 @@ describe("Given an instance of the HydraClient class", () => { "should throw", run(async () => { try { - await this.client.getResource({ iri: null }); + await this.client.getResource({ iri: null, type: TypesCollection.empty }); } catch (e) { expect(e.message).toBe(HydraClient.noUrlProvided); } @@ -260,8 +257,8 @@ describe("Given an instance of the HydraClient class", () => { describe("and that resource was provided correctly", () => { beforeEach( run(async () => { - this.resource = { hypermedia: {} }; - this.resourceResponse = returnOk(this.resource); + this.resource = {}; + this.resourceResponse = returnOk(this.resourceUrl, this.resource); this.httpCall.withArgs(this.resourceUrl).returns(Promise.resolve(this.resourceResponse)); this.hypermediaProcessor.process.withArgs(this.resourceResponse).returns(Promise.resolve(this.resource)); this.result = await this.client.getResource(this.resourceUrl); diff --git a/tests/HydraClientFactory.spec.ts b/tests/HydraClientFactory.spec.ts index 03d5ed30..850ed678 100644 --- a/tests/HydraClientFactory.spec.ts +++ b/tests/HydraClientFactory.spec.ts @@ -22,7 +22,7 @@ describe("Given instance of HydraClientFactory class", () => { expect((this.factory.withSameRootLinks().andCreate() as any).linksPolicy).toBe(LinksPolicy.SameRoot); }); - it("should create a client with customer hypermedia processor", () => { + it("should create a client with custom hypermedia processor", () => { expect( this.factory .with(this.processor) diff --git a/tests/JsonLd/GraphTransformers/EntryPointCorrectingGraphTransformer.spec.ts b/tests/JsonLd/GraphTransformers/EntryPointCorrectingGraphTransformer.spec.ts index 45028ea0..a34d248c 100644 --- a/tests/JsonLd/GraphTransformers/EntryPointCorrectingGraphTransformer.spec.ts +++ b/tests/JsonLd/GraphTransformers/EntryPointCorrectingGraphTransformer.spec.ts @@ -2,7 +2,6 @@ import * as sinon from "sinon"; /* tslint:disable:max-line-length */ import EntryPointCorrectingGraphTransformer from "../../../src/JsonLd/GraphTransformations/EntryPointCorrectingGraphTransformer"; import { hydra } from "../../../src/namespaces"; -import { run } from "../../../testing/AsyncHelper"; import { returnOk } from "../../../testing/ResponseHelper"; describe("Given instance of EntryPointCorrectingGraphTransformer class", () => { @@ -10,7 +9,6 @@ describe("Given instance of EntryPointCorrectingGraphTransformer class", () => { this.entryPoint = { "@id": "http://temp.uri/" }; this.apiDocumentation = { "@id": "http://temp.uri/#documentation", "@type": [hydra.ApiDocumentation] }; this.graph = [this.apiDocumentation]; - this.response = returnOk("http://temp.uri/#documentation", this.apiDocumentation); this.options = { auxiliaryOriginalUrl: "http://temp.uri/", auxiliaryResponse: returnOk("http://temp.uri/", this.entryPoint) @@ -20,17 +18,15 @@ describe("Given instance of EntryPointCorrectingGraphTransformer class", () => { }); describe("when transforming", () => { - beforeEach( - run(async () => { - this.result = await this.transformer.transform(this.graph, this.processor, this.options); - }) - ); + beforeEach(() => { + this.transformer.transform(this.graph, this.processor, this.options); + }); it("should check processor supports the initial request", () => { expect(this.processor.supports).toHaveBeenCalledWith(this.options.auxiliaryResponse); }); - it("should flatten graphs", () => { + it("should setup an entrypoint", () => { expect(this.apiDocumentation[hydra.entrypoint][0]["@id"]).toBe("http://temp.uri/"); }); }); diff --git a/tests/JsonLd/IndirectTypingProvider.spec.ts b/tests/JsonLd/IndirectTypingProvider.spec.ts index 03195e2b..23df3c5b 100644 --- a/tests/JsonLd/IndirectTypingProvider.spec.ts +++ b/tests/JsonLd/IndirectTypingProvider.spec.ts @@ -58,7 +58,7 @@ describe("Given instance of the IndirectTypingProvider class", () => { expect(this.result).toBeTruthy(); }); - it("should check domain", () => { + it("should check range", () => { expect(this.ontologyProvider.getRangeFor).toHaveBeenCalledWith("some:predicate"); }); }); diff --git a/tests/JsonLd/JsonLdHypermediaProcessor.spec.ts b/tests/JsonLd/JsonLdHypermediaProcessor.spec.ts index eb7e1f33..7022a62b 100644 --- a/tests/JsonLd/JsonLdHypermediaProcessor.spec.ts +++ b/tests/JsonLd/JsonLdHypermediaProcessor.spec.ts @@ -73,11 +73,12 @@ describe("Given instance of the JsonLdHypermediaProcessor class", () => { }); }); - describe("JSON-LD response", () => { + describe("JSON-LD response with resource", () => { beforeEach( run(async () => { this.response = returnOk("http://temp.uri/api", inputJsonLd); this.result = await this.hypermediaProcessor.process(this.response, this.client); + this.resource = await this.result.json(); }) ); @@ -86,27 +87,27 @@ describe("Given instance of the JsonLdHypermediaProcessor class", () => { }); it("should process data", () => { - expect(this.result).toEqual(inputJsonLd); + expect(this.resource).toEqual(inputJsonLd); }); it("should discover all collections", () => { - expect(this.result.hypermedia.collections.length).toBe(2); + expect(this.result.collections.length).toBe(2); }); it("should discover people collection", () => { - expect(this.result.hypermedia.collections.first().iri).toMatch("/api/people$"); + expect(this.result.collections.first().iri).toMatch("/api/people$"); }); it("should discover events collection", () => { - expect(this.result.hypermedia.collections.last().iri).toMatch("/api/events$"); + expect(this.result.collections.last().iri).toMatch("/api/events$"); }); it("should provide response headers", () => { - expect(this.result.hypermedia.headers.get("Content-Type")).toBe("application/ld+json"); + expect(this.result.headers.get("Content-Type")).toBe("application/ld+json"); }); it("should separate hypermedia", () => { - expect(this.result.hypermedia).toBeLike([ + expect(this.result).toBeLike([ { collections: [ { @@ -127,7 +128,7 @@ describe("Given instance of the JsonLdHypermediaProcessor class", () => { { baseUrl: "http://temp.uri/api", collections: [], - iri: "http://temp.uri/api/events/closed", + iri: "http://temp.uri/vocab/closed-events", links: [], operations: [], relation: "http://temp.uri/vocab/closed-events", @@ -138,7 +139,7 @@ describe("Given instance of the JsonLdHypermediaProcessor class", () => { { baseUrl: "http://temp.uri/api", collections: [], - iri: "_:b0", + iri: hydra.search, links: [], mappings: [ { @@ -174,71 +175,6 @@ describe("Given instance of the JsonLdHypermediaProcessor class", () => { target: null, template: "http://temp.uri/api/events{?searchPhrase}", type: [hydra.TemplatedLink] - }, - { - baseUrl: "http://temp.uri/api", - collections: [], - iri: "http://temp.uri/api/events?page=1", - links: [ - { - baseUrl: "http://temp.uri/api", - collections: [], - iri: "http://temp.uri/api/events?page=1", - links: [], - operations: [], - relation: hydra.first, - target: { iri: "http://temp.uri/api/events?page=1", type: [] }, - type: [hydra.Link] - }, - { - baseUrl: "http://temp.uri/api", - collections: [], - iri: "http://temp.uri/api/events?page=9", - links: [], - operations: [], - relation: hydra.last, - target: { iri: "http://temp.uri/api/events?page=9", type: [] }, - type: [hydra.Link] - } - ], - operations: [], - relation: hydra.view, - supportedOperations: [], - target: { iri: "_:b2", type: [] }, - type: [hydra.Link] - }, - { - baseUrl: "http://temp.uri/api", - collections: [], - iri: "http://temp.uri/api/events/1", - links: [], - operations: [], - relation: hydra.member, - supportedOperations: [], - target: { iri: "http://temp.uri/api/events/1", type: [] }, - type: [hydra.Link] - }, - { - baseUrl: "http://temp.uri/api", - collections: [], - iri: hydra.first, - links: [], - operations: [], - relation: hydra.first, - supportedOperations: [], - target: { iri: "http://temp.uri/api/events?page=1", type: [] }, - type: [hydra.Link] - }, - { - baseUrl: "http://temp.uri/api", - collections: [], - iri: hydra.last, - links: [], - operations: [], - relation: hydra.last, - supportedOperations: [], - target: { iri: "http://temp.uri/api/events?page=9", type: [] }, - type: [hydra.Link] } ], manages: [], @@ -257,48 +193,15 @@ describe("Given instance of the JsonLdHypermediaProcessor class", () => { view: { collections: [], first: { - baseUrl: "http://temp.uri/api", - collections: [], iri: "http://temp.uri/api/events?page=1", - links: [], - operations: [], - relation: hydra.first, - target: { iri: "http://temp.uri/api/events?page=1", type: [] }, type: [hydra.Link] }, iri: "http://temp.uri/api/events?page=1", last: { - baseUrl: "http://temp.uri/api", - collections: [], iri: "http://temp.uri/api/events?page=9", - links: [], - operations: [], - relation: hydra.last, - target: { iri: "http://temp.uri/api/events?page=9", type: [] }, type: [hydra.Link] }, - links: [ - { - baseUrl: "http://temp.uri/api", - collections: [], - iri: "http://temp.uri/api/events?page=1", - links: [], - operations: [], - relation: hydra.first, - target: { iri: "http://temp.uri/api/events?page=1", type: [] }, - type: [hydra.Link] - }, - { - baseUrl: "http://temp.uri/api", - collections: [], - iri: "http://temp.uri/api/events?page=9", - links: [], - operations: [], - relation: hydra.last, - target: { iri: "http://temp.uri/api/events?page=9", type: [] }, - type: [hydra.Link] - } - ], + links: [], operations: [], type: [hydra.PartialCollectionView] } @@ -341,7 +244,7 @@ describe("Given instance of the JsonLdHypermediaProcessor class", () => { run(async () => { this.response = returnOk(api.people.markus, nestedResourcesInputJsonLd); const result = await this.hypermediaProcessor.process(this.response, this.client); - this.markus = result.hypermedia.where(control => control.iri.match(/markus/)).first(); + this.markus = result.where(control => control.iri.match(/markus/)).first(); this.karol = this.markus.links.withRelationOf(schema.knows).first().target; }) ); @@ -366,7 +269,7 @@ describe("Given instance of the JsonLdHypermediaProcessor class", () => { run(async () => { this.response = returnOk("http://temp.uri/api/people", operationInputJsonLd); const result = await this.hypermediaProcessor.process(this.response, this.client); - this.addPerson = result.hypermedia.operations.withTemplate().first(); + this.addPerson = result.operations.withTemplate().first(); }) ); @@ -387,8 +290,8 @@ describe("Given instance of the JsonLdHypermediaProcessor class", () => { this.response = returnOk("http://temp.uri/api", collectionsInputJsonLd); const result = await this.hypermediaProcessor.process(this.response, this.client); - this.people = result.hypermedia.collections.withMembersOfType(schema.Person).first(); - this.known = result.hypermedia.collections.withMembersInRelationWith(api.people.karol, schema.knows).first(); + this.people = result.collections.withMembersOfType(schema.Person).first(); + this.known = result.collections.withMembersInRelationWith(api.people.karol, schema.knows).first(); }) ); diff --git a/tests/JsonLd/linksExtractor.spec.ts b/tests/JsonLd/linksExtractor.spec.ts index b3e74e4b..34f0c010 100644 --- a/tests/JsonLd/linksExtractor.spec.ts +++ b/tests/JsonLd/linksExtractor.spec.ts @@ -7,15 +7,19 @@ import HydraResourceMatcher from "../../testing/HydraResourceMatcher"; const baseUrl = "http://temp.uri/api/resource"; -function linkOf(predicate: string, resource: any): object { - resource.baseUrl = baseUrl; - resource.collections = []; - resource.links = []; - resource.operations = []; - resource.supportedOperations = []; - resource.relation = predicate.indexOf("http:") === -1 ? `http://temp.uri/vocab#${predicate}` : predicate; - resource.target = { iri: resource.iri, type: [] }; - return resource; +function linkOf(predicate: string, resource: string = null): object { + const iri = predicate.indexOf("http:") === -1 ? `http://temp.uri/vocab#${predicate}` : predicate; + return { + baseUrl, + collections: [], + iri, + links: [], + operations: [], + relation: iri, + supportedOperations: [], + target: resource != null ? { iri: resource, type: [] } : null, + type: [resource !== null ? hydra.Link : hydra.TemplatedLink] + }; } describe("Given a resources with relations", () => { @@ -34,12 +38,12 @@ describe("Given a resources with relations", () => { }; this.explicitLink = { "@id": "http://temp.uri/vocab#explicit_link", "@type": [hydra.Link] }; this.processingState.processedObject = this.resource = {}; - this.resource[hydra.freetextQuery] = [{ "@id": "some:hydra_link" }]; this.resource[this.explicitLink["@id"]] = [{ "@id": "some:explicit_link" }]; this.resource["http://temp.uri/vocab#same_root_link"] = [{ "@id": `${this.processingState.rootUrl}some_resource` }]; this.resource["http://temp.uri/vocab#http_link"] = [{ "@id": "http://other.uri/some_http_resource" }]; this.resource["http://temp.uri/vocab#ftp_link"] = [{ "@id": "ftp://temp.uri/some_ftp_resource" }]; this.resource["http://temp.uri/vocab#link"] = [{ "@id": "urn:name" }]; + this.resource[hydra.freetextQuery] = [{ "@id": "some:hydra_link" }]; this.graph = [this.resource, this.explicitLink]; }); @@ -50,8 +54,8 @@ describe("Given a resources with relations", () => { it("should provide only explicitly marked links in strict mode", () => { expect(linksExtractor(this.graph, this.processingState)).toBeLike([ - linkOf(this.explicitLink["@id"], { iri: "some:explicit_link", type: [hydra.Link] }), - linkOf(hydra.freetextQuery, { iri: "some:hydra_link", type: [hydra.Link] }) + linkOf(this.explicitLink["@id"], "some:explicit_link"), + linkOf(hydra.freetextQuery) ]); }); }); @@ -63,9 +67,9 @@ describe("Given a resources with relations", () => { it("should provide only explicitly marked and same root url links", () => { expect(linksExtractor(this.graph, this.processingState)).toBeLike([ - linkOf(this.explicitLink["@id"], { iri: "some:explicit_link", type: [hydra.Link] }), - linkOf("same_root_link", { iri: `${this.processingState.rootUrl}some_resource`, type: [hydra.Link] }), - linkOf(hydra.freetextQuery, { iri: "some:hydra_link", type: [hydra.Link] }) + linkOf(this.explicitLink["@id"], "some:explicit_link"), + linkOf("same_root_link", `${this.processingState.rootUrl}some_resource`), + linkOf(hydra.freetextQuery) ]); }); }); @@ -77,27 +81,27 @@ describe("Given a resources with relations", () => { it("should provide only explicitly marked links, same root urls and all HTTP links", () => { expect(linksExtractor(this.graph, this.processingState)).toBeLike([ - linkOf(this.explicitLink["@id"], { iri: "some:explicit_link", type: [hydra.Link] }), - linkOf("same_root_link", { iri: `${this.processingState.rootUrl}some_resource`, type: [hydra.Link] }), - linkOf("http_link", { iri: "http://other.uri/some_http_resource", type: [hydra.Link] }), - linkOf(hydra.freetextQuery, { iri: "some:hydra_link", type: [hydra.Link] }) + linkOf(this.explicitLink["@id"], "some:explicit_link"), + linkOf("same_root_link", `${this.processingState.rootUrl}some_resource`), + linkOf("http_link", "http://other.uri/some_http_resource"), + linkOf(hydra.freetextQuery) ]); }); }); describe("when using all links policy", () => { beforeEach(() => { - this.processingState.linksPolicy = LinksPolicy.AllHttp; + this.processingState.linksPolicy = LinksPolicy.All; }); it("should provide all links", () => { expect(linksExtractor(this.graph, this.processingState)).toBeLike([ - linkOf(this.explicitLink["@id"], { iri: "some:explicit_link", type: [hydra.Link] }), - linkOf("same_root_link", { iri: `${this.processingState.rootUrl}some_resource`, type: [hydra.Link] }), - linkOf("http_link", { iri: "http://other.uri/some_http_resource", type: [hydra.Link] }), - linkOf("ftp_link", { iri: "ftp://temp.uri/some_ftp_resource", type: [hydra.Link] }), - linkOf("link", { iri: "urn:name", type: [hydra.Link] }), - linkOf(hydra.freetextQuery, { iri: "some:hydra_link", type: [hydra.Link] }) + linkOf(this.explicitLink["@id"], "some:explicit_link"), + linkOf("same_root_link", `${this.processingState.rootUrl}some_resource`), + linkOf("http_link", "http://other.uri/some_http_resource"), + linkOf("ftp_link", "ftp://temp.uri/some_ftp_resource"), + linkOf("link", "urn:name"), + linkOf(hydra.freetextQuery) ]); }); }); diff --git a/tests/JsonLd/operationInput.json b/tests/JsonLd/operationInput.json index 8cdd13f7..ef733419 100644 --- a/tests/JsonLd/operationInput.json +++ b/tests/JsonLd/operationInput.json @@ -11,6 +11,18 @@ "knows": { "@id": "schema:knows", "@type": "@id" + }, + "hydra:variableRepresentation": { + "@id": "hydra:variableRepresentation", + "@type": "@id" + }, + "hydra:property": { + "@id": "hydra:property", + "@type": "@id" + }, + "hydra:expects": { + "@id": "hydra:expects", + "@type": "@id" } }, "@graph": [ @@ -20,13 +32,13 @@ "vocab:addPerson": { "@type": "hydra:IriTemplate", "hydra:template": "http://temp.uri/api/people/{name}", - "variableRepresentation": "hydra:BasicRepresentation", - "mappings": [ + "hydra:variableRepresentation": "hydra:BasicRepresentation", + "hydra:mappings": [ { "@type": "hydra:IriTemplateMapping", - "variable": "name", - "property": "schema:name", - "required": true + "hydra:variable": "name", + "hydra:property": "schema:name", + "hydra:required": true } ] } @@ -35,7 +47,7 @@ "@id": "vocab:addPerson", "@type": "hydra:TemplatedLink", "hydra:supportedOperation": { - "@type": ["hydra:Operation", "schema:AddAction"], + "@type": [ "hydra:Operation", "schema:AddAction" ], "hydra:method": "POST", "hydra:expects": "schema:Person" } diff --git a/tests/N3/N3HypermediaProcessor.spec.ts b/tests/N3/N3HypermediaProcessor.spec.ts index b9292c7e..e772ab8e 100644 --- a/tests/N3/N3HypermediaProcessor.spec.ts +++ b/tests/N3/N3HypermediaProcessor.spec.ts @@ -10,7 +10,9 @@ describe("Given instance of N3HypermediaProcessor class", () => { beforeEach(() => { jasmine.addMatchers({ toBeLike: () => new HydraResourceMatcher() }); this.options = { originalUrl: "http://temp.uri/api" }; - this.resource = { hypermedia: {}, iri: this.options.originalUrl, type: TypesCollection.empty }; + this.resource = []; + this.resource.iri = this.options.originalUrl; + this.resource.type = TypesCollection.empty; this.jsonLdProcessor = { process: sinon.stub().returns(this.resource) }; this.processor = new N3HypermediaProcessor(this.jsonLdProcessor); this.response = returnOk(this.options.originalUrl, inputTurtle, { "Content-Type": "text/turtle" }); @@ -42,12 +44,12 @@ describe("Given instance of N3HypermediaProcessor class", () => { ); it("should provide statements set", () => { - expect(this.result.length).toBe(5); + expect((this.result as any).dataset().length).toBe(5); }); it("should provide hypermedia container", () => { - expect(this.result.hypermedia).not.toBeUndefined(); - expect(this.result.hypermedia).not.toBeNull(); + expect((this.result as any).dataset()).not.toBeUndefined(); + expect((this.result as any).dataset()).not.toBeNull(); }); it("should pass the client", () => { diff --git a/tests/PartialCollectionCrawler.spec.ts b/tests/PartialCollectionCrawler.spec.ts index 6380ca12..4b6ec61c 100644 --- a/tests/PartialCollectionCrawler.spec.ts +++ b/tests/PartialCollectionCrawler.spec.ts @@ -8,21 +8,21 @@ describe("Given instance of the PartialCollectionCrawler class", () => { beforeEach(() => { this.part = 2; this.iterator = { - firstPartIri: "page:1", - getFirstPart: sinon.stub().callsFake(() => [{ iri: `item:${(this.part = 1)}` }]), - getLastPart: sinon.stub().callsFake(() => [{ iri: `item:${(this.part = 4)}` }]), + getFirstPart: sinon.stub().callsFake(() => Promise.resolve([{ iri: "item:1" }])), + getLastPart: sinon.stub().callsFake(() => Promise.resolve([{ iri: "item:4" }])), getNextPart: sinon.stub(), - getPreviousPart: sinon.stub(), - lastPartIri: "page:4" + getPreviousPart: sinon.stub() }; const makePartIri = (direction: number) => { return (direction > 0 && this.part >= 4) || (direction < 0 && this.part <= 1) ? null : `page:${this.part + direction}`; }; - Object.defineProperty(this.iterator, "current", { get: () => `page:${this.part}` }); + Object.defineProperty(this.iterator, "firstPartIri", { get: () => `page:${(this.part = 1)}` }); + Object.defineProperty(this.iterator, "currentPartIri", { get: () => makePartIri(0) }); Object.defineProperty(this.iterator, "nextPartIri", { get: () => makePartIri(1) }); Object.defineProperty(this.iterator, "previousPartIri", { get: () => makePartIri(-1) }); + Object.defineProperty(this.iterator, "lastPartIri", { get: () => `page:${(this.part = 4)}` }); this.initialCollection = { getIterator: sinon.stub().returns(this.iterator), iri: "collection", @@ -37,9 +37,9 @@ describe("Given instance of the PartialCollectionCrawler class", () => { beforeEach(() => { this.iterator.getNextPart .onFirstCall() - .callsFake(() => [{ iri: `item:${(this.part = 3)}` }]) + .callsFake(() => Promise.resolve([{ iri: `item:${(this.part = 3)}` }])) .onSecondCall() - .callsFake(() => [{ iri: `item:${(this.part = 4)}` }]); + .callsFake(() => Promise.resolve([{ iri: `item:${(this.part = 4)}` }])); }); describe("through all members with loop", () => { @@ -111,9 +111,9 @@ describe("Given instance of the PartialCollectionCrawler class", () => { beforeEach(() => { this.iterator.getPreviousPart .onFirstCall() - .callsFake(() => [{ iri: `item:${(this.part = 1)}` }]) + .callsFake(() => Promise.resolve([{ iri: `item:${(this.part = 1)}` }])) .onSecondCall() - .callsFake(() => [{ iri: `item:${(this.part = 3)}` }]); + .callsFake(() => Promise.resolve([{ iri: `item:${(this.part = 3)}` }])); }); describe("through all members with loop", () => {