openapi_generate_typescript_fetch 0.1.0

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.
@@ -0,0 +1,844 @@
1
+ <%=
2
+ Gen.output.config = Gen.x.cfg['ts_indentation']
3
+ Gen.x.generator_info
4
+ %>
5
+
6
+ import {expect} from 'chai';
7
+ import type {CallExtras, String2String, EncodedBodyType, EncoderFunction, <%= Gen.x.schemas.import_types.join(', ') %>} from '../src/schemas.d.ts';
8
+ import {<%= Gen.x.schemas.imports.join(', ') %>} from '../src/schemas.js';
9
+ import type {UrlRequestInit, RequestInitModifier, PrepareArgs, <%= Gen.x.callclasses.import_types.join(', ') %>} from '../src/callclasses.d.ts';
10
+ import {isUrlRequestInit, RequestError, ResponseError, <%= Gen.x.callclasses.imports.join(', ') %>} from '../src/callclasses.js';
11
+ import {<%= Gen.x.makers.functions.sort.join(', ') %>} from './makers.js';
12
+ import { shared } from '../src/shared.js';
13
+
14
+
15
+ describe('Not generated', function () {
16
+ describe('isUrlRequestInit', function () {
17
+ it('valids pass', function () {
18
+ expect(isUrlRequestInit({ url: 'any', ri: {}})).to.be.true;
19
+ expect(isUrlRequestInit({ url: 'https://example.com', ri: { any: 'object' }})).to.be.true;
20
+ })
21
+ it('invalids', function () {
22
+ expect(isUrlRequestInit(null)).to.be.false;
23
+ expect(isUrlRequestInit({})).to.be.false;
24
+ expect(isUrlRequestInit({ notUrl: 'string', any: {} })).to.be.false;
25
+ expect(isUrlRequestInit({ url: 345, ri: {} })).to.be.false;
26
+ expect(isUrlRequestInit({ url: 'https://valid.looking', ri: 'invalid' })).to.be.false;
27
+ expect(isUrlRequestInit(3)).to.be.false;
28
+ expect(isUrlRequestInit('invalid')).to.be.false;
29
+ expect(isUrlRequestInit(true)).to.be.false;
30
+ })
31
+ })
32
+ })
33
+
34
+ // Caller class tests.
35
+
36
+ <%=
37
+ out = []
38
+ Gen.doc['paths'].each do |path, path_item_object|
39
+ oos = OpenAPISourceTools::ApiObjects.operation_objects(path_item_object)
40
+ oos.each do |method, operation_object|
41
+ oid = operation_object['operationId']
42
+ next if oid.nil?
43
+ clsName = operation_object[:name]
44
+ clsRespName = operation_object[:resp_name]
45
+ clsReqError = operation_object[:req_error]
46
+ paramless = operation_object[:parameterless]
47
+ req = operation_object[:request]
48
+ has_encoder = req[:has_encoder]
49
+ paraschemas = operation_object[:paraschemas]
50
+ out.push("describe('#{oid} #{clsName}', function () {")
51
+ s = <<EOB
52
+ it('setExtras headers', function () {
53
+ const x: CallExtras = {
54
+ headers: { arbitrary: 'stringvalue' }
55
+ };
56
+ const r: #{clsName} = new #{clsName}();
57
+ expect(Object.is(r, r.setExtras(x))).to.be.true;
58
+ expect(r.extras).to.deep.equal(x);
59
+ const x2: CallExtras = {
60
+ headers: { secondary: 'valuestring' }
61
+ };
62
+ expect(Object.is(r, r.setExtras(x2))).to.be.true;
63
+ expect(r.extras).to.have.property('headers');
64
+ expect(r.extras!.headers).to.have.property('arbitrary', x.headers!.arbitrary);
65
+ expect(r.extras!.headers).to.have.property('secondary', x2.headers!.secondary);
66
+ });
67
+ it('setExtras query', function () {
68
+ const x: CallExtras = {
69
+ query: { arbitrary: 'stringvalue' }
70
+ };
71
+ const r: #{clsName} = new #{clsName}();
72
+ expect(Object.is(r, r.setExtras(x))).to.be.true;
73
+ expect(r.extras).to.deep.equal(x);
74
+ const x2: CallExtras = {
75
+ query: { secondary: 'valuestring' }
76
+ };
77
+ expect(Object.is(r, r.setExtras(x2))).to.be.true;
78
+ expect(r.extras).to.have.property('query');
79
+ expect(r.extras!.query).to.have.property('arbitrary', x.query!.arbitrary);
80
+ expect(r.extras!.query).to.have.property('secondary', x2.query!.secondary);
81
+ });
82
+ it('setExtras headers and query', function () {
83
+ const x: CallExtras = {
84
+ headers: { arbitrary: 'stringvalue' },
85
+ query: { param: 'value' }
86
+ };
87
+ const r: #{clsName} = new #{clsName}();
88
+ expect(Object.is(r, r.setExtras(x))).to.be.true;
89
+ expect(r.extras).to.deep.equal(x);
90
+ const x2: CallExtras = {
91
+ headers: { secondary: 'valuestring' },
92
+ query: { other: 'string' }
93
+ };
94
+ expect(Object.is(r, r.setExtras(x2))).to.be.true;
95
+ expect(r.extras).to.have.property('headers');
96
+ expect(r.extras!.headers).to.have.property('arbitrary', x.headers!.arbitrary);
97
+ expect(r.extras!.headers).to.have.property('secondary', x2.headers!.secondary);
98
+ expect(r.extras).to.have.property('query');
99
+ expect(r.extras!.query).to.have.property('param', x.query!.param);
100
+ expect(r.extras!.query).to.have.property('other', x2.query!.other);
101
+ });
102
+ it('setModifier', function () {
103
+ const modder = (ur: UrlRequestInit) => {};
104
+ const r: #{clsName} = new #{clsName}();
105
+ expect(r.modifier).to.be.undefined;
106
+ expect(Object.is(r, r.setModifier(modder))).to.be.true;
107
+ expect(r.modifier).to.equal(modder);
108
+ });
109
+ it('setServerUrl', function () {
110
+ const su = 'serverurl';
111
+ const r: #{clsName} = new #{clsName}();
112
+ expect(r.serverUrl).to.be.undefined;
113
+ expect(Object.is(r, r.setServerUrl(su))).to.be.true;
114
+ expect(r.serverUrl).to.equal(su);
115
+ });
116
+ EOB
117
+ out.push(s)
118
+ if has_encoder
119
+ s = <<EOB
120
+ it('setEncoder', function () {
121
+ const enc = (body: any): [EncodedBodyType, string, number, String2String] => { return ['encodedBody', 'somevalue', 10, {} as String2String]; }
122
+ const r: #{clsName} = new #{clsName}();
123
+ expect(r.encoder).to.be.undefined;
124
+ expect(Object.is(r, r.setEncoder(enc))).to.be.true;
125
+ expect(r.encoder).to.equal(enc);
126
+ });
127
+ EOB
128
+ out.push(s)
129
+ end
130
+ if paramless
131
+ [ 'instance', clsName, 'shared' ].each do |source|
132
+ [ true, false ].each do |signalable|
133
+ [ true, false ].each do |create|
134
+ [ true, false ].each do |precreate|
135
+ s = <<EOB
136
+ it('#{signalable ? 'A' : 'Not a'}bort signalable via #{source}, #{create ? '' : 'do not '}create signal#{precreate ? ', added before' : ''}', function () {
137
+ const instance: #{clsName} = new #{clsName}();
138
+ #{precreate ? '' : '// '}instance.abortController = new AbortController();
139
+ #{clsName}.abortSignalable = undefined;
140
+ #{source}.abortSignalable = #{signalable};
141
+ const p: UrlRequestInit = instance.prepare(#{create ? '{ createAbortSignal: true }' : ''});
142
+ shared.abortSignalable = false;
143
+ expect(isUrlRequestInit(p)).to.be.true;
144
+ expect(Object.is(instance.urlRequestInit, p)).to.be.true;
145
+ expect(instance.abortController).#{signalable || create || precreate ? 'not.' : ''}to.be.null;
146
+ #{signalable || create || precreate ? '' : '// '}expect(Object.is(p.ri.signal, instance.abortController!.signal)).to.be.true;
147
+ #{signalable || create || precreate ? '// ' : ''}expect(p.ri.signal).to.be.undefined;
148
+ });
149
+ EOB
150
+ out.push(s)
151
+ end
152
+ end
153
+ end
154
+ red = (source == 'instance') ? 'error' : 'manual'
155
+ s = <<EOB
156
+ it('Modifier via #{source}', function () {
157
+ const modder = (ur: UrlRequestInit) => { ur.ri.redirect = '#{red}'; };
158
+ const instance: #{clsName} = new #{clsName}();
159
+ #{source}.#{source == 'instance' ? 'setModifier(modder)' : 'modifier = modder'};
160
+ const p: UrlRequestInit = instance.prepare();
161
+ shared.modifier = null;
162
+ expect(isUrlRequestInit(p)).to.be.true;
163
+ expect(Object.is(instance.urlRequestInit, p)).to.be.true;
164
+ expect(p.ri).to.have.property('redirect', '#{red}');
165
+ });
166
+ EOB
167
+ out.push(s)
168
+ ident = 'a'
169
+ value = 'b'
170
+ s = <<EOB
171
+ it('Extra query parameter via #{source}', function () {
172
+ const xt: CallExtras = { query: { #{ident}: '#{value}' }};
173
+ const instance: #{clsName} = new #{clsName}();
174
+ #{source}.#{source == 'instance' ? 'setExtras(xt)' : 'extras = xt'};
175
+ const p: UrlRequestInit = instance.prepare();
176
+ #{clsName}.extras = {};
177
+ shared.extras = {};
178
+ expect(p.url.endsWith(`${encodeURIComponent('#{ident}')}=${encodeURIComponent('#{value}')}`)).to.be.true;
179
+ });
180
+ EOB
181
+ out.push(s)
182
+ end
183
+ else
184
+ param_info = paraschemas.map do |ps|
185
+ {
186
+ name: ps[:param][:name],
187
+ in: ps[:param]['in'], # Where to look from: path, query, header.
188
+ schema: ps[:schema],
189
+ required: ps[:param].fetch('required', false),
190
+ maker: ps[:schema][:pass].first
191
+ }
192
+ end
193
+ s = <<EOB
194
+ it('Missing parameters', function () {
195
+ const r: #{clsName} = new #{clsName}();
196
+ expect(() => r.prepare()).to.throw(#{clsReqError}, 'parameters do not conform');
197
+ });
198
+ EOB
199
+ out.push(s)
200
+ expects = param_info.select { |p| p[:required] }.map do |pi|
201
+ case pi[:in]
202
+ when 'path' then "if (params.#{pi[:name]} !== undefined) expect(p.url.includes(params.#{pi[:name]})).to.be.true;"
203
+ when 'query' then "if (params.#{pi[:name]} !== undefined) expect(p.url.includes(`${encodeURIComponent(#{pi[:name]})}=${encodeURIComponent(params.#{pi[:name]})}`)).to.be.true;"
204
+ when 'header' then "if (params.#{pi[:name]} !== undefined) expect((p.ri.headers! as Headers).get('#{pi[:name]}')).to.equal(params.#{pi[:name]});"
205
+ else
206
+ "// #{clsName} parameter #{pi[:name]} located in #{pi[:in]} not tested."
207
+ end
208
+ end
209
+ # Loop over all pass functions.
210
+ operation_object[:pass].each do |fn|
211
+ s = <<EOB
212
+ it('Pass using #{fn}', function () {
213
+ const params: #{operation_object[:args_name]} = #{fn}();
214
+ const r: #{clsName} = new #{clsName}(params);
215
+ const p: UrlRequestInit = r.prepare();
216
+ expect(isUrlRequestInit(p)).to.be.true;
217
+ expect(Object.is(r.urlRequestInit, p)).to.be.true;
218
+ #{expects.compact.join("\n")}
219
+ const keys = Object.keys(params);
220
+ if (!keys.includes('encoded')) {
221
+ const others = keys.filter((name: string): boolean => name.startsWith('body') && name != 'body');
222
+ if (keys.includes('body')) {
223
+ expect(p.ri).to.have.property('body');
224
+ const pr: object = params as object;
225
+ for (const other of others)
226
+ expect(Object.is(pr['body' as keyof object], pr[other as keyof object])).to.be.true;
227
+ } else if (0 < others.length) {
228
+ expect(others).to.have.lengthOf(1);
229
+ expect(p.ri).to.have.property('body');
230
+ }
231
+ } else {
232
+ expect(p.ri).to.have.property('body');
233
+ }
234
+ });
235
+ EOB
236
+ out.push(s)
237
+ end
238
+ fn = operation_object[:pass].first
239
+ unless fn.nil?
240
+ [ true, false ].each do |signalable|
241
+ [ true, false ].each do |create|
242
+ [ true, false ].each do |precreate|
243
+ [ 'instance', clsName, 'shared' ].each do |source|
244
+ s = <<EOB
245
+ it('#{signalable ? 'A' : 'Not a'}bort signalable via #{source}, #{create ? '' : 'do not '}create signal#{precreate ? ', added before' : ''}', function () {
246
+ const params: #{operation_object[:args_name]} = #{fn}();
247
+ const instance: #{clsName} = new #{clsName}(params);
248
+ #{precreate ? '' : '// '}instance.abortController = new AbortController();
249
+ #{source}.abortSignalable = #{signalable};
250
+ const p: UrlRequestInit = instance.prepare({ createAbortSignal: #{create} });
251
+ shared.abortSignalable = false;
252
+ expect(isUrlRequestInit(p)).to.be.true;
253
+ expect(Object.is(instance.urlRequestInit, p)).to.be.true;
254
+ #{expects.compact.join("\n")}
255
+ expect(instance.abortController).#{signalable || create || precreate ? 'not.' : ''}to.be.null;
256
+ #{signalable || create || precreate ? '' : '// '}expect(Object.is(p.ri.signal, instance.abortController!.signal)).to.be.true;
257
+ #{signalable || create || precreate ? '// ' : ''}expect(p.ri.signal).to.be.undefined;
258
+ });
259
+ EOB
260
+ out.push(s)
261
+ end
262
+ end
263
+ end
264
+ end
265
+ [ 'instance', clsName, 'shared' ].each do |source|
266
+ red = (source == 'instance') ? 'error' : 'manual'
267
+ s = <<EOB
268
+ it('Modifier via #{source}', function () {
269
+ const modder = (ur: UrlRequestInit) => { ur.ri.redirect = '#{red}'; };
270
+ const params: #{operation_object[:args_name]} = #{fn}();
271
+ const instance: #{clsName} = new #{clsName}(params);
272
+ #{source}.#{source == 'instance' ? 'setModifier(modder)' : 'modifier = modder'};
273
+ const p: UrlRequestInit = instance.prepare();
274
+ shared.modifier = null;
275
+ expect(isUrlRequestInit(p)).to.be.true;
276
+ expect(Object.is(instance.urlRequestInit, p)).to.be.true;
277
+ expect(p.ri).to.have.property('redirect', '#{red}');
278
+ });
279
+ EOB
280
+ out.push(s)
281
+ end
282
+ count = param_info.select { |p| p[:in] == 'query' }.map { |p| p[:name].size }.max || 1
283
+ ident = 'a' * (count + 1)
284
+ value = 'b' * (count + 1)
285
+ [ 'instance', clsName, 'shared' ].each do |source|
286
+ s = <<EOB
287
+ it('Extra query parameter via #{source}', function () {
288
+ const xt: CallExtras = { query: { #{ident}: '#{value}' }};
289
+ const params: #{operation_object[:args_name]} = #{fn}();
290
+ const instance: #{clsName} = new #{clsName}(params);
291
+ #{source}.#{source == 'instance' ? 'setExtras(xt)' : 'extras = xt'};
292
+ const p: UrlRequestInit = instance.prepare();
293
+ shared.extras = {};
294
+ expect(p.url.includes(`${encodeURIComponent('#{ident}')}=${encodeURIComponent('#{value}')}`)).to.be.true;
295
+ });
296
+ EOB
297
+ out.push(s)
298
+ end
299
+ if has_encoder
300
+ s = <<EOB
301
+ it('No encoder', function () {
302
+ const params: #{operation_object[:args_name]} = #{fn}();
303
+ const r: #{clsName} = new #{clsName}(params);
304
+ const enc = #{clsName}.encoder;
305
+ #{clsName}.encoder = null;
306
+ shared.encoder = null;
307
+ expect(() => r.prepare()).to.throw(#{clsReqError}, 'no request body encoder');
308
+ #{clsName}.encoder = enc;
309
+ });
310
+ EOB
311
+ out.push(s)
312
+ [ 'instance', clsName, 'shared' ].each do |source|
313
+ s = <<EOB
314
+ it('Encoder via #{source}', function () {
315
+ const encBody = JSON.stringify('body from #{source} encoder');
316
+ const enc = (body: any): [EncodedBodyType, string, number, String2String] => { return [encBody, 'application/json', encBody.length, {} as String2String]; }
317
+ const params: #{operation_object[:args_name]} = #{fn}();
318
+ const instance: #{clsName} = new #{clsName}(params);
319
+ const original = #{clsName}.encoder;
320
+ #{clsName}.encoder = null;
321
+ shared.encoder = null;
322
+ #{source}.#{source == 'instance' ? 'setEncoder(enc)' : 'encoder = enc'};
323
+ const uri = instance.prepare();
324
+ expect(uri.ri).to.have.property('body', encBody);
325
+ #{clsName}.encoder = original;
326
+ });
327
+ EOB
328
+ out.push(s)
329
+ end
330
+ [ 'instance', clsName ].each do |source|
331
+ s = <<EOB
332
+ it('Encoder via #{source}, overriding shared', function () {
333
+ const encBody = JSON.stringify('body from #{source} encoder');
334
+ const enc = (body: any): [EncodedBodyType, string, number, String2String] => { return [encBody, 'application/json', encBody.length, {} as String2String]; }
335
+ const sharedEnc = (body: any): [EncodedBodyType, string, number, String2String] => { return ['wrong', 'application/json', 5, {} as String2String]; }
336
+ const params: #{operation_object[:args_name]} = #{fn}();
337
+ const instance: #{clsName} = new #{clsName}(params);
338
+ const original = #{clsName}.encoder;
339
+ #{clsName}.encoder = null;
340
+ shared.encoder = sharedEnc;
341
+ #{source}.#{source == 'instance' ? 'setEncoder(enc)' : 'encoder = enc'};
342
+ const uri = instance.prepare();
343
+ expect(uri.ri).to.have.property('body', encBody);
344
+ #{clsName}.encoder = original;
345
+ shared.encoder = null;
346
+ });
347
+ EOB
348
+ out.push(s)
349
+ end
350
+ s = <<EOB
351
+ it('Encoder via instance, overriding #{clsName} and shared', function () {
352
+ const encBody = JSON.stringify('body from instance encoder');
353
+ const enc = (body: any): [EncodedBodyType, string, number, String2String] => { return [encBody, 'application/json', encBody.length, {} as String2String]; }
354
+ const sharedEnc = (body: any): [EncodedBodyType, string, number, String2String] => { return ['wrong', 'application/json', 5, {} as String2String]; }
355
+ const params: #{operation_object[:args_name]} = #{fn}();
356
+ const instance: #{clsName} = new #{clsName}(params);
357
+ shared.encoder = sharedEnc;
358
+ instance.setEncoder(enc);
359
+ const uri = instance.prepare();
360
+ expect(uri.ri).to.have.property('body', encBody);
361
+ shared.encoder = null;
362
+ });
363
+ EOB
364
+ out.push(s)
365
+ s = <<EOB
366
+ it('Encoder that produces invalid content-type', function () {
367
+ const enc = (body: any): [EncodedBodyType, string, number, String2String] => { return ['encodedBody', 'somevalue', 10, {} as String2String]; }
368
+ const params: #{operation_object[:args_name]} = #{fn}();
369
+ const r: #{clsName} = new #{clsName}(params).setEncoder(enc);
370
+ expect(() => r.prepare()).to.throw(#{clsReqError}, 'encoded content-type');
371
+ });
372
+ EOB
373
+ out.push(s)
374
+ s = <<EOB
375
+ it('Encoder adds other headers', function () {
376
+ const params: #{operation_object[:args_name]} = #{fn}();
377
+ const enc = (body: any): [EncodedBodyType, string, number, String2String] => {
378
+ for (const key in Object.keys(params)) {
379
+ if (key.startsWith('body'))
380
+ return ['encodedBody', #{clsName}.body2mediaTypes.get(key)![0], 11, { headerName: 'headerValue' }];
381
+ }
382
+ return ['encodedBody', #{clsName}.allBodyMediaTypes[0], 11, { headerName: 'headerValue' }];
383
+ }
384
+ const r: #{clsName} = new #{clsName}(params).setEncoder(enc);
385
+ const p: UrlRequestInit = r.prepare();
386
+ expect((p.ri.headers! as Headers).get('headerName')).to.equal('headerValue');
387
+ });
388
+ EOB
389
+ out.push(s)
390
+ s = <<EOB
391
+ it('No body to encode', function () {
392
+ const params: Partial<#{operation_object[:args_name]}> = #{fn}();
393
+ for (const key of Object.keys(params)) {
394
+ if (key.startsWith('body'))
395
+ delete params[key as keyof #{operation_object[:args_name]}];
396
+ }
397
+ const r: #{clsName} = new #{clsName}(params as #{operation_object[:args_name]});
398
+ expect(() => r.prepare({ ignoreMismatches: true })).to.throw(#{clsReqError}, 'no body present');
399
+ });
400
+ EOB
401
+ out.push(s)
402
+ tgt = Gen.x.cfg.fetch('target', 'browser')
403
+ if tgt == 'browser'
404
+ enc_arr = 'new Uint8Array([1, 2, 3, 4, 5, 6, 7, 8])'
405
+ elsif tgt == 'node'
406
+ enc_arr = "Buffer.from('encodedBody')"
407
+ end
408
+ s = <<EOB
409
+ it('Already encoded, same instance', function () {
410
+ const params: #{operation_object[:args_name]} = #{fn}();
411
+ for (const key of Object.keys(params)) {
412
+ if (key.startsWith('body'))
413
+ delete params[key as keyof #{operation_object[:args_name]}];
414
+ }
415
+ params.encoded = #{enc_arr};
416
+ const r: #{clsName} = new #{clsName}(params);
417
+ const p: UrlRequestInit = r.prepare();
418
+ expect(Object.is(params.encoded, p.ri.body)).to.be.true;
419
+ });
420
+ EOB
421
+ out.push(s)
422
+ end
423
+ end
424
+ operation_object[:fail].each do |fn|
425
+ s = <<EOB
426
+ it('Fail using #{fn}', function () {
427
+ const params: #{operation_object[:args_name]} = #{fn}();
428
+ const r: #{clsName} = new #{clsName}(params);
429
+ expect(() => r.prepare()).to.throw(#{clsReqError}, 'parameters do not conform');
430
+ });
431
+ EOB
432
+ out.push(s)
433
+ end
434
+ operation_object[:typefail].each do |fn|
435
+ s = <<EOB
436
+ it('Fail using #{fn}', function () {
437
+ const params: #{operation_object[:args_name]} = #{fn}();
438
+ const r: #{clsName} = new #{clsName}(params);
439
+ expect(() => r.prepare()).to.throw(#{clsReqError}, 'parameters do not conform');
440
+ });
441
+ EOB
442
+ out.push(s)
443
+ end
444
+ operation_object[:throw].each do |fn|
445
+ s = <<EOB
446
+ it('Fail using #{fn}', function () {
447
+ const params: #{operation_object[:args_name]} = #{fn}();
448
+ const r: #{clsName} = new #{clsName}(params);
449
+ expect(() => r.prepare()).to.throw(Error, 'body');
450
+ });
451
+ EOB
452
+ out.push(s)
453
+ end
454
+ end
455
+ out.push('});')
456
+ end
457
+ end
458
+ # Call mock response could be set up to provide desired response.
459
+ Gen.output.join(out)
460
+ %>
461
+
462
+ // Response class tests.
463
+
464
+ const utf8encoder = new TextEncoder();
465
+
466
+ function bodyData(contentType: string, item: any): [Uint8Array, string, number] {
467
+ const content: Uint8Array = utf8encoder.encode(JSON.stringify(item));
468
+ if (contentType == 'application/octet-stream') {
469
+ return [content, contentType, content.length];
470
+ }
471
+ return [content, `${contentType}; charset=utf-8`, content.length];
472
+ }
473
+
474
+ <%=
475
+ out = []
476
+ Gen.doc['paths'].each do |path, path_item_object|
477
+ oos = OpenAPISourceTools::ApiObjects.operation_objects(path_item_object)
478
+ oos.each do |method, operation_object|
479
+ oid = operation_object['operationId']
480
+ next if oid.nil?
481
+ clsName = operation_object[:name]
482
+ clsRespName = operation_object[:resp_name]
483
+ clsRespError = operation_object[:resp_error]
484
+ out.push("describe('#{oid} #{clsRespName}', function () {")
485
+ unless operation_object.key?(:responses)
486
+ warn("#{path} #{method} no response")
487
+ next
488
+ end
489
+ status_order = operation_object[:responses][:status_order]
490
+ code2content = operation_object[:responses][:code2content]
491
+ # Figure out what code to use in case there are ranges or default.
492
+ has_default = false
493
+ ranges = {}
494
+ code2content.keys.each do |code|
495
+ dc = code.downcase
496
+ if dc == 'default'
497
+ has_default = true
498
+ ranges[code] = { key: code, code: dc, low: 100, high: 599 }
499
+ elsif dc.end_with?('xx')
500
+ hundreds = dc[0..-2].to_i * 100
501
+ ranges[code] = { key: code, code: dc, low: hundreds, high: hundreds + 99 }
502
+ elsif dc.end_with?('x')
503
+ tens = dc[0..-1].to_i * 10
504
+ ranges[code] = { key: code, code: dc, low: tens, high: tens + 9 }
505
+ else
506
+ ones = dc.to_i
507
+ ranges[code] = { key: code, code: dc, low: ones, high: ones }
508
+ end
509
+ end
510
+ order = ranges.values.sort do |a, b|
511
+ d = (a[:high] - a[:low]) <=> (b[:high] - b[:low])
512
+ if d.zero?
513
+ d = a[:low] <=> b[:low]
514
+ if d.zero?
515
+ a[:high] <=> b[:high]
516
+ else
517
+ d
518
+ end
519
+ else
520
+ d
521
+ end
522
+ end
523
+ used_status_codes = Set.new
524
+ order.each do |info|
525
+ if info[:low] == info[:high]
526
+ used_status_codes.add(info[:low])
527
+ code2content[info[:key]][:status_code] = info[:low]
528
+ else
529
+ (info[:low]..info[:high]).each do |k|
530
+ next if used_status_codes.member?(k)
531
+ used_status_codes.add(k)
532
+ code2content[info[:key]][:status_code] = k
533
+ break
534
+ end
535
+ end
536
+ end
537
+ status_vars = code2content.values.map { |info| info[:status_code] }.sort!
538
+ has_599 = false
539
+ status_order.each do |key|
540
+ info = code2content[key]
541
+ status_code = info[:status_code]
542
+ has_599 = true if status_code == 599
543
+ [ nil, false, true ].each do |throwNon2xx|
544
+ [ nil, false, true ].each do |throwBodyMismatch|
545
+ [ :none, :fail, :pass, :typefail ].each do |body_kind|
546
+ # No body for this status code.
547
+ if info[:content].empty?
548
+ if body_kind == :fail || body_kind == :typefail
549
+ if throwNon2xx == true && status_code < 200 || 300 <= status_code
550
+ tst = <<EOF
551
+ try {
552
+ await #{clsRespName}.obtain(args);
553
+ expect(true).to.be.false;
554
+ }
555
+ catch (err) {
556
+ expect(err instanceof #{clsRespError}).to.be.true;
557
+ expect((err as Error).message).to.include('non-2xx');
558
+ }
559
+ EOF
560
+ elsif throwBodyMismatch == true
561
+ tst = <<EOF
562
+ try {
563
+ await #{clsRespName}.obtain(args);
564
+ expect(true).to.be.false;
565
+ }
566
+ catch (err) {
567
+ expect(err instanceof #{clsRespError}).to.be.true;
568
+ expect((err as Error).message).to.include('body mismatch');
569
+ }
570
+ EOF
571
+ else
572
+ tst = <<EOF
573
+ const out: #{clsRespName} = await #{clsRespName}.obtain(args);
574
+ expect(Object.is(out.response, resp)).to.be.true;
575
+ expect(Object.is(out.caller, args.caller)).to.be.true;
576
+ #{status_vars.map { |v| "expect(out.c#{v}).to.be.undefined;" }.join("\n")}
577
+ EOF
578
+ end
579
+ s = <<EOF
580
+ it('#{throwNon2xx ? 'throw non-2xx ' : ''}#{throwBodyMismatch ? 'throwBodyMismatch ' : ''}#{status_code} #{body_kind.to_s}', async function () {
581
+ const [bd, ct, cl] = bodyData('application/json', () => { return { any: 'fails' }; });
582
+ const hdrs = new Headers();
583
+ hdrs.set('content-type', ct);
584
+ hdrs.set('content-length', cl.toString());
585
+ const resp: Response = new Response(bd, {
586
+ status: #{status_code},
587
+ headers: hdrs
588
+ });
589
+ const args: #{clsRespName}Args = {
590
+ #{throwNon2xx.nil? ? '// ' : ''} #{throwNon2xx ? 'throwNon2xx: true,' : 'throwNon2xx: false,'}
591
+ #{throwBodyMismatch.nil? ? '// ' : ''} #{throwBodyMismatch ? 'throwBodyMismatch: true,' : 'throwBodyMismatch: false,'}
592
+ caller: { faker: 'caller' } as any as #{clsName},
593
+ response: resp
594
+ };
595
+ #{tst}
596
+ });
597
+ EOF
598
+ out.push(s) unless status_code == 204 # Response constructor throws.
599
+ elsif body_kind == :none
600
+ if throwNon2xx == true && status_code < 200 || 300 <= status_code
601
+ tst = <<EOF
602
+ try {
603
+ await #{clsRespName}.obtain(args);
604
+ expect(true).to.be.false;
605
+ }
606
+ catch (err) {
607
+ expect(err instanceof #{clsRespError}).to.be.true;
608
+ expect((err as Error).message).to.include('non-2xx');
609
+ }
610
+ EOF
611
+ else
612
+ tst = <<EOF
613
+ const out: #{clsRespName} = await #{clsRespName}.obtain(args);
614
+ expect(Object.is(out.response, resp)).to.be.true;
615
+ expect(Object.is(out.caller, args.caller)).to.be.true;
616
+ expect(out).to.have.property('#{info[:status_var]}').not.to.be.undefined;
617
+ #{status_vars.reject { |v| "c#{v}" == info[:status_var] }.map { |v| "expect(out.c#{v}).to.be.undefined;" }.join("\n")}
618
+ EOF
619
+ end
620
+ s = <<EOF
621
+ it('#{throwNon2xx ? 'throw non-2xx ' : ''}#{throwBodyMismatch ? 'throwBodyMismatch ' : ''}#{status_code} #{body_kind.to_s}', async function () {
622
+ const resp: Response = new Response(null, {
623
+ status: #{status_code}
624
+ });
625
+ const args: #{clsRespName}Args = {
626
+ #{throwNon2xx.nil? ? '// ' : ''}throwNon2xx: #{throwNon2xx ? 'true' : 'false'},
627
+ #{throwBodyMismatch.nil? ? '// ' : ''}throwBodyMismatch: #{throwBodyMismatch ? 'true' : 'false'},
628
+ caller: { fake: 'caller' } as any as #{clsName},
629
+ response: resp
630
+ };
631
+ #{tst}
632
+ });
633
+ EOF
634
+ out.push(s)
635
+ end
636
+ next
637
+ end
638
+ # Loop over all content types for this status code.
639
+ info[:content].keys.sort!.map do |ct|
640
+ if ct != 'application/json'
641
+ # Support for other types requires user-provided encoding function.
642
+ out.push("// No function to encode body to content-type #{ct}")
643
+ next
644
+ end
645
+ mr = info[:content][ct] # Has mto, rt (response type).
646
+ # rt[:type] is mto[:decoded] is deferenced mto['schema']
647
+ # Bad and good body in response.body.
648
+ # If need bad and no fail maker, skip.
649
+ # If need good and no pass maker, skip.
650
+ schema = mr[:rt][:type]
651
+ next if body_kind != :none && schema[body_kind].empty?
652
+ if body_kind == :pass
653
+ body_chk = <<EOF
654
+ expect(out.mismatch).to.equal(0);
655
+ expect(out).to.have.property('#{mr[:rt][:name]}');
656
+ expect(#{mr[:rt][:type][:is]}(out.#{mr[:rt][:name]})).to.be.true;
657
+ expect(out).to.have.property('#{info[:status_var]}');
658
+ expect(out.#{info[:status_var]}).to.have.property('#{mr[:rt][:name]}');
659
+ expect(Object.is(out.#{info[:status_var]}!.#{mr[:rt][:name]}, out.#{mr[:rt][:name]})).to.be.true;
660
+ EOF
661
+ else # Fail, typefail, and none all fail is-check.
662
+ body_chk = <<EOF
663
+ expect(out.mismatch).to.equal(2);
664
+ expect(out).not.to.have.property('#{info[:status_var]}');
665
+ EOF
666
+ end
667
+ # ct is content-type, pair with loader, ties with encoded body
668
+ # response.body gets a ReadableStream of the encoded body.
669
+ # response.headers gets content-type from ct.
670
+ # response.status is status_code as number if not already. Check default.
671
+ # Keep track of has_500. Ensure at least one is not ok.
672
+ # args.loader gets the loader based on ct.
673
+
674
+ # Default loaders must be covered. They will fail body checks.
675
+ # Then there is custom loader set in multiple levels.
676
+
677
+ # Does json() decode uft-8? One would hope so...
678
+ # Otherwise read blob, decode if content-encoding, proceed.
679
+ if throwNon2xx == true && (status_code < 200 || 300 <= status_code)
680
+ tst = <<EOF
681
+ try {
682
+ await #{clsRespName}.obtain(args);
683
+ expect(true).to.be.false;
684
+ }
685
+ catch (err) {
686
+ expect(err instanceof #{clsRespError}).to.be.true;
687
+ expect((err as Error).message).to.include('non-2xx');
688
+ }
689
+ EOF
690
+ elsif throwBodyMismatch == true && body_kind != :pass
691
+ tst = <<EOF
692
+ try {
693
+ await #{clsRespName}.obtain(args);
694
+ expect(true).to.be.false;
695
+ }
696
+ catch (err) {
697
+ expect(err instanceof #{clsRespError}).to.be.true;
698
+ expect((err as Error).message).to.include('body mismatch');
699
+ }
700
+ EOF
701
+ elsif body_kind != :pass
702
+ tst = <<EOF
703
+ const out: #{clsRespName} = await #{clsRespName}.obtain(args);
704
+ expect(Object.is(out.response, resp)).to.be.true;
705
+ expect(Object.is(out.caller, args.caller)).to.be.true;
706
+ #{status_vars.map { |v| "expect(out.c#{v}).to.be.undefined;" }.join("\n")}
707
+ EOF
708
+ else
709
+ tst = <<EOF
710
+ const out: #{clsRespName} = await #{clsRespName}.obtain(args);
711
+ expect(Object.is(out.response, resp)).to.be.true;
712
+ expect(Object.is(out.caller, args.caller)).to.be.true;
713
+ expect(out).to.have.property('#{info[:status_var]}').not.to.be.undefined;
714
+ #{status_vars.reject { |v| "c#{v}" == info[:status_var] }.map { |v| "expect(out.c#{v}).to.be.undefined;" }.join("\n")}
715
+ #{body_chk}
716
+ EOF
717
+ end
718
+ if body_kind == :none
719
+ prep = <<EOF
720
+ const [bd, ct, cl] = bodyData('#{ct}', {});
721
+ const hdrs = new Headers();
722
+ hdrs.set('content-type', ct);
723
+ hdrs.set('content-length', cl.toString());
724
+ const resp: Response = new Response(null, {
725
+ status: #{status_code},
726
+ headers: hdrs
727
+ });
728
+ EOF
729
+ else
730
+ mak = schema[body_kind].first
731
+ unless mak.nil?
732
+ prep = <<EOF
733
+ const [bd, ct, cl] = bodyData('#{ct}', #{mak}());
734
+ const hdrs = new Headers();
735
+ hdrs.set('content-type', ct);
736
+ hdrs.set('content-length', cl.toString());
737
+ const resp: Response = new Response(bd, {
738
+ status: #{status_code},
739
+ headers: hdrs
740
+ });
741
+ EOF
742
+ else
743
+ prep = nil
744
+ end
745
+ end
746
+ unless prep.nil?
747
+ s = <<EOF
748
+ it('#{throwNon2xx ? 'throw non-2xx ' : ''}#{throwBodyMismatch ? 'throwBodyMismatch ' : ''}#{status_code} #{body_kind.to_s}', async function () {
749
+ #{prep}
750
+ const args: #{clsRespName}Args = {
751
+ #{throwNon2xx.nil? ? '// ' : ''} #{throwNon2xx ? 'throwNon2xx: true,' : 'throwNon2xx: false,'}
752
+ #{throwBodyMismatch.nil? ? '// ' : ''} #{throwBodyMismatch ? 'throwBodyMismatch: true,' : 'throwBodyMismatch: false,'}
753
+ caller: { fake: 'caller' } as any as #{clsName},
754
+ response: resp
755
+ };
756
+ #{tst}
757
+ });
758
+ EOF
759
+ out.push(s)
760
+ end
761
+ end
762
+ end
763
+ end
764
+ end
765
+ next if info[:content].empty?
766
+ # Add content-type invalid header value test here. Otherwise like pass.
767
+ mr = info[:content][info[:content].keys.first]
768
+ schema = mr[:rt][:type]
769
+ ct = 'a' * (1 + info[:content].keys.map(&:size).max);
770
+ mak = schema[:pass].first
771
+ unless mak.nil?
772
+ s = <<EOF
773
+ it('#{status_code} unexpected header', async function () {
774
+ const [bd, ct, cl] = bodyData('#{ct}', #{mak}());
775
+ const hdrs = new Headers();
776
+ hdrs.set('content-type', ct);
777
+ hdrs.set('content-length', cl.toString());
778
+ const resp: Response = new Response(bd, {
779
+ status: #{status_code},
780
+ headers: hdrs
781
+ });
782
+ const args: #{clsRespName}Args = {
783
+ throwNon2xx: false,
784
+ throwBodyMismatch: false,
785
+ caller: { fake: 'caller' } as any as #{clsName},
786
+ response: resp
787
+ };
788
+ try {
789
+ await #{clsRespName}.obtain(args);
790
+ expect(true).to.be.false;
791
+ }
792
+ catch (err) {
793
+ expect(err instanceof #{clsRespError}).to.be.true;
794
+ expect((err as Error).message).to.include('unexpected content-type');
795
+ }
796
+ });
797
+ EOF
798
+ out.push(s)
799
+ end
800
+ end
801
+ unless has_default || has_599
802
+ [ nil, false, true ].each do |throwNon2xx|
803
+ status_code = 599
804
+ if throwNon2xx
805
+ tst = <<EOF
806
+ try {
807
+ await #{clsRespName}.obtain(args);
808
+ expect(true).to.be.false;
809
+ }
810
+ catch (err) {
811
+ expect(err instanceof #{clsRespError}).to.be.true;
812
+ expect((err as Error).message).to.include('non-2xx');
813
+ }
814
+ EOF
815
+ else
816
+ tst = <<EOF
817
+ const out: #{clsRespName} = await #{clsRespName}.obtain(args);
818
+ expect(Object.is(out.response, resp)).to.be.true;
819
+ expect(Object.is(out.caller, args.caller)).to.be.true;
820
+ #{status_vars.map { |v| "expect(out.c#{v}).to.be.undefined;" }.join("\n")}
821
+ expect(out).to.have.property('mismatch', 2);
822
+ EOF
823
+ end
824
+ s = <<EOF
825
+ it('#{throwNon2xx ? 'throw non-2xx ' : (throwNon2xx.nil? ? '' : 'no-throw ')}#{status_code}', async function () {
826
+ const resp: Response = new Response(null, {
827
+ status: #{status_code}
828
+ });
829
+ const args: #{clsRespName}Args = {
830
+ #{throwNon2xx.nil? ? '// ' : ''}throwNon2xx: #{throwNon2xx ? 'true' : 'false'},
831
+ caller: { fake: 'caller' } as any as #{clsName},
832
+ response: resp
833
+ };
834
+ #{tst}
835
+ });
836
+ EOF
837
+ out.push(s)
838
+ end
839
+ end
840
+ out.push('});')
841
+ end
842
+ end
843
+ Gen.output.join(out)
844
+ %>