-
Notifications
You must be signed in to change notification settings - Fork 333
/
index.ts
135 lines (123 loc) · 5.36 KB
/
index.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
import { IPrismDiagnostic, ValidatorFn } from '@stoplight/prism-core';
import {
DiagnosticSeverity,
IHttpOperation,
IHttpOperationResponse,
IMediaTypeContent,
IHttpOperationRequestBody,
} from '@stoplight/types';
import * as caseless from 'caseless';
import { findFirst, isNonEmpty } from 'fp-ts/lib/Array';
import * as O from 'fp-ts/lib/Option';
import * as E from 'fp-ts/lib/Either';
import * as typeIs from 'type-is';
import { pipe } from 'fp-ts/lib/pipeable';
import { inRange } from 'lodash';
import { validateSecurity } from './validators/security';
// @ts-ignore
import { URI } from 'uri-template-lite';
import { IHttpRequest, IHttpResponse } from '../types';
import {
header as headerDeserializerRegistry,
query as queryDeserializerRegistry,
path as pathDeserializerRegistry,
} from './deserializers';
import { findOperationResponse } from './utils/spec';
import { HttpBodyValidator, HttpHeadersValidator, HttpQueryValidator } from './validators';
import { NonEmptyArray } from 'fp-ts/lib/NonEmptyArray';
import { sequenceValidation, sequenceOption } from './validators/utils';
import { HttpPathValidator } from './validators/path';
export const bodyValidator = new HttpBodyValidator('body');
export const headersValidator = new HttpHeadersValidator(headerDeserializerRegistry, 'header');
export const queryValidator = new HttpQueryValidator(queryDeserializerRegistry, 'query');
export const pathValidator = new HttpPathValidator(pathDeserializerRegistry, 'path');
const checkBodyIsProvided = (requestBody: IHttpOperationRequestBody, body: unknown) =>
pipe(
requestBody,
E.fromPredicate<NonEmptyArray<IPrismDiagnostic>, IHttpOperationRequestBody>(
requestBody => !(!!requestBody.required && !body),
() => [{ code: 'required', message: 'Body parameter is required', severity: DiagnosticSeverity.Error }]
)
);
const validateIfBodySpecIsProvided = (body: unknown, mediaType: string, contents?: IMediaTypeContent[]) =>
pipe(
sequenceOption(O.fromNullable(body), O.fromNullable(contents)),
O.fold(
() => E.right(body),
([body, contents]) => bodyValidator.validate(body, contents, mediaType)
)
);
const validateBody = (requestBody: IHttpOperationRequestBody, body: unknown, mediaType: string) =>
pipe(
checkBodyIsProvided(requestBody, body),
E.chain(() => validateIfBodySpecIsProvided(body, mediaType, requestBody.contents))
);
const validateInput: ValidatorFn<IHttpOperation, IHttpRequest> = ({ resource, element }) => {
const mediaType = caseless(element.headers || {}).get('content-type');
const { request } = resource;
const { body } = element;
return pipe(
E.fromNullable(undefined)(request),
E.fold(
e => E.right<NonEmptyArray<IPrismDiagnostic>, unknown>(e),
request =>
sequenceValidation(
request.body ? validateBody(request.body, body, mediaType) : E.right(undefined),
request.headers ? headersValidator.validate(element.headers || {}, request.headers) : E.right(undefined),
request.query ? queryValidator.validate(element.url.query || {}, request.query) : E.right(undefined),
request.path
? pathValidator.validate(getPathParams(element.url.path, resource.path), request.path)
: E.right(undefined)
)
),
E.map(() => element)
);
};
const findResponseByStatus = (responses: NonEmptyArray<IHttpOperationResponse>, statusCode: number) =>
pipe(
findOperationResponse(responses, statusCode),
E.fromOption<IPrismDiagnostic>(() => ({
message: `Unable to match the returned status code with those defined in the document: ${responses
.map(response => response.code)
.join(',')}`,
severity: inRange(statusCode, 200, 300) ? DiagnosticSeverity.Error : DiagnosticSeverity.Warning,
})),
E.mapLeft<IPrismDiagnostic, NonEmptyArray<IPrismDiagnostic>>(error => [error])
);
const validateMediaType = (contents: NonEmptyArray<IMediaTypeContent>, mediaType: string) =>
pipe(
contents,
findFirst(c => !!typeIs.is(mediaType, [c.mediaType])),
E.fromOption<IPrismDiagnostic>(() => ({
message: `The received media type "${mediaType || ''}" does not match the one${
contents.length > 1 ? 's' : ''
} specified in the current response: ${contents.map(c => c.mediaType).join(', ')}`,
severity: DiagnosticSeverity.Error,
})),
E.mapLeft<IPrismDiagnostic, NonEmptyArray<IPrismDiagnostic>>(e => [e])
);
const validateOutput: ValidatorFn<IHttpOperation, IHttpResponse> = ({ resource, element }) => {
const mediaType = caseless(element.headers || {}).get('content-type');
return pipe(
findResponseByStatus(resource.responses, element.statusCode),
E.chain(response =>
sequenceValidation(
pipe(
O.fromNullable(response.contents),
O.chain(contents => pipe(contents, O.fromPredicate(isNonEmpty))),
O.fold(
() => E.right<NonEmptyArray<IPrismDiagnostic>, unknown>(undefined),
contents => validateMediaType(contents, mediaType)
)
),
bodyValidator.validate(element.body, response.contents || [], mediaType),
headersValidator.validate(element.headers || {}, response.headers || [])
)
),
E.map(() => element)
);
};
function getPathParams(path: string, template: string) {
return new URI.Template(template).match(path);
}
export { validateInput, validateOutput, validateSecurity };