Skip to content
This repository has been archived by the owner on Nov 27, 2023. It is now read-only.

Commit

Permalink
feat: widen interceptor interface (#86)
Browse files Browse the repository at this point in the history
The more general requestInterceptor and decoder interfaces are transformed in the more general
interceptor interface which could modify request on call side and responses on answer side.
  • Loading branch information
KnisterPeter authored Nov 14, 2016
1 parent 9db09af commit 9a0da26
Show file tree
Hide file tree
Showing 3 changed files with 131 additions and 36 deletions.
48 changes: 46 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,13 @@
[![Commitizen friendly](https://img.shields.io/badge/commitizen-friendly-brightgreen.svg)](http://commitizen.github.io/cz-cli/)
[![Standard Version](https://img.shields.io/badge/release-standard%20version-brightgreen.svg)](https://github.com/conventional-changelog/standard-version)

A decorator based http webservice client build with typescript (inspired bei feign).
A decorator based http webservice client build with typescript (inspired bei [feign](https://github.com/OpenFeign/feign)).

## Features

* Handle REST based webservices
* Configure a decoder (defaults to JSON)
* Request interceptors
* Generic request/response interceptor chain
* Basic authentication
* Request parameters (currently on GET requests)

Expand Down Expand Up @@ -60,6 +60,9 @@ call();

```

Decoders, basicAuthentication and requestInterceptors are all special forms
of the more generic interceptors which could be chained per request/response.

```js
// Configure a text based decoder
const client = Pretend
Expand Down Expand Up @@ -88,6 +91,47 @@ call();
.target(Test, 'http://host:port/');
```

#### interceptors

Multiple interceptors could be added to each builder. The order of interceptor
calls will result in a chain of calls like illistrated below:

```js
// Configure a request interceptor
const client = Pretend
.builder()
.interceptor(async (chain, request) => {
console.log('interceptor 1: request');
const response = await chain(request);
console.log('interceptor 1: response');
return response;
})
.interceptor(async (chain, request) => {
console.log('interceptor 2: request');
const response = await chain(request);
console.log('interceptor 2: response');
return response;
})
.target(Test, 'http://host:port/');
```

```text
+---------------+ +---------------+
Request ---> | | -> | |
| Interceptor 1 | | Interceptor 2 | -> HTTP REST call
Response <-- | | <- | |
+---------------+ +---------------+
```

This leads to the following console output:

```text
interceptor 1: request
interceptor 2: request
interceptor 2: response
interceptor 1: response
```

## Future ideas / Roadmap

* Named parameters
99 changes: 65 additions & 34 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,19 @@ export type IPretendDecoder = (response: Response) => Promise<any>;
export type IPretendRequest = { url: string, options: RequestInit };
export type IPretendRequestInterceptor = (request: IPretendRequest) => IPretendRequest;

interface IPretendConfiguration {
baseUrl: string;
decoder: IPretendDecoder;
requestInterceptors: IPretendRequestInterceptor[];
export interface Interceptor {
(chain: Chain, request: IPretendRequest): Promise<any>;
}

export interface Chain {
(request: IPretendRequest): Promise<any>;
}

interface Instance {
__Pretend__: {
baseUrl: string;
interceptors: Interceptor[];
};
}

function createUrl(url: string, args: any[]): [string, number] {
Expand All @@ -35,47 +44,67 @@ function buildUrl(tmpl: string, args: any[], appendQuery: boolean): [string, num
return [`${url}${query}`, queryOrBodyIndex];
}

async function execute(config: IPretendConfiguration, method: string, tmpl: string, args: any[], sendBody: boolean,
function chainFactory(interceptors: Interceptor[]): (request: IPretendRequest) => Promise<Response> {
let i = 0;
return function chainStep(request: IPretendRequest): Promise<Response> {
return interceptors[i++](chainStep, request);
};
}

function execute(instance: Instance, method: string, tmpl: string, args: any[], sendBody: boolean,
appendQuery: boolean): Promise<any> {
const createUrlResult = buildUrl(tmpl, args, appendQuery);
const url = createUrlResult[0];
const queryOrBodyIndex = createUrlResult[1];
const request = config.requestInterceptors
.reduce<IPretendRequest>((data, interceptor) => interceptor(data), {
url,
options: {
method,
headers: {},
body: sendBody ? JSON.stringify(args[appendQuery ? queryOrBodyIndex + 1 : queryOrBodyIndex]) : undefined
}
});
const response = await fetch(request.url, request.options);
return config.decoder(response);
const body = sendBody ? JSON.stringify(args[appendQuery ? queryOrBodyIndex + 1 : queryOrBodyIndex]) : undefined;

const chain = chainFactory(instance.__Pretend__.interceptors);
return chain({
url,
options: {
method,
headers: {},
body
}
});
}

function decoratorFactory(method: string, url: string, sendBody: boolean, appendQuery: boolean): MethodDecorator {
return (target: Object, propertyKey: string, descriptor: TypedPropertyDescriptor<any>) => {
descriptor.value = async function(): Promise<any> {
const config = this.__Pretend__ as IPretendConfiguration;
return execute(config, method, `${config.baseUrl}${url}`, Array.prototype.slice.call(arguments), sendBody,
appendQuery);
descriptor.value = async function(this: Instance): Promise<any> {
return execute(this, method, `${this.__Pretend__.baseUrl}${url}`,
Array.prototype.slice.call(arguments), sendBody, appendQuery);
};
return descriptor;
};
}

export class Pretend {

private requestInterceptors: IPretendRequestInterceptor[] = [];
private interceptors: Interceptor[] = [];
private decoder: IPretendDecoder = Pretend.JsonDecoder;

private static FetchInterceptor: Interceptor =
async (chain: Chain, request: IPretendRequest) => fetch(request.url, request.options);
public static JsonDecoder: IPretendDecoder = (response: Response) => response.json();
public static TextDecoder: IPretendDecoder = (response: Response) => response.text();

public static builder(): Pretend {
return new Pretend();
}

public interceptor(interceptor: Interceptor): this {
this.interceptors.push(interceptor);
return this;
}

public requestInterceptor(requestInterceptor: IPretendRequestInterceptor): this {
this.interceptors.push((chain: Chain, request: IPretendRequest) => {
return chain(requestInterceptor(request));
});
return this;
}

public basicAuthentication(username: string, password: string): this {
const usernameAndPassword = `${username}:${password}`;
const auth = 'Basic '
Expand All @@ -89,25 +118,27 @@ export class Pretend {
return this;
}

public requestInterceptor(requestInterceptor: IPretendRequestInterceptor): this {
this.requestInterceptors.push(requestInterceptor);
return this;
}

public decode(decoder: IPretendDecoder): this {
this.decoder = decoder;
return this;
}

public target<T>(descriptor: {new(): T}, baseUrl: string): T {
const instance = new descriptor();
(instance as any).__Pretend__ = {
baseUrl: baseUrl.endsWith('/')
? baseUrl.substring(0, baseUrl.length - 1)
: baseUrl,
decoder: this.decoder,
requestInterceptors: this.requestInterceptors
} as IPretendConfiguration;
if (this.decoder) {
// If we have a decoder, the first thing to do with a response is to decode it
this.interceptors.push(async (chain: Chain, request: IPretendRequest) => {
const response = await chain(request);
return this.decoder(response);
});
}
// This is the end of the request chain
this.interceptors.push(Pretend.FetchInterceptor);

const instance = new descriptor() as T & Instance;
instance.__Pretend__ = {
baseUrl: baseUrl.endsWith('/') ? baseUrl.substring(0, baseUrl.length - 1) : baseUrl,
interceptors: this.interceptors
};
return instance;
}

Expand Down
20 changes: 20 additions & 0 deletions test/index-test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -111,3 +111,23 @@ test('Pretend should use basic auth if configured', async t => {

t.deepEqual(repsponse, {});
});

test('Pretend should return from the interceptor', async t => {
nock('http://host:port/')
.get('/path/id').reply(200, response)
.get('/path/id').reply(500, {});

let firstReponse: any = undefined;
const test = Pretend.builder()
.interceptor((chain, request) => {
if (!firstReponse) {
firstReponse = chain(request);
}
return firstReponse;
})
.target(Test, 'http://host:port/');
// First call gets through
await test.get('id');
// Second should be return from the interceptor (nock would fail)
t.deepEqual(await test.get('id'), response);
});

0 comments on commit 9a0da26

Please sign in to comment.