1 | "use strict";
|
2 | Object.defineProperty(exports, "__esModule", { value: true });
|
3 | const ts_morph_1 = require("ts-morph");
|
4 | const errors_1 = require("../errors");
|
5 | const locations_1 = require("../locations");
|
6 | const util_1 = require("../util");
|
7 | const default_response_parser_1 = require("./default-response-parser");
|
8 | const parser_helpers_1 = require("./parser-helpers");
|
9 | const request_parser_1 = require("./request-parser");
|
10 | const response_parser_1 = require("./response-parser");
|
11 | function parseEndpoint(klass, typeTable, lociTable) {
|
12 | var _a, _b;
|
13 | const decorator = klass.getDecoratorOrThrow("endpoint");
|
14 | const decoratorConfig = parser_helpers_1.getDecoratorConfigOrThrow(decorator);
|
15 |
|
16 | const name = klass.getNameOrThrow();
|
17 |
|
18 | const methodProp = parser_helpers_1.getObjLiteralPropOrThrow(decoratorConfig, "method");
|
19 | const methodLiteral = parser_helpers_1.getPropValueAsStringOrThrow(methodProp);
|
20 | const method = methodLiteral.getLiteralText();
|
21 | if (!parser_helpers_1.isHttpMethod(method)) {
|
22 | throw new Error(`expected a HttpMethod, got ${method}`);
|
23 | }
|
24 |
|
25 | const tagsResult = extractEndpointTags(decoratorConfig);
|
26 | if (tagsResult.isErr())
|
27 | return tagsResult;
|
28 | const tags = tagsResult.unwrap();
|
29 |
|
30 | const description = (_a = parser_helpers_1.getJsDoc(klass)) === null || _a === void 0 ? void 0 : _a.getDescription().trim();
|
31 |
|
32 | const draft = klass.getDecorator("draft") !== undefined;
|
33 |
|
34 | const requestMethod = parser_helpers_1.getMethodWithDecorator(klass, "request");
|
35 | const requestResult = requestMethod
|
36 | ? request_parser_1.parseRequest(requestMethod, typeTable, lociTable)
|
37 | : util_1.ok(undefined);
|
38 | if (requestResult.isErr())
|
39 | return requestResult;
|
40 | const request = requestResult.unwrap();
|
41 |
|
42 | const responsesResult = extractEndpointResponses(klass, typeTable, lociTable);
|
43 | if (responsesResult.isErr())
|
44 | return responsesResult;
|
45 | const responses = responsesResult.unwrap();
|
46 |
|
47 | const defaultResponseMethod = parser_helpers_1.getMethodWithDecorator(klass, "defaultResponse");
|
48 | const defaultResponseResult = defaultResponseMethod
|
49 | ? default_response_parser_1.parseDefaultResponse(defaultResponseMethod, typeTable, lociTable)
|
50 | : util_1.ok(undefined);
|
51 | if (defaultResponseResult.isErr())
|
52 | return defaultResponseResult;
|
53 | const defaultResponse = defaultResponseResult.unwrap();
|
54 |
|
55 | const pathResult = extractEndpointPath(decoratorConfig);
|
56 | if (pathResult.isErr())
|
57 | return pathResult;
|
58 | const path = pathResult.unwrap();
|
59 |
|
60 | const pathParamsInPath = getDynamicPathComponents(path);
|
61 | const pathParamsInRequest = (_b = request === null || request === void 0 ? void 0 : request.pathParams.map(pathParam => pathParam.name)) !== null && _b !== void 0 ? _b : [];
|
62 | const exclusivePathParamsInPath = pathParamsInPath.filter(pathParam => !pathParamsInRequest.includes(pathParam));
|
63 | const exclusivePathParamsInRequest = pathParamsInRequest.filter(pathParam => !pathParamsInPath.includes(pathParam));
|
64 | if (exclusivePathParamsInPath.length !== 0) {
|
65 | return util_1.err(new errors_1.ParserError(`endpoint path dynamic components must have a corresponding path param defined in @request. Violating path components: ${exclusivePathParamsInPath.join(", ")}`, {
|
66 | file: klass.getSourceFile().getFilePath(),
|
67 | position: klass.getPos()
|
68 | }));
|
69 | }
|
70 | if (exclusivePathParamsInRequest.length !== 0) {
|
71 | return util_1.err(new errors_1.ParserError(`endpoint request path params must have a corresponding dynamic path component defined in @endpoint. Violating path params: ${exclusivePathParamsInRequest.join(", ")}`, {
|
72 | file: klass.getSourceFile().getFilePath(),
|
73 | position: klass.getPos()
|
74 | }));
|
75 | }
|
76 |
|
77 | lociTable.addMorphNode(locations_1.LociTable.endpointClassKey(name), klass);
|
78 | lociTable.addMorphNode(locations_1.LociTable.endpointDecoratorKey(name), decorator);
|
79 | lociTable.addMorphNode(locations_1.LociTable.endpointMethodKey(name), methodProp);
|
80 | return util_1.ok({
|
81 | name,
|
82 | description,
|
83 | tags,
|
84 | method,
|
85 | path,
|
86 | request,
|
87 | responses,
|
88 | defaultResponse,
|
89 | draft
|
90 | });
|
91 | }
|
92 | exports.parseEndpoint = parseEndpoint;
|
93 | function extractEndpointTags(decoratorConfig) {
|
94 | const tagsProp = parser_helpers_1.getObjLiteralProp(decoratorConfig, "tags");
|
95 | if (tagsProp === undefined)
|
96 | return util_1.ok([]);
|
97 | const tagsLiteral = parser_helpers_1.getPropValueAsArrayOrThrow(tagsProp);
|
98 | const tags = [];
|
99 | for (const elementExpr of tagsLiteral.getElements()) {
|
100 |
|
101 | if (!ts_morph_1.TypeGuards.isStringLiteral(elementExpr)) {
|
102 | return util_1.err(new errors_1.ParserError("endpoint tag must be a string", {
|
103 | file: elementExpr.getSourceFile().getFilePath(),
|
104 | position: elementExpr.getPos()
|
105 | }));
|
106 | }
|
107 | const tag = elementExpr.getLiteralText().trim();
|
108 | if (tag.length === 0) {
|
109 | return util_1.err(new errors_1.ParserError("endpoint tag cannot be blank", {
|
110 | file: elementExpr.getSourceFile().getFilePath(),
|
111 | position: elementExpr.getPos()
|
112 | }));
|
113 | }
|
114 | if (!/^[\w\s-]*$/.test(tag)) {
|
115 | return util_1.err(new errors_1.ParserError("endpoint tag may only contain alphanumeric, space, underscore and hyphen characters", {
|
116 | file: elementExpr.getSourceFile().getFilePath(),
|
117 | position: elementExpr.getPos()
|
118 | }));
|
119 | }
|
120 | tags.push(tag);
|
121 | }
|
122 | const duplicateTags = [
|
123 | ...new Set(tags.filter((tag, index) => tags.indexOf(tag) !== index))
|
124 | ];
|
125 | if (duplicateTags.length !== 0) {
|
126 | return util_1.err(new errors_1.ParserError(`endpoint tags may not contain duplicates: ${duplicateTags.join(", ")}`, {
|
127 | file: tagsProp.getSourceFile().getFilePath(),
|
128 | position: tagsProp.getPos()
|
129 | }));
|
130 | }
|
131 | return util_1.ok(tags.sort((a, b) => (b > a ? -1 : 1)));
|
132 | }
|
133 | function extractEndpointPath(decoratorConfig) {
|
134 | const pathProp = parser_helpers_1.getObjLiteralPropOrThrow(decoratorConfig, "path");
|
135 | const pathLiteral = parser_helpers_1.getPropValueAsStringOrThrow(pathProp);
|
136 | const path = pathLiteral.getLiteralText();
|
137 | const dynamicComponents = getDynamicPathComponents(path);
|
138 | const duplicateDynamicComponents = [
|
139 | ...new Set(dynamicComponents.filter((component, index) => dynamicComponents.indexOf(component) !== index))
|
140 | ];
|
141 | if (duplicateDynamicComponents.length !== 0) {
|
142 | return util_1.err(new errors_1.ParserError("endpoint path dynamic components must have unique names", {
|
143 | file: pathProp.getSourceFile().getFilePath(),
|
144 | position: pathProp.getPos()
|
145 | }));
|
146 | }
|
147 | return util_1.ok(path);
|
148 | }
|
149 | function extractEndpointResponses(klass, typeTable, lociTable) {
|
150 | const responseMethods = klass
|
151 | .getMethods()
|
152 | .filter(m => m.getDecorator("response") !== undefined);
|
153 | const responses = [];
|
154 | for (const method of responseMethods) {
|
155 | const responseResult = response_parser_1.parseResponse(method, typeTable, lociTable);
|
156 | if (responseResult.isErr())
|
157 | return responseResult;
|
158 | responses.push(responseResult.unwrap());
|
159 | }
|
160 |
|
161 | const statuses = responses.map(r => r.status);
|
162 | const duplicateStatuses = [
|
163 | ...new Set(statuses.filter((status, index) => statuses.indexOf(status) !== index))
|
164 | ];
|
165 | if (duplicateStatuses.length !== 0) {
|
166 | return util_1.err(new errors_1.ParserError(`endpoint responses must have unique statuses. Duplicates found: ${duplicateStatuses.join(", ")}`, { file: klass.getSourceFile().getFilePath(), position: klass.getPos() }));
|
167 | }
|
168 | return util_1.ok(responses.sort((a, b) => (b.status > a.status ? -1 : 1)));
|
169 | }
|
170 | function getDynamicPathComponents(path) {
|
171 | return path
|
172 | .split("/")
|
173 | .filter(component => component.startsWith(":"))
|
174 | .map(component => component.substr(1));
|
175 | }
|