-
Notifications
You must be signed in to change notification settings - Fork 57
/
Copy pathbatch-response-parser.ts
227 lines (198 loc) · 6.72 KB
/
batch-response-parser.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
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
/* Copyright (c) 2020 SAP SE or an SAP affiliate company. All rights reserved. */
import {
last,
createLogger,
pickValueIgnoreCase,
ErrorWithCause
} from '@sap-cloud-sdk/util';
import { HttpResponse } from '../../../http-client';
const logger = createLogger({
package: 'core',
messageContext: 'batch-response-parser'
});
/**
* Detects the system dependent line break in a string.
* @param str The string to check for line breaks. Should have at least two lines, otherwise an error will be thrown.
* @returns The system dependent line break
*/
export function detectNewLineSymbol(str: string): string {
if (str.includes('\r\n')) {
return '\r\n';
}
if (str.includes('\n')) {
return '\n';
}
throw new Error('Cannot detect line breaks in the batch response body.');
}
/**
* Get the response body from the string representation of a response.
* @param response String representation of a response.
* @returns The response body as a one line string.
*/
export function getResponseBody(response: string): string {
const newLineSymbol = detectNewLineSymbol(response);
const lines = response.split(newLineSymbol);
// A valid response should contain at least three lines, part id, empty line and response body.
if (lines.length >= 3) {
return lines[lines.length - 1];
}
throw Error(
`Cannot parse batch subrequest response body. Expected at least three lines in the response, got ${lines.length}.`
);
}
/**
* Parse the headers in the string representation of a response headers into an object. This will only look at the highest level of headers.
* @param response String representation of a response
* @returns The headers as an object.
*/
function parseHeaders(response: string): Record<string, any> {
const newLineSymbol = detectNewLineSymbol(response);
// split on the first empty line
const [responseHeaders] = response.split(newLineSymbol + newLineSymbol);
return responseHeaders.split(newLineSymbol).reduce((headers, line) => {
const [key, value] = line.split(':');
return { ...headers, [key]: value?.trim() };
}, {});
}
/**
* Get the boundary from the content type header value. Throws an error if no boundary can be found.
* @param contentType Value of the content type header
* @returns The boundary.
*/
function getBoundary(contentType: string | undefined): string {
const boundary = contentType?.match(/.*boundary=.+/)
? last(contentType.split('boundary='))
: undefined;
if (!boundary) {
throw new Error('No boundary found.');
}
return boundary;
}
/**
* Split a batch response into an array of sub responses for the retrieve requests and changesets.
* @param response The raw HTTP response.
* @returns A list of sub responses represented as strings.
*/
export function splitBatchResponse(response: HttpResponse): string[] {
const body = response.data.trim();
if (!body) {
return [];
}
try {
const boundary = getBoundary(
pickValueIgnoreCase(response.headers, 'content-type')
);
return splitResponse(body, boundary);
} catch (err) {
throw new ErrorWithCause('Could not parse batch response.', err);
}
}
/**
* Split a changeset (sub) response into an array of sub responses.
* @param changeSetResponse The string representation of a change set response.
* @returns A list of sub responses represented as strings.
*/
export function splitChangeSetResponse(changeSetResponse: string): string[] {
const headers = parseHeaders(changeSetResponse);
try {
const boundary = getBoundary(pickValueIgnoreCase(headers, 'content-type'));
return splitResponse(changeSetResponse, boundary);
} catch (err) {
throw new ErrorWithCause('Could not parse change set response.', err);
}
}
/**
* Split a string representation of a response into sub responses given its boundary.
* @param response The string representation of the response to split.
* @param boundary The boundary to split by.
* @returns A list of sub responses represented as strings.
*/
export function splitResponse(response: string, boundary: string): string[] {
const parts = response.split(`--${boundary}`).map(part => part.trim());
if (parts.length >= 3) {
return parts.slice(1, parts.length - 1);
}
throw new Error(
'Could not parse batch response body. Expected at least two response boundaries.'
);
}
/**
* Parse the HTTP code of response.
* @param response String representation of the response.
* @returns The HTTP code.
*/
export function parseHttpCode(response: string): number {
const group = response.match(/HTTP\/\d\.\d (\d{3}).*?/);
if (group) {
return parseInt(group[1].toString());
}
throw new Error('Cannot parse http code of the response.');
}
/**
* Get the body from the given response and parse it to JSON.
* @param response The string representation of a single response.
* @returns The parsed JSON representation of the response body.
*/
function parseResponseBody(response: string): Record<string, any> {
const responseBody = getResponseBody(response);
if (responseBody) {
try {
return JSON.parse(responseBody);
} catch (err) {
logger.error(
`Could not parse response body. Invalid JSON. Original Error: ${err}`
);
}
}
return {};
}
/**
* Parse the body and http code of a batch sub response.
* @param response A batch sub response.
* @returns The parsed response.s
*/
export function parseResponseData(response: string): ResponseData {
return {
body: parseResponseBody(response),
httpCode: parseHttpCode(response)
};
}
/**
* Parse the complete batch HTTP response.
* @param batchResponse HTTP response of a batch request.
* @returns An array of parsed sub responses of the batch response.
*/
export function parseBatchResponse(
batchResponse: HttpResponse
): (ResponseData | ResponseData[])[] {
return splitBatchResponse(batchResponse).map(response => {
const contentType = pickValueIgnoreCase(
parseHeaders(response),
'content-type'
);
if (isChangeSetContentType(contentType)) {
return splitChangeSetResponse(response).map(subResponse =>
parseResponseData(subResponse)
);
}
if (isRetrieveOrErrorContentType(contentType)) {
return parseResponseData(response);
}
throw Error(
`Cannot parse batch response. Unknown subresponse 'Content-Type' header '${contentType}'.`
);
});
}
function isChangeSetContentType(contentType: string): boolean {
return contentType?.trim().startsWith('multipart/mixed');
}
function isRetrieveOrErrorContentType(contentType: string): boolean {
return contentType?.trim().startsWith('application/http');
}
export function isHttpSuccessCode(httpCode): boolean {
return httpCode >= 200 && httpCode < 300;
}
export interface ResponseData {
body: Record<string, any>;
httpCode: number;
}