@outfoxx/sunday 1.0.9 → 1.1.0-alpha.11

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (179) hide show
  1. package/LICENSE.txt +203 -0
  2. package/README.md +35 -2
  3. package/dist/any-type.d.ts +2 -0
  4. package/dist/any-type.js +13 -0
  5. package/dist/any-type.js.map +1 -1
  6. package/dist/class-type.d.ts +2 -2
  7. package/dist/class-type.js +13 -0
  8. package/dist/class-type.js.map +1 -1
  9. package/dist/date-time-types.d.ts +2 -7
  10. package/dist/date-time-types.js +15 -4
  11. package/dist/date-time-types.js.map +1 -1
  12. package/dist/event-parser.d.ts +17 -0
  13. package/dist/event-parser.js +153 -0
  14. package/dist/event-parser.js.map +1 -0
  15. package/dist/fetch-event-source.d.ts +12 -10
  16. package/dist/fetch-event-source.js +154 -138
  17. package/dist/fetch-event-source.js.map +1 -1
  18. package/dist/fetch-request-factory.d.ts +10 -5
  19. package/dist/fetch-request-factory.js +100 -71
  20. package/dist/fetch-request-factory.js.map +1 -1
  21. package/dist/fetch.d.ts +7 -1
  22. package/dist/fetch.js +74 -8
  23. package/dist/fetch.js.map +1 -1
  24. package/dist/header-parameters.d.ts +3 -0
  25. package/dist/header-parameters.js +53 -0
  26. package/dist/header-parameters.js.map +1 -0
  27. package/dist/index.d.ts +12 -12
  28. package/dist/index.js +25 -12
  29. package/dist/index.js.map +1 -1
  30. package/dist/logger.js +13 -0
  31. package/dist/logger.js.map +1 -1
  32. package/dist/{binary-decoder.d.ts → media-type-codecs/binary-decoder.d.ts} +2 -2
  33. package/dist/media-type-codecs/binary-decoder.js +29 -0
  34. package/dist/media-type-codecs/binary-decoder.js.map +1 -0
  35. package/dist/{binary-encoder.d.ts → media-type-codecs/binary-encoder.d.ts} +0 -0
  36. package/dist/media-type-codecs/binary-encoder.js +26 -0
  37. package/dist/media-type-codecs/binary-encoder.js.map +1 -0
  38. package/dist/media-type-codecs/cbor-decoder.d.ts +39 -0
  39. package/dist/media-type-codecs/cbor-decoder.js +426 -0
  40. package/dist/media-type-codecs/cbor-decoder.js.map +1 -0
  41. package/dist/media-type-codecs/cbor-encoder.d.ts +43 -0
  42. package/dist/media-type-codecs/cbor-encoder.js +239 -0
  43. package/dist/media-type-codecs/cbor-encoder.js.map +1 -0
  44. package/dist/media-type-codecs/cbor-tags.d.ts +4 -0
  45. package/dist/media-type-codecs/cbor-tags.js +18 -0
  46. package/dist/media-type-codecs/cbor-tags.js.map +1 -0
  47. package/dist/media-type-codecs/json-decoder.d.ts +39 -0
  48. package/dist/media-type-codecs/json-decoder.js +380 -0
  49. package/dist/media-type-codecs/json-decoder.js.map +1 -0
  50. package/dist/media-type-codecs/json-encoder.d.ts +44 -0
  51. package/dist/media-type-codecs/json-encoder.js +276 -0
  52. package/dist/media-type-codecs/json-encoder.js.map +1 -0
  53. package/dist/media-type-codecs/media-type-decoder.d.ts +11 -0
  54. package/dist/media-type-codecs/media-type-decoder.js +19 -0
  55. package/dist/media-type-codecs/media-type-decoder.js.map +1 -0
  56. package/dist/{media-type-decoders.d.ts → media-type-codecs/media-type-decoders.d.ts} +5 -4
  57. package/dist/media-type-codecs/media-type-decoders.js +53 -0
  58. package/dist/media-type-codecs/media-type-decoders.js.map +1 -0
  59. package/dist/{media-type-encoder.d.ts → media-type-codecs/media-type-encoder.d.ts} +5 -1
  60. package/dist/media-type-codecs/media-type-encoder.js +24 -0
  61. package/dist/media-type-codecs/media-type-encoder.js.map +1 -0
  62. package/dist/{media-type-encoders.d.ts → media-type-codecs/media-type-encoders.d.ts} +5 -4
  63. package/dist/media-type-codecs/media-type-encoders.js +55 -0
  64. package/dist/media-type-codecs/media-type-encoders.js.map +1 -0
  65. package/dist/{url-encoder.d.ts → media-type-codecs/www-form-url-encoder.d.ts} +8 -8
  66. package/dist/{url-encoder.js → media-type-codecs/www-form-url-encoder.js} +51 -37
  67. package/dist/media-type-codecs/www-form-url-encoder.js.map +1 -0
  68. package/dist/media-type.d.ts +91 -13
  69. package/dist/media-type.js +286 -15
  70. package/dist/media-type.js.map +1 -1
  71. package/dist/problem.d.ts +24 -9
  72. package/dist/problem.js +153 -6
  73. package/dist/problem.js.map +1 -1
  74. package/dist/request-factory.d.ts +10 -10
  75. package/dist/request-factory.js +13 -0
  76. package/dist/request-factory.js.map +1 -1
  77. package/dist/sunday-error.d.ts +11 -0
  78. package/dist/sunday-error.js +31 -0
  79. package/dist/sunday-error.js.map +1 -0
  80. package/dist/url-template.js +13 -0
  81. package/dist/url-template.js.map +1 -1
  82. package/dist/util/any.d.ts +2 -0
  83. package/dist/util/any.js +24 -0
  84. package/dist/util/any.js.map +1 -0
  85. package/dist/util/base64.js +13 -0
  86. package/dist/util/base64.js.map +1 -1
  87. package/dist/util/error.d.ts +2 -0
  88. package/dist/util/error.js +26 -0
  89. package/dist/util/error.js.map +1 -0
  90. package/dist/util/hex.js +16 -2
  91. package/dist/util/hex.js.map +1 -1
  92. package/dist/util/rxjs.d.ts +3 -0
  93. package/dist/util/rxjs.js +22 -5
  94. package/dist/util/rxjs.js.map +1 -1
  95. package/dist/util/stream-rxjs.js +13 -0
  96. package/dist/util/stream-rxjs.js.map +1 -1
  97. package/dist/util/temporal.d.ts +2 -0
  98. package/dist/util/temporal.js +31 -0
  99. package/dist/util/temporal.js.map +1 -0
  100. package/package.json +29 -22
  101. package/src/any-type.ts +18 -0
  102. package/src/class-type.ts +20 -2
  103. package/src/date-time-types.ts +35 -8
  104. package/src/event-parser.ts +204 -0
  105. package/src/fetch-event-source.ts +163 -159
  106. package/src/fetch-request-factory.ts +142 -100
  107. package/src/fetch.ts +79 -14
  108. package/src/header-parameters.ts +66 -0
  109. package/src/index.ts +26 -12
  110. package/src/logger.ts +14 -0
  111. package/src/media-type-codecs/binary-decoder.ts +38 -0
  112. package/src/media-type-codecs/binary-encoder.ts +33 -0
  113. package/src/media-type-codecs/cbor-decoder.ts +529 -0
  114. package/src/media-type-codecs/cbor-encoder.ts +321 -0
  115. package/src/media-type-codecs/cbor-tags.ts +18 -0
  116. package/src/media-type-codecs/json-decoder.ts +484 -0
  117. package/src/media-type-codecs/json-encoder.ts +342 -0
  118. package/src/media-type-codecs/media-type-decoder.ts +34 -0
  119. package/src/media-type-codecs/media-type-decoders.ts +81 -0
  120. package/src/media-type-codecs/media-type-encoder.ts +45 -0
  121. package/src/media-type-codecs/media-type-encoders.ts +83 -0
  122. package/src/media-type-codecs/www-form-url-encoder.ts +193 -0
  123. package/src/media-type.ts +340 -22
  124. package/src/problem.ts +158 -12
  125. package/src/request-factory.ts +35 -12
  126. package/src/sunday-error.ts +51 -0
  127. package/src/url-template.ts +14 -0
  128. package/src/util/any.ts +24 -0
  129. package/src/util/base64.ts +14 -0
  130. package/src/util/error.ts +28 -0
  131. package/src/util/hex.ts +17 -2
  132. package/src/util/rxjs.ts +30 -5
  133. package/src/util/stream-rxjs.ts +14 -0
  134. package/src/util/temporal.ts +32 -0
  135. package/dist/binary-decoder.js +0 -16
  136. package/dist/binary-decoder.js.map +0 -1
  137. package/dist/binary-encoder.js +0 -13
  138. package/dist/binary-encoder.js.map +0 -1
  139. package/dist/cbor-decoder.d.ts +0 -15
  140. package/dist/cbor-decoder.js +0 -126
  141. package/dist/cbor-decoder.js.map +0 -1
  142. package/dist/cbor-encoder.d.ts +0 -29
  143. package/dist/cbor-encoder.js +0 -81
  144. package/dist/cbor-encoder.js.map +0 -1
  145. package/dist/cbor-tags.d.ts +0 -3
  146. package/dist/cbor-tags.js +0 -4
  147. package/dist/cbor-tags.js.map +0 -1
  148. package/dist/http-error.d.ts +0 -10
  149. package/dist/http-error.js +0 -45
  150. package/dist/http-error.js.map +0 -1
  151. package/dist/json-decoder.d.ts +0 -31
  152. package/dist/json-decoder.js +0 -139
  153. package/dist/json-decoder.js.map +0 -1
  154. package/dist/json-encoder.d.ts +0 -35
  155. package/dist/json-encoder.js +0 -134
  156. package/dist/json-encoder.js.map +0 -1
  157. package/dist/media-type-decoder.d.ts +0 -4
  158. package/dist/media-type-decoder.js +0 -2
  159. package/dist/media-type-decoder.js.map +0 -1
  160. package/dist/media-type-decoders.js +0 -40
  161. package/dist/media-type-decoders.js.map +0 -1
  162. package/dist/media-type-encoder.js +0 -6
  163. package/dist/media-type-encoder.js.map +0 -1
  164. package/dist/media-type-encoders.js +0 -42
  165. package/dist/media-type-encoders.js.map +0 -1
  166. package/dist/url-encoder.js.map +0 -1
  167. package/src/binary-decoder.ts +0 -24
  168. package/src/binary-encoder.ts +0 -19
  169. package/src/cbor-decoder.ts +0 -148
  170. package/src/cbor-encoder.ts +0 -95
  171. package/src/cbor-tags.ts +0 -3
  172. package/src/http-error.ts +0 -55
  173. package/src/json-decoder.ts +0 -164
  174. package/src/json-encoder.ts +0 -162
  175. package/src/media-type-decoder.ts +0 -5
  176. package/src/media-type-decoders.ts +0 -59
  177. package/src/media-type-encoder.ts +0 -16
  178. package/src/media-type-encoders.ts +0 -61
  179. package/src/url-encoder.ts +0 -173
@@ -1,29 +1,46 @@
1
- import { Observable, of, Subscriber } from 'rxjs';
2
- import { mapTo, switchMap } from 'rxjs/operators';
1
+ // Copyright 2020 Outfox, Inc.
2
+ //
3
+ // Licensed under the Apache License, Version 2.0 (the "License");
4
+ // you may not use this file except in compliance with the License.
5
+ // You may obtain a copy of the License at
6
+ //
7
+ // http://www.apache.org/licenses/LICENSE-2.0
8
+ //
9
+ // Unless required by applicable law or agreed to in writing, software
10
+ // distributed under the License is distributed on an "AS IS" BASIS,
11
+ // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ // See the License for the specific language governing permissions and
13
+ // limitations under the License.
14
+
15
+ import { defer, map, Observable, of, switchMap } from 'rxjs';
3
16
  import { AnyType } from './any-type';
17
+ import { ConstructableClassType } from './class-type';
4
18
  import { validate } from './fetch';
5
19
  import { FetchEventSource } from './fetch-event-source';
6
- import { HttpError } from './http-error';
7
- import { JSONDecoder } from './json-decoder';
20
+ import { HeaderParameters } from './header-parameters';
8
21
  import { Logger } from './logger';
9
- import { mediaType, MediaType } from './media-type';
10
- import { MediaTypeDecoders } from './media-type-decoders';
11
- import { isURLQueryParamsEncoder } from './media-type-encoder';
12
- import { MediaTypeEncoders } from './media-type-encoders';
22
+ import { MediaType } from './media-type';
23
+ import { TextMediaTypeDecoder } from './media-type-codecs/media-type-decoder';
24
+ import { MediaTypeDecoders } from './media-type-codecs/media-type-decoders';
25
+ import { isURLQueryParamsEncoder } from './media-type-codecs/media-type-encoder';
26
+ import { MediaTypeEncoders } from './media-type-codecs/media-type-encoders';
27
+ import { Problem } from './problem';
13
28
  import {
14
- EventTypes,
15
29
  ExtEventSource,
16
30
  RequestAdapter,
17
31
  RequestFactory,
18
32
  RequestSpec,
19
33
  } from './request-factory';
34
+ import { SundayError } from './sunday-error';
20
35
  import { URLTemplate } from './url-template';
36
+ import { errorToMessage } from './util/error';
21
37
 
22
38
  export class FetchRequestFactory implements RequestFactory {
23
39
  public baseUrl: URLTemplate;
24
40
  public adapter?: RequestAdapter;
25
41
  public mediaTypeEncoders: MediaTypeEncoders;
26
42
  public mediaTypeDecoders: MediaTypeDecoders;
43
+ public problemTypes = new Map<string, ConstructableClassType<Problem>>();
27
44
  public logger?: Logger;
28
45
 
29
46
  constructor(
@@ -45,71 +62,92 @@ export class FetchRequestFactory implements RequestFactory {
45
62
  this.logger = options?.logger ?? console;
46
63
  }
47
64
 
65
+ registerProblem(
66
+ type: URL | string,
67
+ problemType: ConstructableClassType<Problem>
68
+ ): void {
69
+ const typeStr = type instanceof URL ? type.toString() : type;
70
+ this.problemTypes.set(typeStr, problemType);
71
+ }
72
+
48
73
  request(
49
74
  requestSpec: RequestSpec<unknown>,
50
75
  requestInit?: RequestInit
51
76
  ): Observable<Request> {
52
77
  //
53
- const url = this.baseUrl.complete(
54
- requestSpec.pathTemplate,
55
- requestSpec.pathParameters ?? {}
56
- );
57
-
58
- if (requestSpec.queryParameters) {
59
- const encoder = this.mediaTypeEncoders.find(
60
- MediaType.WWW_URL_FORM_ENCODED
78
+ return defer(() => {
79
+ const url = this.baseUrl.complete(
80
+ requestSpec.pathTemplate,
81
+ requestSpec.pathParameters ?? {}
61
82
  );
62
- if (!isURLQueryParamsEncoder(encoder)) {
63
- throw Error(
64
- `MediaTypeEncoder for ${MediaType.WWW_URL_FORM_ENCODED} must be an instance of URLEncoder`
83
+
84
+ if (requestSpec.queryParameters) {
85
+ const encoder = this.mediaTypeEncoders.find(
86
+ MediaType.WWWFormUrlEncoded
65
87
  );
88
+ if (!isURLQueryParamsEncoder(encoder)) {
89
+ throw Error(
90
+ `MediaTypeEncoder for ${MediaType.WWWFormUrlEncoded} must be an instance of URLQueryParamsEncoder`
91
+ );
92
+ }
93
+ url.search = `?${encoder.encodeQueryString(
94
+ requestSpec.queryParameters
95
+ )}`;
66
96
  }
67
- url.search = `?${encoder.encodeQueryString(requestSpec.queryParameters)}`;
68
- }
69
97
 
70
- const headers = new Headers(requestSpec.headers);
98
+ const headers = new Headers(HeaderParameters.encode(requestSpec.headers));
71
99
 
72
- // Determine & add accept header
73
- const supportedAcceptTypes = requestSpec.acceptTypes?.filter((type) =>
74
- this.mediaTypeDecoders.supports(type)
75
- );
76
- if (supportedAcceptTypes?.length) {
77
- const accept = supportedAcceptTypes.join(' , ');
100
+ // Determine & add accept header
101
+ if (requestSpec.acceptTypes) {
102
+ const supportedAcceptTypes = requestSpec.acceptTypes.filter((type) =>
103
+ this.mediaTypeDecoders.supports(type)
104
+ );
78
105
 
79
- headers.set('accept', accept);
80
- }
106
+ if (!supportedAcceptTypes.length) {
107
+ throw Error(
108
+ 'None of the provided accept types has a reqistered decoder'
109
+ );
110
+ }
81
111
 
82
- // Determine content type
83
- const contentType = requestSpec.contentTypes?.find((type) =>
84
- this.mediaTypeEncoders.supports(type)
85
- );
112
+ const accept = supportedAcceptTypes.join(' , ');
86
113
 
87
- // If matched, add content type (even if body is nil, to match any expected server requirements)
88
- if (contentType) {
89
- headers.set('content-type', contentType);
90
- }
114
+ headers.set('accept', accept);
115
+ }
91
116
 
92
- // Encode & add body data
93
- let body: BodyInit | undefined;
94
- if (requestSpec.body) {
95
- if (!contentType) {
96
- throw Error('Unsupported content-type for request body');
117
+ // Determine content type
118
+ const contentType = requestSpec.contentTypes?.find((type) =>
119
+ this.mediaTypeEncoders.supports(type)
120
+ );
121
+
122
+ // If matched, add content type (even if body is nil, to match any expected server requirements)
123
+ if (contentType) {
124
+ headers.set('content-type', contentType.toString());
97
125
  }
98
126
 
99
- body = this.mediaTypeEncoders
100
- .find(contentType)
101
- .encode(requestSpec.body, requestSpec.bodyType);
102
- }
127
+ // Encode & add body data
128
+ let body: BodyInit | undefined;
129
+ if (requestSpec.body) {
130
+ if (!contentType) {
131
+ throw Error(
132
+ 'None of the provided content types has a registered encoder'
133
+ );
134
+ }
103
135
 
104
- const init: RequestInit = Object.assign({}, requestInit, {
105
- headers,
106
- body,
107
- method: requestSpec.method,
108
- });
136
+ body = this.mediaTypeEncoders
137
+ .find(contentType)
138
+ .encode(requestSpec.body, requestSpec.bodyType);
139
+ }
109
140
 
110
- const request = new Request(url.toString(), init);
141
+ const init: RequestInit = Object.assign({}, requestInit, {
142
+ headers,
143
+ body,
144
+ method: requestSpec.method,
145
+ });
146
+
147
+ const request = new Request(url.toString(), init);
111
148
 
112
- return this.adapter?.(request) ?? of(request);
149
+ return this.adapter?.(request) ?? of(request);
150
+ });
113
151
  }
114
152
 
115
153
  response(
@@ -120,7 +158,9 @@ export class FetchRequestFactory implements RequestFactory {
120
158
  request instanceof Request ? of(request) : this.request(request);
121
159
  return request$.pipe(
122
160
  switchMap((req) => fetch(req)),
123
- switchMap((response) => validate(response, dataExpected ?? false))
161
+ switchMap((response) =>
162
+ validate(response, dataExpected ?? false, this.problemTypes)
163
+ )
124
164
  );
125
165
  }
126
166
 
@@ -133,20 +173,20 @@ export class FetchRequestFactory implements RequestFactory {
133
173
  const response$ = this.response(request, !!responseType);
134
174
 
135
175
  if (!responseType) {
136
- return response$.pipe(mapTo(undefined));
176
+ return response$.pipe(map(() => undefined));
137
177
  } else {
138
178
  return response$.pipe(
139
179
  switchMap(async (response) => {
140
180
  try {
141
- const contentType = mediaType(
181
+ const contentType = MediaType.from(
142
182
  response.headers.get('content-type'),
143
- MediaType.OCTET_STREAM
183
+ MediaType.OctetStream
144
184
  );
145
185
  const decoder = this.mediaTypeDecoders.find(contentType);
146
186
  return await decoder.decode(response, responseType);
147
187
  } catch (error) {
148
- throw await HttpError.fromResponse(
149
- error.message ?? 'Unknown Error',
188
+ throw await SundayError.fromResponse(
189
+ errorToMessage(error, 'Response Decoding Failed'),
150
190
  response
151
191
  );
152
192
  }
@@ -155,15 +195,7 @@ export class FetchRequestFactory implements RequestFactory {
155
195
  }
156
196
  }
157
197
 
158
- events(requestSpec: RequestSpec<void>): ExtEventSource;
159
- events<E>(
160
- requestSpec: RequestSpec<void>,
161
- eventTypes: EventTypes<E>
162
- ): Observable<E>;
163
- events(
164
- requestSpec: RequestSpec<void>,
165
- eventTypes?: EventTypes<unknown>
166
- ): ExtEventSource | Observable<unknown> {
198
+ eventSource(requestSpec: RequestSpec<void>): ExtEventSource {
167
199
  //
168
200
  const adapter = (
169
201
  url: string,
@@ -175,45 +207,55 @@ export class FetchRequestFactory implements RequestFactory {
175
207
  return this.request(eventSourceSpec, requestInit);
176
208
  };
177
209
 
178
- const eventSource = new FetchEventSource(requestSpec.pathTemplate, {
210
+ return new FetchEventSource(requestSpec.pathTemplate, {
179
211
  logger: this.logger,
180
212
  adapter,
181
213
  });
214
+ }
182
215
 
183
- if (!eventTypes) {
184
- return eventSource;
185
- }
216
+ eventStream<E>(
217
+ requestSpec: RequestSpec<void>,
218
+ decoder: (
219
+ decoder: TextMediaTypeDecoder,
220
+ event: string | undefined,
221
+ id: string | undefined,
222
+ data: string,
223
+ logger?: Logger
224
+ ) => E | undefined
225
+ ): Observable<E> {
226
+ const eventSource = this.eventSource(requestSpec);
186
227
 
187
- const generateEventHandler = (
188
- eventType: AnyType,
189
- subscriber: Subscriber<unknown>
190
- ) => {
191
- return async (event: Event) => {
192
- const decoder = this.mediaTypeDecoders.find(
193
- 'application/json'
194
- ) as JSONDecoder;
195
- const msgEvent = event as MessageEvent;
196
- const deserializedEvent = await decoder.decodeText(
197
- msgEvent.data,
198
- eventType
199
- );
200
- subscriber.next(deserializedEvent);
201
- };
202
- };
228
+ const jsonDecoder = this.mediaTypeDecoders.find(
229
+ MediaType.JSON
230
+ ) as TextMediaTypeDecoder;
203
231
 
204
232
  return new Observable((subscriber) => {
205
- for (const eventTypeName of Object.keys(eventTypes)) {
206
- const eventType = eventTypes[eventTypeName];
233
+ eventSource.onmessage = (event: MessageEvent<string>) => {
234
+ if (!event.data) {
235
+ return;
236
+ }
207
237
 
208
- eventSource.addEventListener(
209
- eventTypeName,
210
- generateEventHandler(eventType, subscriber)
211
- );
238
+ try {
239
+ const decodedEvent = decoder(
240
+ jsonDecoder,
241
+ event.type,
242
+ event.lastEventId,
243
+ event.data,
244
+ this.logger
245
+ );
246
+ if (!decodedEvent) {
247
+ return;
248
+ }
212
249
 
213
- eventSource.onerror = (event) => {
214
- this.logger?.error?.({ event }, 'event source error');
215
- };
216
- }
250
+ subscriber.next(decodedEvent);
251
+ } catch (error) {
252
+ subscriber.error(error);
253
+ }
254
+ };
255
+
256
+ eventSource.onerror = (event) => {
257
+ this.logger?.error?.({ event }, 'event source error');
258
+ };
217
259
 
218
260
  eventSource.connect();
219
261
 
package/src/fetch.ts CHANGED
@@ -1,30 +1,95 @@
1
- import { mediaType, MediaType } from './media-type';
2
- import { HttpError } from './http-error';
1
+ // Copyright 2020 Outfox, Inc.
2
+ //
3
+ // Licensed under the Apache License, Version 2.0 (the "License");
4
+ // you may not use this file except in compliance with the License.
5
+ // You may obtain a copy of the License at
6
+ //
7
+ // http://www.apache.org/licenses/LICENSE-2.0
8
+ //
9
+ // Unless required by applicable law or agreed to in writing, software
10
+ // distributed under the License is distributed on an "AS IS" BASIS,
11
+ // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ // See the License for the specific language governing permissions and
13
+ // limitations under the License.
14
+
15
+ import { ConstructableClassType } from './class-type';
16
+ import { MediaType } from './media-type';
3
17
  import { Problem } from './problem';
18
+ import { SundayError } from './sunday-error';
19
+ import { Base64 } from './util/base64';
20
+ import { errorToMessage } from './util/error';
4
21
 
5
22
  export async function validate(
6
23
  response: Response,
7
- dataExpected: boolean
24
+ dataExpected: boolean,
25
+ problemTypes?: Map<string, ConstructableClassType<Problem>>
8
26
  ): Promise<Response> {
9
27
  if (response.status < 200 || response.status >= 300) {
10
- if (
11
- mediaType(response.headers.get('content-type')) !== MediaType.PROBLEM_JSON
12
- ) {
13
- await response.body?.cancel?.();
14
- throw await HttpError.fromResponse(
15
- 'Unacceptable response status code',
16
- response
17
- );
28
+ const mediaType = MediaType.from(
29
+ response.headers.get('content-type'),
30
+ MediaType.OctetStream
31
+ );
32
+ const isProblemJSON = mediaType?.compatible(MediaType.ProblemJSON) ?? false;
33
+ if (!isProblemJSON) {
34
+ throw await Problem.fromResponse(response);
18
35
  }
19
36
 
20
37
  const problemData = await response.json();
21
-
22
- throw new Problem(problemData as Problem);
38
+ const problemType = problemTypes?.get(problemData.type) ?? Problem;
39
+ throw new problemType(problemData as Problem);
23
40
  }
24
41
 
25
42
  if (dataExpected && (response.status === 204 || response.status === 205)) {
26
- throw await HttpError.fromResponse('Unexpected empty response', response);
43
+ throw await SundayError.fromResponse('Unexpected Empty Response', response);
27
44
  }
28
45
 
29
46
  return response;
30
47
  }
48
+
49
+ export namespace ResponseExample {
50
+ export function build(response: Response, bodyExample?: string): string {
51
+ return (
52
+ `HTTP/?.? ${response.status} ${response.statusText}\n` +
53
+ Array.from(response.headers.entries())
54
+ .map(([name, value]) => `${name}: ${value}\n`)
55
+ .join('') +
56
+ '\n' +
57
+ (bodyExample ?? '<none>')
58
+ );
59
+ }
60
+
61
+ export async function bodyExcerpt(
62
+ response: Response,
63
+ maxLength: number
64
+ ): Promise<[string, unknown]> {
65
+ let body: unknown;
66
+ let bodyExcerpt: string;
67
+ try {
68
+ if (response.headers.get('content-type')?.startsWith('text/')) {
69
+ const text = await response.text();
70
+ body = text;
71
+ if (text.length > maxLength) {
72
+ const exampleText = text.slice(0, maxLength);
73
+ const remainingTextLength = text.length - maxLength;
74
+ bodyExcerpt = `${exampleText}<<... ${remainingTextLength} characters>>`;
75
+ } else {
76
+ bodyExcerpt = text;
77
+ }
78
+ } else {
79
+ const blob = await response.blob();
80
+ body = blob;
81
+ if (blob.size > maxLength) {
82
+ bodyExcerpt = `<<binary data: ${blob.size} bytes>>`;
83
+ } else {
84
+ const dataSlice = await blob.slice(0, maxLength).arrayBuffer();
85
+ bodyExcerpt = Base64.encode(dataSlice);
86
+ }
87
+ }
88
+ } catch (error) {
89
+ // ignore errors
90
+ const message = errorToMessage(error);
91
+ body = bodyExcerpt = `<<error displaying response data: ${message}>>`;
92
+ }
93
+ return [bodyExcerpt, body];
94
+ }
95
+ }
@@ -0,0 +1,66 @@
1
+ // Copyright 2020 Outfox, Inc.
2
+ //
3
+ // Licensed under the Apache License, Version 2.0 (the "License");
4
+ // you may not use this file except in compliance with the License.
5
+ // You may obtain a copy of the License at
6
+ //
7
+ // http://www.apache.org/licenses/LICENSE-2.0
8
+ //
9
+ // Unless required by applicable law or agreed to in writing, software
10
+ // distributed under the License is distributed on an "AS IS" BASIS,
11
+ // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ // See the License for the specific language governing permissions and
13
+ // limitations under the License.
14
+
15
+ export namespace HeaderParameters {
16
+ export function encode(
17
+ parameters?: Record<string, unknown>
18
+ ): [string, string][] {
19
+ if (parameters == null) {
20
+ return [];
21
+ }
22
+
23
+ const headers: [string, string][] = [];
24
+
25
+ Object.entries(parameters).forEach(([name, parameter]) => {
26
+ for (const value of encodeParam(name, parameter)) {
27
+ headers.push([name, value]);
28
+ }
29
+ });
30
+
31
+ return headers;
32
+ }
33
+
34
+ function encodeParam(name: string, value: unknown): string[] {
35
+ if (value == null) {
36
+ return [];
37
+ }
38
+
39
+ if (value instanceof Array) {
40
+ const result: string[] = [];
41
+
42
+ for (const item of value) {
43
+ if (item == null) {
44
+ continue;
45
+ }
46
+
47
+ result.push(validate(name, `${item}`));
48
+ }
49
+
50
+ return result;
51
+ }
52
+
53
+ return [validate(name, `${value}`)];
54
+ }
55
+
56
+ const asciiRegex = /^[\x20-\x7F]*$/;
57
+
58
+ function validate(name: string, value: string): string {
59
+ if (!asciiRegex.test(value)) {
60
+ throw new Error(
61
+ `The encoded header value contains one or more invalid characters: header=${name}, value=${value}`
62
+ );
63
+ }
64
+ return value;
65
+ }
66
+ }
package/src/index.ts CHANGED
@@ -1,3 +1,17 @@
1
+ // Copyright 2020 Outfox, Inc.
2
+ //
3
+ // Licensed under the Apache License, Version 2.0 (the "License");
4
+ // you may not use this file except in compliance with the License.
5
+ // You may obtain a copy of the License at
6
+ //
7
+ // http://www.apache.org/licenses/LICENSE-2.0
8
+ //
9
+ // Unless required by applicable law or agreed to in writing, software
10
+ // distributed under the License is distributed on an "AS IS" BASIS,
11
+ // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ // See the License for the specific language governing permissions and
13
+ // limitations under the License.
14
+
1
15
  export * from './any-type';
2
16
  export * from './class-type';
3
17
  export * from './request-factory';
@@ -5,19 +19,19 @@ export * from './fetch-request-factory';
5
19
  export * from './fetch-event-source';
6
20
  export * from './url-template';
7
21
  export * from './media-type';
8
- export * from './media-type-decoder';
9
- export * from './media-type-decoders';
10
- export * from './media-type-encoder';
11
- export * from './media-type-encoders';
12
- export * from './json-decoder';
13
- export * from './json-encoder';
14
- export * from './cbor-decoder';
15
- export * from './cbor-encoder';
16
- export * from './binary-decoder';
17
- export * from './binary-encoder';
18
- export * from './url-encoder';
22
+ export * from './media-type-codecs/media-type-decoder';
23
+ export * from './media-type-codecs/media-type-decoders';
24
+ export * from './media-type-codecs/media-type-encoder';
25
+ export * from './media-type-codecs/media-type-encoders';
26
+ export * from './media-type-codecs/json-decoder';
27
+ export * from './media-type-codecs/json-encoder';
28
+ export * from './media-type-codecs/cbor-decoder';
29
+ export * from './media-type-codecs/cbor-encoder';
30
+ export * from './media-type-codecs/binary-decoder';
31
+ export * from './media-type-codecs/binary-encoder';
32
+ export * from './media-type-codecs/www-form-url-encoder';
19
33
  export * from './logger';
20
- export * from './http-error';
34
+ export * from './sunday-error';
21
35
  export * from './problem';
22
36
  export * from './date-time-types';
23
37
 
package/src/logger.ts CHANGED
@@ -1,3 +1,17 @@
1
+ // Copyright 2020 Outfox, Inc.
2
+ //
3
+ // Licensed under the Apache License, Version 2.0 (the "License");
4
+ // you may not use this file except in compliance with the License.
5
+ // You may obtain a copy of the License at
6
+ //
7
+ // http://www.apache.org/licenses/LICENSE-2.0
8
+ //
9
+ // Unless required by applicable law or agreed to in writing, software
10
+ // distributed under the License is distributed on an "AS IS" BASIS,
11
+ // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ // See the License for the specific language governing permissions and
13
+ // limitations under the License.
14
+
1
15
  export interface Logger {
2
16
  trace?(...data: unknown[]): void;
3
17
  debug?(...data: unknown[]): void;
@@ -0,0 +1,38 @@
1
+ // Copyright 2020 Outfox, Inc.
2
+ //
3
+ // Licensed under the Apache License, Version 2.0 (the "License");
4
+ // you may not use this file except in compliance with the License.
5
+ // You may obtain a copy of the License at
6
+ //
7
+ // http://www.apache.org/licenses/LICENSE-2.0
8
+ //
9
+ // Unless required by applicable law or agreed to in writing, software
10
+ // distributed under the License is distributed on an "AS IS" BASIS,
11
+ // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ // See the License for the specific language governing permissions and
13
+ // limitations under the License.
14
+
15
+ import { MediaTypeDecoder } from './media-type-decoder';
16
+ import { AnyConstructableType } from '../any-type';
17
+
18
+ export class BinaryDecoder implements MediaTypeDecoder {
19
+ static default = new BinaryDecoder();
20
+
21
+ async decode<T>(response: Response, type: AnyConstructableType): Promise<T> {
22
+ const arrayBuffer = await response.arrayBuffer();
23
+
24
+ if (type[0] === ArrayBuffer) {
25
+ return arrayBuffer as unknown as T;
26
+ } else if (
27
+ type[0] === Uint8Array ||
28
+ type[0] === Int8Array ||
29
+ type[0] === DataView
30
+ ) {
31
+ return new type[0](arrayBuffer) as T;
32
+ }
33
+
34
+ throw Error(
35
+ 'Invalid value, expected ArrayBuffer, (Int|Uint)Array or DataView'
36
+ );
37
+ }
38
+ }
@@ -0,0 +1,33 @@
1
+ // Copyright 2020 Outfox, Inc.
2
+ //
3
+ // Licensed under the Apache License, Version 2.0 (the "License");
4
+ // you may not use this file except in compliance with the License.
5
+ // You may obtain a copy of the License at
6
+ //
7
+ // http://www.apache.org/licenses/LICENSE-2.0
8
+ //
9
+ // Unless required by applicable law or agreed to in writing, software
10
+ // distributed under the License is distributed on an "AS IS" BASIS,
11
+ // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ // See the License for the specific language governing permissions and
13
+ // limitations under the License.
14
+
15
+ import { MediaTypeEncoder } from './media-type-encoder';
16
+
17
+ export class BinaryEncoder implements MediaTypeEncoder {
18
+ static default = new BinaryEncoder();
19
+
20
+ encode(value: unknown): BodyInit {
21
+ if (
22
+ !ArrayBuffer.isView(value) &&
23
+ !(value instanceof ArrayBuffer) &&
24
+ !(value instanceof Blob) &&
25
+ !(value instanceof ReadableStream)
26
+ ) {
27
+ throw Error(
28
+ 'Invalid value, expected BufferSource, Blob or ReadableStream'
29
+ );
30
+ }
31
+ return value;
32
+ }
33
+ }