Skip to content

Commit 7b66a7a

Browse files
committed
Extract generic interfaces for API models, separate from OpenAPI
1 parent 2c81336 commit 7b66a7a

File tree

11 files changed

+137
-63
lines changed

11 files changed

+137
-63
lines changed

src/components/view/http/http-api-card.tsx

+2-2
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import { styled } from '../../../styles';
66
import { Icon } from '../../../icons';
77
import { joinAnd } from '../../../util';
88

9-
import { ApiExchange, Parameter } from '../../../model/api/openapi';
9+
import { ApiExchange, ApiParameter } from '../../../model/api/api-interfaces';
1010

1111
import {
1212
CollapsibleCardHeading,
@@ -63,7 +63,7 @@ const UnsetValue = styled.span`
6363
`;
6464

6565
const ParamMetadata = styled((p: {
66-
param: Parameter,
66+
param: ApiParameter,
6767
className?: string
6868
}) => <div className={p.className}>
6969
{

src/components/view/http/http-details-pane.tsx

+1-1
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ import { UiStore } from '../../../model/ui-store';
1212
import { RulesStore } from '../../../model/rules/rules-store';
1313
import { AccountStore } from '../../../model/account/account-store';
1414
import { getStatusColor } from '../../../model/events/categorization';
15-
import { ApiExchange } from '../../../model/api/openapi';
15+
import { ApiExchange } from '../../../model/api/api-interfaces';
1616
import { buildRuleFromRequest } from '../../../model/rules/rule-definitions';
1717
import { WebSocketStream } from '../../../model/websockets/websocket-stream';
1818

src/components/view/http/http-response-card.tsx

+1-1
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import { get } from 'typesafe-get';
66
import { HtkResponse, Omit } from '../../../types';
77
import { Theme } from '../../../styles';
88

9-
import { ApiExchange } from '../../../model/api/openapi';
9+
import { ApiExchange } from '../../../model/api/api-interfaces';
1010
import { getStatusColor } from '../../../model/events/categorization';
1111
import { getStatusDocs, getStatusMessage } from '../../../model/http/http-docs';
1212

src/model/api/api-interfaces.ts

+72
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
import type * as querystring from 'querystring';
2+
import type {
3+
SchemaObject
4+
} from 'openapi-directory';
5+
6+
import type {
7+
HtkResponse,
8+
Html
9+
} from "../../types";
10+
import { OpenApiMetadata } from './build-openapi';
11+
12+
export type ApiMetadata =
13+
| OpenApiMetadata;
14+
15+
export interface ApiRequestMatcher {
16+
pathMatcher: RegExp;
17+
queryMatcher: querystring.ParsedUrlQuery;
18+
}
19+
20+
export interface ApiExchange {
21+
readonly service: ApiService;
22+
readonly operation: ApiOperation;
23+
readonly request: ApiRequest;
24+
25+
readonly response: ApiResponse | undefined;
26+
27+
updateWithResponse(response: HtkResponse | 'aborted' | undefined): void;
28+
29+
matchedOperation(): boolean;
30+
}
31+
32+
export interface ApiService {
33+
readonly name: string;
34+
readonly logoUrl?: string;
35+
readonly description?: Html;
36+
readonly docsUrl?: string;
37+
}
38+
39+
export interface ApiOperation {
40+
readonly name: string;
41+
readonly description?: Html;
42+
readonly docsUrl?: string;
43+
44+
readonly warnings: string[];
45+
}
46+
47+
export interface ApiRequest {
48+
parameters: ApiParameter[];
49+
bodySchema?: SchemaObject;
50+
}
51+
52+
export interface ApiParameter {
53+
name: string;
54+
description?: Html;
55+
value?: unknown;
56+
defaultValue?: unknown;
57+
enum?: unknown[];
58+
type?: string;
59+
in:
60+
| 'cookie'
61+
| 'path'
62+
| 'header'
63+
| 'query';
64+
required: boolean;
65+
deprecated: boolean;
66+
warnings: string[];
67+
}
68+
69+
export interface ApiResponse {
70+
description?: Html;
71+
bodySchema?: SchemaObject;
72+
}

src/model/api/api-store.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import { lazyObservablePromise } from "../../util/observable";
1010
import { hydrate, persist } from "../../util/mobx-persist/persist";
1111

1212
import { AccountStore } from "../account/account-store";
13-
import { ApiMetadata } from "./build-openapi";
13+
import { ApiMetadata } from "./api-interfaces";
1414
import { buildApiMetadataAsync } from '../../services/ui-worker-api';
1515
import { findBestMatchingApi } from './openapi';
1616
import { serializeRegex, serializeMap } from '../serialization';

src/model/api/build-openapi.ts

+12-13
Original file line numberDiff line numberDiff line change
@@ -4,23 +4,21 @@ import * as querystring from 'querystring';
44
import { OpenAPIObject, PathItemObject } from 'openapi-directory';
55
import * as Ajv from 'ajv';
66

7+
import type { ApiRequestMatcher } from './api-interfaces';
78
import { openApiSchema } from './openapi-schema';
89
import { dereference } from '../../util/json-schema';
910

10-
interface Path {
11-
path: string;
12-
pathSpec: PathItemObject;
13-
}
14-
15-
interface RequestMatcher {
16-
pathMatcher: RegExp;
17-
queryMatcher: querystring.ParsedUrlQuery;
18-
}
1911

20-
export interface ApiMetadata {
12+
export interface OpenApiMetadata {
13+
type: 'openapi';
2114
spec: OpenAPIObject;
2215
serverMatcher: RegExp;
23-
requestMatchers: Map<RequestMatcher, Path>;
16+
requestMatchers: Map<ApiRequestMatcher, Path>;
17+
}
18+
19+
interface Path {
20+
path: string;
21+
pathSpec: PathItemObject;
2422
}
2523

2624
const filterSpec = new Ajv({
@@ -40,7 +38,7 @@ function templateStringToRegexString(template: string): string {
4038
export async function buildApiMetadata(
4139
spec: OpenAPIObject,
4240
baseUrlOverrides?: string[]
43-
): Promise<ApiMetadata> {
41+
): Promise<OpenApiMetadata> {
4442
const specId = `${
4543
spec.info['x-providerName'] || 'unknown'
4644
}/${
@@ -71,7 +69,7 @@ export async function buildApiMetadata(
7169
// Build a regex that matches any of these at the start of a URL
7270
const serverMatcher = new RegExp(`^(${serverUrlRegexSources.join('|')})`, 'i')
7371

74-
const requestMatchers = new Map<RequestMatcher, Path>();
72+
const requestMatchers = new Map<ApiRequestMatcher, Path>();
7573
_.entries(spec.paths)
7674
// Sort path & pathspec pairs to ensure that more specific paths are
7775
// always listed first, so that later on we can always use the first match
@@ -129,6 +127,7 @@ export async function buildApiMetadata(
129127
});
130128

131129
return {
130+
type: 'openapi',
132131
spec,
133132
serverMatcher,
134133
requestMatchers

src/model/api/openapi.ts

+30-33
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,7 @@ import type {
88
ParameterObject,
99
ResponseObject,
1010
RequestBodyObject,
11-
SchemaObject,
12-
ParameterLocation
11+
SchemaObject
1312
} from 'openapi-directory';
1413
import * as Ajv from 'ajv';
1514

@@ -24,7 +23,15 @@ import { firstMatch, empty, lastHeader } from '../../util';
2423
import { formatAjvError } from '../../util/json-schema';
2524
import { reportError } from '../../errors';
2625

27-
import { ApiMetadata } from './build-openapi';
26+
import {
27+
ApiExchange,
28+
ApiService,
29+
ApiOperation,
30+
ApiRequest,
31+
ApiResponse,
32+
ApiParameter
33+
} from './api-interfaces';
34+
import { OpenApiMetadata } from './build-openapi';
2835
import { fromMarkdown } from '../markdown';
2936

3037
const paramValidator = new Ajv({
@@ -35,7 +42,10 @@ const paramValidator = new Ajv({
3542
// If we match multiple APIs, build all of them, try to match each one
3643
// against the request, and return only the matching one. This happens if
3744
// multiple services have the exact same base request URL (rds.amazonaws.com)
38-
export function findBestMatchingApi(apis: ApiMetadata[], request: HtkRequest): ApiMetadata | undefined {
45+
export function findBestMatchingApi(
46+
apis: OpenApiMetadata[],
47+
request: HtkRequest
48+
): OpenApiMetadata | undefined {
3949
const matchingApis = apis.filter((api) => matchOperation(api, request).matched);
4050

4151
// If we've successfully found one matching API, return it
@@ -56,7 +66,7 @@ export function findBestMatchingApi(apis: ApiMetadata[], request: HtkRequest): A
5666
return _.maxBy(matchingApis, a => a.spec.paths.length)!;
5767
}
5868

59-
function getPath(api: ApiMetadata, request: HtkRequest): {
69+
function getPath(api: OpenApiMetadata, request: HtkRequest): {
6070
pathSpec: PathObject,
6171
path: string
6272
} | undefined {
@@ -94,7 +104,7 @@ export function getParameters(
94104
path: string,
95105
parameters: ParameterObject[],
96106
request: HtkRequest
97-
): Parameter[] {
107+
): ApiParameter[] {
98108
if (!parameters) return [];
99109

100110
const query = request.parsedUrl.searchParams;
@@ -270,7 +280,7 @@ export function getBodySchema(
270280
);
271281
}
272282

273-
function getDummyPath(api: ApiMetadata, request: HtkRequest): string {
283+
function getDummyPath(api: OpenApiMetadata, request: HtkRequest): string {
274284
const { parsedUrl } = request;
275285
const url = `${parsedUrl.protocol}//${parsedUrl.hostname}${parsedUrl.pathname}`;
276286
const serverMatch = api.serverMatcher.exec(url);
@@ -292,16 +302,16 @@ function stripTags(input: string | undefined): string | undefined {
292302
return input.replace(/(<([^>]+)>)/ig, '');
293303
}
294304

295-
export class ApiExchange {
296-
constructor(api: ApiMetadata, exchange: HttpExchange) {
305+
export class OpenApiExchange implements ApiExchange {
306+
constructor(api: OpenApiMetadata, exchange: HttpExchange) {
297307
const { request } = exchange;
298-
this.service = new ApiService(api.spec);
308+
this.service = new OpenApiService(api.spec);
299309

300310
this._spec = api.spec;
301311
this._opSpec = matchOperation(api, request);
302312

303-
this.operation = new ApiOperation(this._opSpec);
304-
this.request = new ApiRequest(api.spec, this._opSpec, request);
313+
this.operation = new OpenApiOperation(this._opSpec);
314+
this.request = new OpenApiRequest(api.spec, this._opSpec, request);
305315

306316
if (exchange.response) {
307317
this.updateWithResponse(exchange.response);
@@ -319,22 +329,22 @@ export class ApiExchange {
319329

320330
updateWithResponse(response: HtkResponse | 'aborted' | undefined): void {
321331
if (response === 'aborted' || response === undefined) return;
322-
this.response = new ApiResponse(this._spec, this._opSpec, response);
332+
this.response = new OpenApiResponse(this._spec, this._opSpec, response);
323333
}
324334

325335
matchedOperation() {
326336
return this._opSpec.matched;
327337
}
328338
}
329339

330-
class ApiService {
340+
class OpenApiService implements ApiService {
331341
constructor(spec: OpenAPIObject) {
332342
const { info: service } = spec;
333343

334344
this.name = service.title;
335345
this.logoUrl = service['x-logo']?.url
336346
this.description = fromMarkdown(service.description);
337-
this.docsUrl = get(spec, 'externalDocs', 'url');
347+
this.docsUrl = spec?.externalDocs?.url;
338348
}
339349

340350
public readonly name: string;
@@ -343,7 +353,7 @@ class ApiService {
343353
public readonly docsUrl?: string;
344354
}
345355

346-
function matchOperation(api: ApiMetadata, request: HtkRequest) {
356+
function matchOperation(api: OpenApiMetadata, request: HtkRequest) {
347357
const matchingPath = getPath(api, request);
348358

349359
const { pathSpec, path } = matchingPath || {
@@ -368,7 +378,7 @@ function matchOperation(api: ApiMetadata, request: HtkRequest) {
368378

369379
type MatchedOperation = ReturnType<typeof matchOperation>;
370380

371-
class ApiOperation {
381+
class OpenApiOperation implements ApiOperation {
372382
constructor(
373383
op: MatchedOperation
374384
) {
@@ -411,7 +421,7 @@ class ApiOperation {
411421
warnings: string[] = [];
412422
}
413423

414-
class ApiRequest {
424+
class OpenApiRequest implements ApiRequest {
415425
constructor(
416426
spec: OpenAPIObject,
417427
op: MatchedOperation,
@@ -430,24 +440,11 @@ class ApiRequest {
430440
);
431441
}
432442

433-
parameters: Parameter[];
443+
parameters: ApiParameter[];
434444
bodySchema?: SchemaObject;
435445
}
436446

437-
export interface Parameter {
438-
name: string;
439-
description?: Html;
440-
value?: unknown;
441-
defaultValue?: unknown;
442-
enum?: unknown[];
443-
type?: string;
444-
in: ParameterLocation;
445-
required: boolean;
446-
deprecated: boolean;
447-
warnings: string[];
448-
}
449-
450-
class ApiResponse {
447+
class OpenApiResponse implements ApiResponse {
451448
constructor(
452449
spec: OpenAPIObject,
453450
op: MatchedOperation,

src/model/http/exchange.ts

+4-3
Original file line numberDiff line numberDiff line change
@@ -31,8 +31,9 @@ import { getContentType } from '../events/content-types';
3131
import { HTKEventBase } from '../events/event-base';
3232

3333
import { ApiStore } from '../api/api-store';
34-
import { ApiExchange } from '../api/openapi';
35-
import { ApiMetadata } from '../api/build-openapi';
34+
import { ApiExchange } from '../api/api-interfaces';
35+
import { OpenApiExchange } from '../api/openapi';
36+
import { ApiMetadata } from '../api/api-interfaces';
3637
import { decodeBody } from '../../services/ui-worker-api';
3738
import {
3839
RequestBreakpoint,
@@ -325,7 +326,7 @@ export class HttpExchange extends HTKEventBase {
325326

326327
if (apiMetadata) {
327328
try {
328-
return new ApiExchange(apiMetadata, this);
329+
return new OpenApiExchange(apiMetadata, this);
329330
} catch (e) {
330331
reportError(e);
331332
throw e;

src/services/ui-worker-api.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ import type {
2424
import Worker from 'worker-loader!./ui-worker';
2525

2626
import { Omit } from '../types';
27-
import { ApiMetadata } from '../model/api/build-openapi';
27+
import { ApiMetadata } from '../model/api/api-interfaces';
2828
import { WorkerFormatterKey } from './ui-worker-formatters';
2929

3030
const worker = new Worker();

src/services/ui-worker.ts

+2-1
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,8 @@ import {
1313
SUPPORTED_ENCODING
1414
} from 'http-encoding';
1515

16-
import { buildApiMetadata, ApiMetadata } from '../model/api/build-openapi';
16+
import { ApiMetadata } from '../model/api/api-interfaces';
17+
import { buildApiMetadata } from '../model/api/build-openapi';
1718
import { parseCert, ParsedCertificate, validatePKCS12, ValidationResult } from '../model/crypto';
1819
import { WorkerFormatterKey, formatBuffer } from './ui-worker-formatters';
1920

0 commit comments

Comments
 (0)