Skip to content

Commit

Permalink
Merge pull request #160 from evert/refactor-representor-mapping
Browse files Browse the repository at this point in the history
Refactor representor mapping
  • Loading branch information
evert authored Nov 2, 2019
2 parents 24e4937 + 49dc2bd commit 57680e3
Show file tree
Hide file tree
Showing 6 changed files with 328 additions and 204 deletions.
94 changes: 8 additions & 86 deletions src/ketting.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,8 @@
import * as LinkHeader from 'http-link-header';
import { FollowerOne } from './follower';
import { LinkSet } from './link';
import Representor from './representor/base';
import HalRepresentor from './representor/hal';
import HtmlRepresentor from './representor/html';
import JsonApiRepresentor from './representor/jsonapi';
import SirenRepresentor from './representor/siren';
import RepresentorHelper from './representor/helper';
import Resource from './resource';
import { ContentType, KettingInit, LinkVariables } from './types';
import { KettingInit, LinkVariables } from './types';
import FetchHelper from './utils/fetch-helper';
import './utils/fetch-polyfill';
import { isSafeMethod } from './utils/http';
Expand All @@ -32,55 +27,26 @@ export default class Ketting {
*/
resourceCache: { [url: string]: Resource };

/**
* Content-Type settings and mappings.
*
* See the constructor for an example of the structure.
*/
contentTypes: ContentType[];
representorHelper: RepresentorHelper;

/**
* The helper class that calls fetch() for us
*/
private fetchHelper: FetchHelper;

constructor(bookMark: string, options?: Partial<KettingInit>) {
constructor(bookMark: string, options?: KettingInit) {

if (typeof options === 'undefined') {
options = {};
}

this.resourceCache = {};

this.contentTypes = [
{
mime: 'application/hal+json',
representor: 'hal',
q: '1.0',
},
{
mime: 'application/vnd.api+json',
representor: 'jsonapi',
q: '0.9',
},
{
mime: 'application/vnd.siren+json',
representor: 'siren',
q: '0.9',
},
{
mime: 'application/json',
representor: 'hal',
q: '0.8',
},
{
mime: 'text/html',
representor: 'html',
q: '0.7',
}
];

this.bookMark = bookMark;

this.representorHelper = new RepresentorHelper(
options.contentTypes || [],
);
this.fetchHelper = new FetchHelper(options, this.beforeRequest.bind(this), this.afterRequest.bind(this));

}
Expand Down Expand Up @@ -153,50 +119,6 @@ export default class Ketting {

}

createRepresentation(uri: string, contentType: string, body: string | null, headerLinks: LinkSet): Representor<any> {

if (contentType.indexOf(';') !== -1) {
contentType = contentType.split(';')[0];
}
contentType = contentType.trim();
const result = this.contentTypes.find(item => {
return item.mime === contentType;
});

if (!result) {
throw new Error('Could not find a representor for contentType: ' + contentType);
}

switch (result.representor) {
case 'html' :
return new HtmlRepresentor(uri, contentType, body, headerLinks);
case 'hal' :
return new HalRepresentor(uri, contentType, body, headerLinks);
case 'jsonapi' :
return new JsonApiRepresentor(uri, contentType, body, headerLinks);
case 'siren' :
return new SirenRepresentor(uri, contentType, body, headerLinks);
default :
throw new Error('Unknown representor: ' + result.representor);

}

}

/**
* Generates an accept header string, based on registered Resource Types.
*/
getAcceptHeader(): string {

return this.contentTypes
.map( contentType => {
let item = contentType.mime;
if (contentType.q) { item += ';q=' + contentType.q; }
return item;
} )
.join(', ');

}

beforeRequest(request: Request): void {

Expand Down
140 changes: 140 additions & 0 deletions src/representor/helper.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
import * as LinkHeader from 'http-link-header';
import { Link, LinkSet } from '../link';
import { ContentType } from '../types';
import Representor from './base';
import HalRepresentor from './hal';
import HtmlRepresentor from './html';
import JsonApiRepresentor from './jsonapi';
import SirenRepresentor from './siren';

export default class RepresentorHelper {

private contentTypes: ContentType[];

constructor(contentTypes: ContentType[]) {

const defaultTypes: ContentType[] = [
{
mime: 'application/hal+json',
representor: 'hal',
q: '1.0',
},
{
mime: 'application/vnd.api+json',
representor: 'jsonapi',
q: '0.9',
},
{
mime: 'application/vnd.siren+json',
representor: 'siren',
q: '0.9',
},
{
mime: 'application/json',
representor: 'hal',
q: '0.8',
},
{
mime: 'text/html',
representor: 'html',
q: '0.7',
}

];
this.contentTypes = defaultTypes.concat(contentTypes);

}

/**
* Generates an accept header string, based on registered Resource Types.
*/
getAcceptHeader(): string {

return this.contentTypes
.map( contentType => {
let item = contentType.mime;
if (contentType.q) { item += ';q=' + contentType.q; }
return item;
} )
.join(', ');

}

getMimeTypes(): string[] {

return this.contentTypes.map( contentType => contentType.mime );

}

create(uri: string, contentType: string, body: string | null, headerLinks: LinkSet): Representor<any> {

const type = this.getRepresentorType(contentType);

switch (type) {
case 'html' :
return new HtmlRepresentor(uri, contentType, body, headerLinks);
case 'hal' :
return new HalRepresentor(uri, contentType, body, headerLinks);
case 'jsonapi' :
return new JsonApiRepresentor(uri, contentType, body, headerLinks);
case 'siren' :
return new SirenRepresentor(uri, contentType, body, headerLinks);
default :
throw new Error('Unknown representor: ' + type);
}

}

/**
* Returns a representor object from a Fetch Response.
*/
createFromResponse(uri: string, response: Response, body: string): Representor<any> {

const contentType = response.headers.get('Content-Type')!;

const httpLinkHeader = response.headers.get('Link');
const headerLinks: LinkSet = new Map();

if (httpLinkHeader) {

for (const httpLink of LinkHeader.parse(httpLinkHeader).refs) {
// Looping through individual links
for (const rel of httpLink.rel.split(' ')) {
// Looping through space separated rel values.
const newLink = new Link({
rel: rel,
context: uri,
href: httpLink.uri
});
if (headerLinks.has(rel)) {
headerLinks.get(rel)!.push(newLink);
} else {
headerLinks.set(rel, [newLink]);
}
}
}
}

return this.create(uri, contentType, body, headerLinks);

}

private getRepresentorType(contentType: string) {

if (contentType.indexOf(';') !== -1) {
contentType = contentType.split(';')[0];
}
contentType = contentType.trim();
const result = this.contentTypes.find(item => {
return item.mime === contentType;
});

if (!result) {
throw new Error('Could not find a representor for contentType: ' + contentType);
}

return result.representor;
}


}
51 changes: 10 additions & 41 deletions src/resource.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
import * as LinkHeader from 'http-link-header';
import { FollowerMany, FollowerOne } from './follower';
import problemFactory from './http-error';
import Ketting from './ketting';
import { Link, LinkSet } from './link';
import { Link } from './link';
import Representator from './representor/base';
import { LinkVariables } from './types';
import { mergeHeaders } from './utils/fetch-helper';
Expand Down Expand Up @@ -83,13 +82,13 @@ export default class Resource<TResource = any, TPatch = Partial<TResource>> {
*/
async put(body: TResource): Promise<void> {

const contentType = this.contentType || this.client.contentTypes[0].mime;
const contentType = this.contentType || this.client.representorHelper.getMimeTypes()[0];
const params = {
method: 'PUT',
body: JSON.stringify(body),
headers: {
'Content-Type': contentType,
'Accept' : this.contentType ? this.contentType : this.client.getAcceptHeader()
'Accept' : this.contentType ? this.contentType : this.client.representorHelper.getAcceptHeader()
},
};
await this.fetchAndThrow(params);
Expand Down Expand Up @@ -122,7 +121,7 @@ export default class Resource<TResource = any, TPatch = Partial<TResource>> {
post<TPostResource>(body: any): Promise<Resource<TPostResource>>;
async post(body: any): Promise<Resource | null> {

const contentType = this.contentType || this.client.contentTypes[0].mime;
const contentType = this.contentType || this.client.representorHelper.getMimeTypes()[0];
const response = await this.fetchAndThrow(
{
method: 'POST',
Expand Down Expand Up @@ -182,7 +181,7 @@ export default class Resource<TResource = any, TPatch = Partial<TResource>> {
if (!this.inFlightRefresh) {

const headers: { [name: string]: string } = {
Accept: this.contentType ? this.contentType : this.client.getAcceptHeader()
Accept: this.contentType ? this.contentType : this.client.representorHelper.getAcceptHeader()
};

if (this.preferPushRels.size > 0) {
Expand Down Expand Up @@ -214,51 +213,21 @@ export default class Resource<TResource = any, TPatch = Partial<TResource>> {

}

const contentType = response!.headers.get('Content-Type');
if (!contentType) {
throw new Error('Server did not respond with a Content-Type header');
}

// Extracting HTTP Link header.
const httpLinkHeader = response!.headers.get('Link');

const headerLinks: LinkSet = new Map();

if (httpLinkHeader) {

for (const httpLink of LinkHeader.parse(httpLinkHeader).refs) {
// Looping through individual links
for (const rel of httpLink.rel.split(' ')) {
// Looping through space separated rel values.
const newLink = new Link({
rel: rel,
context: this.uri,
href: httpLink.uri
});
if (headerLinks.has(rel)) {
headerLinks.get(rel)!.push(newLink);
} else {
headerLinks.set(rel, [newLink]);
}
}
}
}
this.repr = this.client.createRepresentation(
this.repr = this.client.representorHelper.createFromResponse(
this.uri,
contentType,
response!,
body!,
headerLinks
) as any as Representator<TResource>;

if (!this.contentType) {
this.contentType = contentType;
this.contentType = this.repr.contentType;
}

for (const [subUri, subBody] of Object.entries(this.repr.getEmbedded())) {
const subResource = this.go(subUri);
subResource.repr = this.client.createRepresentation(
subResource.repr = this.client.representorHelper.create(
subUri,
contentType,
this.repr.contentType,
null,
new Map(),
);
Expand Down
Loading

0 comments on commit 57680e3

Please sign in to comment.