Skip to content

Commit

Permalink
feat(ama-sdk): create Mock Intercept angular plugin (AmadeusITGroup#2079
Browse files Browse the repository at this point in the history
)

## Proposed change

- Support for Angular ApiClient plugins
- Mock Intercept angular plugin
  • Loading branch information
kpanot authored Sep 11, 2024
2 parents f7b5443 + b80eeb8 commit c151652
Show file tree
Hide file tree
Showing 7 changed files with 170 additions and 4 deletions.
11 changes: 11 additions & 0 deletions packages/@ama-sdk/core/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,17 @@
"node": "./dist/cjs/clients/*.js",
"require": "./dist/cjs/clients/*.js"
},
"./plugins/mock-intercept/angular": {
"module": "./dist/src/plugins/mock-intercept/mock-intercept.angular.js",
"esm2020": "./dist/src/plugins/mock-intercept/mock-intercept.angular.js",
"esm2015": "./dist/esm2015/plugins/mock-intercept/mock-intercept.angular.js",
"es2020": "./dist/cjs/plugins/mock-intercept/mock-intercept.angular.js",
"default": "./dist/cjs/plugins/mock-intercept/mock-intercept.angular.js",
"typings": "./dist/src/plugins/mock-intercept/mock-intercept.angular.d.ts",
"import": "./dist/src/plugins/mock-intercept/mock-intercept.angular.js",
"node": "./dist/cjs/plugins/mock-intercept/mock-intercept.angular.js",
"require": "./dist/cjs/plugins/mock-intercept/mock-intercept.angular.js"
},
"./plugins/*": {
"module": "./dist/src/plugins/*/index.js",
"esm2020": "./dist/src/plugins/*/index.js",
Expand Down
29 changes: 26 additions & 3 deletions packages/@ama-sdk/core/src/clients/api-angular-client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,14 @@ import type { ApiClient,RequestOptionsParameters } from '../fwk/core/api-client'
import { BaseApiClientOptions } from '../fwk/core/base-api-constructor';
import { EmptyResponseError } from '../fwk/errors';
import { ReviverType } from '../fwk/Reviver';
import type { AngularCall, AngularPlugin, PluginObservableRunner } from '../plugins/core/angular-plugin';

/** @see BaseApiClientOptions */
export interface BaseApiAngularClientOptions extends BaseApiClientOptions {
/** Angular HTTP Client */
httpClient: HttpClient;
/** List of plugins to apply to the Angular Http call */
angularPlugins: AngularPlugin[];
}

/** @see BaseApiConstructor */
Expand All @@ -21,7 +25,7 @@ export interface BaseApiAngularClientConstructor extends PartialExcept<BaseApiAn

const DEFAULT_OPTIONS: Omit<BaseApiAngularClientOptions, 'basePath' | 'httpClient'> = {
replyPlugins: [new ReviverReply(), new ExceptionReply()],
// AngularPlugins: [],
angularPlugins: [],
requestPlugins: [],
enableTokenization: false,
disableFallback: false
Expand Down Expand Up @@ -104,11 +108,30 @@ export class ApiAngularClient implements ApiClient {
let data: HttpResponse<any>;
const metadataSignal = options.metadata?.signal;
metadataSignal?.throwIfAborted();
const subscription = this.options.httpClient.request(options.method, url, {

const loadedPlugins: (PluginObservableRunner<HttpResponse<any>, AngularCall>)[] = [];
if (this.options.angularPlugins) {
loadedPlugins.push(...this.options.angularPlugins.map((plugin) => plugin.load({
angularPlugins: loadedPlugins,
apiClient: this,
url,
apiName,
requestOptions: options,
logger: this.options.logger
})));
}

let httpRequest = this.options.httpClient.request(options.method, url, {
...options,
observe: 'response',
headers
}).subscribe({
});

for (const plugin of loadedPlugins) {
httpRequest = plugin.transform(httpRequest);
}

const subscription = httpRequest.subscribe({
next: (res) => data = res,
error: (err) => reject(err),
complete: () => resolve(data)
Expand Down
10 changes: 9 additions & 1 deletion packages/@ama-sdk/core/src/clients/api-fetch-client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -117,7 +117,15 @@ export class ApiFetchClient implements ApiClient {

const loadedPlugins: (PluginAsyncRunner<Response, FetchCall> & PluginAsyncStarter)[] = [];
if (this.options.fetchPlugins) {
loadedPlugins.push(...this.options.fetchPlugins.map((plugin) => plugin.load({url, options, fetchPlugins: loadedPlugins, controller, apiClient: this, logger: this.options.logger})));
loadedPlugins.push(...this.options.fetchPlugins.map((plugin) => plugin.load({
url,
options,
fetchPlugins: loadedPlugins,
controller,
apiClient: this,
logger: this.options.logger,
apiName
})));
}

const canStart = await Promise.all(loadedPlugins.map((plugin) => !plugin.canStart || plugin.canStart()));
Expand Down
46 changes: 46 additions & 0 deletions packages/@ama-sdk/core/src/plugins/core/angular-plugin.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import type { Observable } from 'rxjs';
import type { HttpResponse } from '@angular/common/http';
import type { ApiClient } from '../../fwk/core/api-client';
import type { PluginContext } from './plugin';
import type { RequestOptions } from './request-plugin';

/**
* Interface of an async runnable plugin
*/
export interface PluginObservableRunner<T, V> {
/** Transformation function */
transform(data: V): Observable<T>;
}

/** Angular HTTP Call Response type */
export type AngularCall = Observable<HttpResponse<any>>;

/**
* Interface of an SDK reply plugin.
* The plugin will be run on the reply of a call
*/
export interface AngularPluginContext extends PluginContext {
/** URL targeted */
url: string;

/** List of loaded plugins apply to the Angular HTTP call */
angularPlugins: PluginObservableRunner<HttpResponse<any>, AngularCall>[];

/** Api Client processing the call the the API */
apiClient: ApiClient;

/** Angular call options */
requestOptions: RequestOptions;
}

/**
* Interface of a Angular plugin.
* The plugin will be run around the Angular Http call
*/
export interface AngularPlugin {
/**
* Load the plugin with the context
* @param context Context of Angular plugin
*/
load(context: AngularPluginContext): PluginObservableRunner<HttpResponse<any>, AngularCall>;
}
1 change: 1 addition & 0 deletions packages/@ama-sdk/core/src/plugins/core/fetch-plugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import type { ApiClient } from '../../fwk/core/api-client';
import type { Plugin, PluginAsyncRunner, PluginContext } from './plugin';
import type { RequestOptions } from './request-plugin';

/** Fetch Call Response type */
export type FetchCall = Promise<Response>;

/**
Expand Down
1 change: 1 addition & 0 deletions packages/@ama-sdk/core/src/plugins/mock-intercept/index.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
export * from './mock-intercept.request';
export * from './mock-intercept.fetch';
export * from './mock-intercept.interface';
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
import { delay, from, mergeMap } from 'rxjs';
import type { AngularCall, AngularPlugin, AngularPluginContext, PluginObservableRunner } from '../core/angular-plugin';
import { CUSTOM_MOCK_OPERATION_ID_HEADER, MockInterceptFetchParameters } from './mock-intercept.interface';
import { MockInterceptRequest } from './mock-intercept.request';
import { HttpResponse } from '@angular/common/http';

/**
* Plugin to mock and intercept the call of SDK
*
* This plugin should be used only with the MockInterceptRequest Plugin.
* It will allow the user to delay the response or to handle the getResponse function provided with the mock (if present).
*/
export class MockInterceptAngular implements AngularPlugin {

constructor(protected options: MockInterceptFetchParameters) {}

public load(context: AngularPluginContext): PluginObservableRunner<HttpResponse<any>, AngularCall> {

if (!context.apiClient.options.requestPlugins.some((plugin) => plugin instanceof MockInterceptRequest)) {
throw new Error('MockInterceptAngular plugin should be used only with the MockInterceptRequest plugin');
}

return {
transform: (call: AngularCall) => {
return from((
async () => {
await this.options.adapter.initialize();

let originalCall = call;
// eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion
if (!context.options.headers || !(context.options.headers instanceof Headers) || !(context.options.headers as Headers).has(CUSTOM_MOCK_OPERATION_ID_HEADER)) {
return originalCall;
}

if (typeof this.options.delayTiming !== 'undefined') {
const delayTime = typeof this.options.delayTiming === 'number' ? this.options.delayTiming : await this.options.delayTiming({
...context,
fetchPlugins: [],
options: context.requestOptions
});
originalCall = originalCall.pipe(delay(delayTime));
}

// eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion
const operationId = (context.options.headers as Headers).get(CUSTOM_MOCK_OPERATION_ID_HEADER)!;
try {
const mock = this.options.adapter.getLatestMock(operationId);

if (!mock.getResponse) {
return originalCall;
}

const response = mock.getResponse();
return originalCall.pipe(
mergeMap(async (res) => {
const body = await response.json();
const responseCloned = res.clone();
return new HttpResponse<any>({
...responseCloned,
body,
url: responseCloned.url || undefined
});
})
);

} catch {
(context.logger || console).error(`Failed to retrieve the latest mock for Operation ID ${operationId}, fallback to default mock`);
return originalCall;
}
})()
).pipe(mergeMap((res) => res));
}
};
}

}

0 comments on commit c151652

Please sign in to comment.