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,1016 @@
1
+ <%=
2
+ Gen.output.config = Gen.x.cfg['ts_indentation']
3
+ Gen.x.generator_info
4
+ %>
5
+
6
+ import type {SchemaCheckers, String2Any} from './helpers.d.ts';
7
+ import {isSchema, baseSchema} from './helpers.js';
8
+
9
+ function unknown(x: unknown, name: string): Error {
10
+ if (isArray(x)) {
11
+ return new Error(`unknown of type array, length ${x.length}, is not ${name}`);
12
+ }
13
+ if (isObject(x)) {
14
+ const keyType: Array<string> = Object.keys(x).map((key) => `${key}: ${typeof x[key as keyof object]}`);
15
+ if (keyType.length == 0)
16
+ return new Error(`unknown of type object with no keys is not ${name}`);
17
+ if (keyType.length == 1)
18
+ return new Error(`unknown of type object { ${keyType[0] } is not ${name}`)
19
+ return new Error(`unknown of type object {\n ${keyType.join('\n ')}\n} is not ${name}`)
20
+ }
21
+ return new Error(`unknown of type ${typeof x} is not ${name}`);
22
+ }
23
+
24
+ // For completeness. Used by is/unknown2/base functions later.
25
+
26
+ export function isNot(x: unknown): x is any {
27
+ return false;
28
+ }
29
+
30
+ export function isString(x: unknown): x is string {
31
+ return typeof x == typeof '';
32
+ }
33
+
34
+ export function unknown2String(x: unknown): string {
35
+ if (isString(x))
36
+ return x as string;
37
+ throw unknown(x, 'string');
38
+ }
39
+
40
+ export function baseString(s: string): string {
41
+ return s;
42
+ }
43
+
44
+ export function isNumber(x: unknown): x is number {
45
+ return typeof x == typeof 1 && !Number.isNaN(x);
46
+ }
47
+
48
+ export function unknown2Number(x: unknown): number {
49
+ if (isNumber(x))
50
+ return x as number;
51
+ throw unknown(x, 'number');
52
+ }
53
+
54
+ export function baseNumber(n: number): number {
55
+ return n;
56
+ }
57
+
58
+ export function isBoolean(x: unknown): x is boolean {
59
+ return typeof x == typeof true;
60
+ }
61
+
62
+ export function unknown2Boolean(x: unknown): boolean {
63
+ if (isBoolean(x))
64
+ return x as boolean;
65
+ throw unknown(x, 'boolean');
66
+ }
67
+
68
+ export function baseBoolean(b: boolean): boolean {
69
+ return b;
70
+ }
71
+
72
+ export function baseArray(a: Array<any>): Array<any> {
73
+ const out = [];
74
+ for (const x of a) {
75
+ if (isArray(x))
76
+ out.push(baseArray(x as Array<any>));
77
+ else if (isObject(x))
78
+ out.push(baseObject(x as object));
79
+ else
80
+ out.push(x); // Boolean, string, number, null, or undefined maybe.
81
+ }
82
+ return out;
83
+ }
84
+
85
+ export function isArray(x: unknown): x is Array<any> {
86
+ return Array.isArray(x);
87
+ }
88
+
89
+ export function unknown2Array(x: unknown): Array<any> {
90
+ if (isArray(x))
91
+ return x as Array<any>;
92
+ throw unknown(x, 'array');
93
+ }
94
+
95
+ export function isObject(x: unknown): x is object {
96
+ return typeof x == typeof {} && x !== null && !Array.isArray(x);
97
+ }
98
+
99
+ export function unknown2Object(x: unknown): object {
100
+ if (isObject(x))
101
+ return x as object;
102
+ throw unknown(x, 'object');
103
+ }
104
+
105
+ export function baseObject(o: object): object {
106
+ const keys = Object.keys(o);
107
+ const out: String2Any = {};
108
+ for (const k of keys) {
109
+ const x: unknown = o[k as keyof object];
110
+ if (isArray(x))
111
+ out[k] = baseArray(x as Array<any>);
112
+ else if (isObject(x))
113
+ out[k] = baseObject(x as object);
114
+ else
115
+ out[k] = x;
116
+ }
117
+ return out;
118
+ }
119
+
120
+ export function isAny(a: any): a is any {
121
+ return true;
122
+ }
123
+
124
+ export function baseAny(a: any): any {
125
+ return a;
126
+ }
127
+
128
+ export function unknown2Any(x: unknown): any {
129
+ return x as any;
130
+ }
131
+
132
+ // Interfaces, types, and checker functions.
133
+ <%=
134
+ # Names first.
135
+ sis = Gen.x.order
136
+ sis.each do |si|
137
+ si.schema[:name] = si.name # For use later.
138
+ si.schema[:is] = "is#{LuckyCase.pascal_case(si.name)}"
139
+ si.schema[:unknown] = "unknown2#{si.name}"
140
+ si.schema[:base] = "base#{LuckyCase.pascal_case(si.name)}"
141
+ si.schema[:checkers] = "checkers#{LuckyCase.pascal_case(si.name)}"
142
+ si.schema[:obj] = OpenAPIGenerateTypeScriptFetch::ObjectSchema.new(si.schema) if si.schema['type'] == 'object'
143
+ end
144
+ # Output.
145
+ out = []
146
+ checker = []
147
+ sis = Gen.x.order
148
+ sis.each do |si|
149
+ Gen.x.schemas.functions.push(si.schema[:is])
150
+ Gen.x.schemas.functions.push(si.schema[:base])
151
+ Gen.x.schemas.functions.push(si.schema[:unknown])
152
+ comment = [ si.schema['summary'], si.schema['description'] ].compact!
153
+ unless comment.empty?
154
+ s = <<EOB
155
+ /* #{si.name}:
156
+ #{comment.join("\n\n")}
157
+ */
158
+ EOB
159
+ out.push(s)
160
+ end
161
+ case si.schema['type']
162
+ when 'object'
163
+ Gen.x.schemas.types.push(si.name)
164
+ os = si.schema[:obj]
165
+ props = si.schema['properties'] || {}
166
+ # Empty object special case.
167
+ if os.props.empty?
168
+ s = <<EOB
169
+ export type #{si.name} = object;
170
+
171
+ export function #{si.schema[:is]}(x: unknown): x is #{si.name} {
172
+ return isObject(x);
173
+ }
174
+
175
+ export function #{si.schema[:unknown]}(x: unknown): #{si.name} {
176
+ if (#{si.schema[:is]}(x))
177
+ return x as #{si.name};
178
+ throw unknown(x, '#{si.name}');
179
+ }
180
+
181
+ export function #{si.schema[:base]}(x: #{si.name}): #{si.name} {
182
+ return x as #{si.name}; // Object without known properties, nothing to do.
183
+ }
184
+ EOB
185
+ out.push(s)
186
+ next
187
+ end
188
+ # Normal object.
189
+ # Call class constructor type.
190
+ # Named properties, required and optional.
191
+ reqd = si.schema['required'] || []
192
+ o = [ true ]
193
+ preq = []
194
+ popt = []
195
+ kreq = []
196
+ os.props.select { |p| !p.pattern && !p.additional }.each do |p|
197
+ ps = Gen.h.dereference(p.spec)
198
+ # oneOf support requires that preq/popt item can be an array of hashes.
199
+ warn("Property '#{p.name}' type '#{p.type}' has nil schema, fully proicessed source?") if ps.nil?
200
+ o.push("#{p.name}#{p.req ? '' : '?'}: #{p.type};")
201
+ (p.req ? preq : popt).push({
202
+ name: p.name,
203
+ is: ps[:is],
204
+ base: ps[:base]
205
+ })
206
+ kreq.push(p.name) if p.req
207
+ end
208
+ # Pattern properties.
209
+ ppat = []
210
+ cres = []
211
+ os.props.select { |p| p.pattern }.each do |p|
212
+ ps = Gen.h.dereference(p.spec)
213
+ o.push("// Pattern: #{p.name}: #{p.type}")
214
+ cre_name = "re#{si.name}_#{cres.size}"
215
+ ppat.push({
216
+ is: ps[:is],
217
+ base: ps[:base],
218
+ cre: cre_name
219
+ })
220
+ cres.push("const #{cre_name} = new RegExp(atob('#{Base64.strict_encode64(p.name)}')); // #{p.name}")
221
+ end
222
+ if os.additional
223
+ add_props = os.props.select(&:additional).first
224
+ if add_props.is_a?(Hash)
225
+ ps = Gen.h.dereference(add_props)
226
+ o.push("// Additional properties of type: #{p.type}")
227
+ ap = "additional: { is: #{ps[:is]}, base: #{ps[:base]} },"
228
+ else
229
+ ap = "additional: { is: isAny, base: baseAny },"
230
+ end
231
+ else
232
+ o.push('// Additional properties are not allowed.')
233
+ ap = "additional: { is: isNot },"
234
+ end
235
+ s = <<EOB
236
+ export type #{si.name} = {
237
+ #{Gen.output.join(o)}
238
+ }
239
+
240
+ #{cres.join("\n")}
241
+ const #{si.schema[:checkers]}: SchemaCheckers = {
242
+ requiredKeys: [#{kreq.map { |key| "'#{key}'" }.join(', ')}],
243
+ required: {
244
+ #{preq.map { |p| "'#{p[:name]}': { is: #{p[:is]}, base: #{p[:base]} }" }.join(",\n")}
245
+ },
246
+ optional: {
247
+ #{popt.map { |p| "'#{p[:name]}': { is: #{p[:is]}, base: #{p[:base]} }" }.join(",\n")}
248
+ },
249
+ #{ap}
250
+ patterns: [
251
+ #{ppat.map { |p| "{ re: #{p[:cre]}, funcs: { is: #{p[:is]}, base: #{p[:base]} }}" }.join(",\n") }
252
+ ]
253
+ }
254
+
255
+ export function #{si.schema[:is]}(x: unknown): x is #{si.name} {
256
+ if (!isObject(x))
257
+ return false;
258
+ return isSchema(x as object, #{si.schema[:checkers]});
259
+ }
260
+
261
+ export function #{si.schema[:unknown]}(x: unknown): #{si.name} {
262
+ if (#{si.schema[:is]}(x))
263
+ return x as #{si.name};
264
+ throw unknown(x, '#{si.name}');
265
+ }
266
+
267
+ export function #{si.schema[:base]}(x: #{si.name}): #{si.name} {
268
+ return baseSchema(x, #{si.schema[:checkers]}) as #{si.name};
269
+ }
270
+ EOB
271
+ out.push(s)
272
+ when 'array'
273
+ Gen.x.schemas.types.push(si.name)
274
+ min_items = si.schema.fetch('minItems', 0)
275
+ max_items = si.schema['maxItems']
276
+ min_contains = si.schema.fetch('minContains', 0)
277
+ max_contains = si.schema['maxContains']
278
+ # Opportunity to warn.
279
+ max_contains = nil if !max_items.nil? && !max_contains.nil? && max_items < max_contains
280
+ # Uniqueness might be better implemented using custom code.
281
+ # Checking here could use lodash or underscore.
282
+ uniq = si.schema.fetch('uniqueItems', false)
283
+ # This requires that the array has optional types.
284
+ # items could be false, so prefixItems handles all?
285
+ # contains can add another type.
286
+ # Prefix sounds like they'd be better as actual named fields.
287
+ prefix = si.schema['prefixItems']
288
+ unless prefix.nil?
289
+ # Opportunity to warn.
290
+ prefix = prefix[0...max_items] if !max_items.nil? && max_items < prefix.size
291
+ prefix = prefix.map { |p| Gen.h.dereference(p) }
292
+ end
293
+ contains = si.schema['contains']
294
+ contains = Gen.h.dereference(contains) unless contains.nil?
295
+ items = si.schema['items']
296
+ items = Gen.h.dereference(items) unless items.nil?
297
+ # Opportunity to warn.
298
+ items = nil if !prefix.nil? && !max_items.nil? && prefix.size == max_items
299
+ alts = []
300
+ bases = []
301
+ if prefix.nil?
302
+ needs_loop = true
303
+ is_prefix = [ '// No prefixItems.' ]
304
+ base_prefix = [ '// No prefixItems.' ]
305
+ else
306
+ needs_loop = !max_items.nil? && prefix.size < max_items
307
+ prefix.each do |p|
308
+ alts.push(p[:name])
309
+ bases.push(p)
310
+ end
311
+ is_prefix = []
312
+ base_prefix = []
313
+ # Prefix schema may be close enough for contains. One derived from another.
314
+ prefix.size.times do |idx|
315
+ p = prefix[idx]
316
+ cont = '// No contains check.'
317
+ unless contains.nil?
318
+ if contains[:is] == p[:is]
319
+ cont = 'else ++contains;'
320
+ elsif contains['type'] == p['type']
321
+ cont = "if (#{contains[:is]}(a[#{idx})]) ++contains;"
322
+ end
323
+ end
324
+ s = <<EOB
325
+ if (a.length > #{idx}) {
326
+ if (!#{p[:is]}(a[#{idx}])) return false;
327
+ #{cont}
328
+ }
329
+ EOB
330
+ is_prefix.push(s)
331
+ base_prefix.push("if (a.length > #{idx}) out.push(#{p[:base]}(a[#{idx}]));")
332
+ end
333
+ end
334
+ # Contains and items may be the same or derive from same.
335
+ if items.nil?
336
+ alts.push('any') if max_items.nil? || prefix.size < max_items
337
+ is_item = ''
338
+ item_ch = '// No items check, anything allowed.'
339
+ base_item = 'out.push(x[k]);'
340
+ else
341
+ alts.push(items[:name])
342
+ is_item = items[:is]
343
+ item_ch = "if (!#{is_item}(a[k])) return false;"
344
+ base_item = "out.push(#{items[:base]}(x[k]));"
345
+ end
346
+ c_incr = '// No contains check.'
347
+ if contains.nil?
348
+ cont = '// No contains counter check.'
349
+ base_cont = '// No contains schema.'
350
+ else
351
+ t_min_ch = min_contains.positive? ? "contains < #{min_contains}" : nil
352
+ t_max_ch = max_contains.nil? ? nil : "#{max_contains} < contains"
353
+ if !t_min_ch.nil? || !t_max_ch.nil?
354
+ c_incr = "#{is_item != contains[:is] ? "if (#{contains[:is]}(a[k])) " : ''}++contains;"
355
+ end
356
+ cont = [ t_min_ch, t_max_ch ].compact.join(' || ') || '// No contains counter check.'
357
+ needs_loop = prefix.nil? || max_items.nil? || prefix.size < max_items || !items.nil?
358
+ base_cont = <<EOB
359
+ if (#{contains[:is]}) {
360
+ out.push(#{contains[:base]}(a[k]));
361
+ continue;
362
+ }
363
+ EOB
364
+ end
365
+ s = <<EOB
366
+ export type #{si.name} = Array<#{alts.uniq.sort!.join('|')}>;
367
+
368
+ export function #{si.schema[:is]}(x: unknown): x is #{si.name} {
369
+ if (!Array.isArray(x))
370
+ return false;
371
+ #{min_items.positive? ? "if (x.length < #{min_items}) return false;" : '// No min item limit.'}
372
+ #{max_items.nil? ? '// No max item limit.' : "if (#{max_items} < x.length) return false;"}
373
+ const a: Array<any> = x as Array<any>;
374
+ #{contains.nil? ? '// No contains counter.' : 'let contains = 0;'}
375
+ #{is_prefix.join("\n")}
376
+ #{needs_loop ? '' : '/* '}for (let k = #{prefix.nil? ? 0 : prefix.size}; k < a.length; ++k) {
377
+ #{item_ch}
378
+ #{c_incr}
379
+ #{uniq ? '// Uniqueness check not implemented.' : '// Uniqueness not required.'}
380
+ }#{needs_loop ? '' : ' */'}
381
+ #{cont}
382
+ return true;
383
+ }
384
+
385
+ export function #{si.schema[:unknown]}(x: unknown): #{si.name} {
386
+ if (#{si.schema[:is]}(x))
387
+ return x as #{si.name};
388
+ throw unknown(x, '#{si.name}');
389
+ }
390
+
391
+ export function #{si.schema[:base]}(x: #{si.name}): #{si.name} {
392
+ const out: #{si.name} = [];
393
+ #{base_prefix.join("\n")}
394
+ #{needs_loop ? '' : '/* '}for (let k = #{prefix.nil? ? 0 : prefix.size}; k < x.length; ++k) {
395
+ #{base_cont}
396
+ #{base_item}
397
+ }#{needs_loop ? '' : ' */'}
398
+ return out;
399
+ }
400
+ EOB
401
+ out.push(s)
402
+ when 'string'
403
+ Gen.x.schemas.types.push(si.name)
404
+ checks = {
405
+ 'maxLength' => "if (value.length > #{si.schema['maxLength']}) throw new Error(`#{si.name} length ${value.length} > #{si.schema['maxLength']}`);",
406
+ 'minLength' => "if (value.length < #{si.schema['minLength']}) throw new Error(`#{si.name} length ${value.length} < #{si.schema['minLength']}`);",
407
+ 'pattern' => "if (!re#{si.name}.test(value)) throw new Error(`#{si.name} value '${value}' does not match pattern ${re#{si.name}.source}`);"
408
+ }
409
+ re = si.schema.key?('pattern') ? "const re#{si.name} = new RegExp(atob('#{Base64.strict_encode64(si.schema['pattern'])}')); // #{si.schema['pattern']}" : nil
410
+ present = checks.keys.map { |k| si.schema.key?(k) ? checks[k] : nil }.compact
411
+ s = <<EOB
412
+ export type #{si.name} = string;
413
+
414
+ #{re || '// No content pattern.'}
415
+ export function check#{LuckyCase.pascal_case(si.name)}(value: string): #{si.name} {
416
+ #{present.join("\n ")}
417
+ return value as #{si.name};
418
+ }
419
+
420
+ export function #{si.schema[:is]}(x: unknown): x is #{si.name} {
421
+ #{present.empty? ? "return isString(x);" : "if (!isString(x))
422
+ return false;
423
+ try {
424
+ check#{LuckyCase.pascal_case(si.name)}(x as string);
425
+ }
426
+ catch {
427
+ return false;
428
+ }
429
+ return true;"}
430
+ }
431
+
432
+ export function #{si.schema[:unknown]}(x: unknown): #{si.name} {
433
+ if (#{si.schema[:is]}(x))
434
+ return x as #{si.name};
435
+ throw unknown(x, '#{si.name}');
436
+ }
437
+
438
+ export function #{si.schema[:base]}(x: #{si.name}): #{si.name} {
439
+ return x; // String, nothing to do.
440
+ }
441
+ EOB
442
+ out.push(s)
443
+ when 'integer', 'number'
444
+ Gen.x.schemas.types.push(si.name)
445
+ si.schema[:native] = si.schema['type'] == 'integer' ? :integer : :float
446
+ checks = {
447
+ 'exclusiveMinimum' => "if (value <= #{si.schema['exclusiveMinimum']}) throw new Error(`#{si.name} value ${value} <= #{si.schema['exclusiveMinimum']}`);",
448
+ 'exclusiveMaximum' => "if (value >= #{si.schema['exclusiveMaximum']}) throw new Error(`#{si.name} value ${value} >= #{si.schema['exclusiveMaximum']}`);",
449
+ 'minimum' => "if (value < #{si.schema['minimum']}) throw new Error(`#{si.name} value ${value} < #{si.schema['minimum']}`);",
450
+ 'maximum' => "if (value > #{si.schema['maximum']}) throw new Error(`#{si.name} value ${value} > #{si.schema['maximum']}`);",
451
+ 'multipleOf' => "if (value % #{si.schema['multipleOf']} !== 0) throw new Error(`#{si.name} value ${value} not a multiple of #{si.schema['multipleOf']}`);"
452
+ }
453
+ present = checks.keys.map { |k| si.schema.key?(k) ? checks[k] : nil }.compact
454
+ present.push("if (!Number.isSafeInteger(value)) throw new Error(`#{si.name} value ${value} is not a safe integer`);") if si.schema[:native] == :integer
455
+ s = <<EOB
456
+ export type #{si.name} = number;
457
+
458
+ export function check#{LuckyCase.pascal_case(si.name)}(value: number): #{si.name} {
459
+ #{present.join("\n ")}
460
+ return value as #{si.name};
461
+ }
462
+
463
+ export function #{si.schema[:is]}(x: unknown): x is #{si.name} {
464
+ if (!isNumber(x))
465
+ return false;
466
+ try {
467
+ check#{LuckyCase.pascal_case(si.name)}(x as number);
468
+ }
469
+ catch {
470
+ return false;
471
+ }
472
+ return true;
473
+ }
474
+
475
+ export function #{si.schema[:unknown]}(x: unknown): #{si.name} {
476
+ if (#{si.schema[:is]}(x))
477
+ return x as #{si.name};
478
+ throw unknown(x, '#{si.name}');
479
+ }
480
+
481
+ export function #{si.schema[:base]}(x: #{si.name}): #{si.name} {
482
+ return x; // Number, nothing to do.
483
+ }
484
+ EOB
485
+ out.push(s)
486
+ when 'boolean'
487
+ Gen.x.schemas.types.push(si.name)
488
+ s = <<EOB
489
+ export type #{si.name} = boolean;
490
+
491
+ export function #{si.schema[:is]}(x: unknown): x is #{si.name} {
492
+ return isBoolean(x);
493
+ }
494
+
495
+ export function #{si.schema[:unknown]}(x: unknown): #{si.name} {
496
+ if (#{si.schema[:is]}(x))
497
+ return x as #{si.name};
498
+ throw unknown(x, '#{si.name}');
499
+ }
500
+
501
+ export function #{si.schema[:base]}(x: #{si.name}): #{si.name} {
502
+ return x; // Boolean, nothing to do.
503
+ }
504
+ EOB
505
+ out.push(s)
506
+ else
507
+ raise StandardError, "Unhandled type: '#{si.schema['type']}' in #{si.name}"
508
+ end
509
+ out.push('')
510
+ end
511
+ Gen.output.join(out)
512
+ %>
513
+
514
+ // Security requirement types.
515
+ <%=
516
+ out = []
517
+ secs = Gen.doc.dig('components', 'securitySchemes')
518
+ if secs.nil?
519
+ out.push('// No security schemes.')
520
+ else
521
+ has_apikey = false
522
+ secs.each do |sname, scheme|
523
+ desc = scheme['description']
524
+ unless desc.nil?
525
+ desc.strip!
526
+ if desc.lines.size < 2
527
+ out.push("// #{desc}")
528
+ else
529
+ out.push("/*\n#{desc}\n*/")
530
+ end
531
+ end
532
+ # The configuration should allow a drop-in type as multi-line text.
533
+ case scheme['type'].downcase
534
+ when 'apikey'
535
+ next if has_apikey
536
+ has_apikey = true
537
+ scheme[:name] = "SecurityApiKey"
538
+ scheme[:is] = 'isString'
539
+ scheme[:base] = 'baseString'
540
+ scheme[:unknown] = 'unknown2String'
541
+ Gen.x.schemas.types.push(scheme[:name])
542
+ out.push("export type #{scheme[:name]} = string;")
543
+ when 'http'
544
+ scheme[:name] = "SecurityHttp#{LuckyCase.pascal_case(sname)}"
545
+ scheme[:is] = 'isObject'
546
+ scheme[:base] = 'baseObject'
547
+ scheme[:unknown] = 'unknown2Object'
548
+ Gen.x.schemas.types.push(scheme[:name])
549
+ out.push("export type #{scheme[:name]} = Object;")
550
+ when 'oauth2'
551
+ # Produce a constant object that contains all data as is.
552
+ scheme[:name] = "SecurityOAuth2#{LuckyCase.pascal_case(sname)}"
553
+ scheme[:is] = 'isObject'
554
+ scheme[:base] = 'baseObject'
555
+ scheme[:unknown] = 'unknown2Object'
556
+ Gen.x.schemas.consts.push(scheme[:name])
557
+ out.push("export const #{scheme[:name]} = #{JSON.pretty_generate(scheme['flows'])}")
558
+ when 'openidconnect'
559
+ scheme[:name] = "SecurityOpenIdConnect#{LuckyCase.pascal_case(sname)}"
560
+ scheme[:is] = 'isString'
561
+ scheme[:base] = 'baseString'
562
+ scheme[:unknown] = 'unknown2String'
563
+ Gen.x.schemas.consts.push(scheme[:name])
564
+ out.push("export const #{scheme[:name]} = #{JSON.pretty_generate(scheme['openIdConnectUrl'])};")
565
+ else
566
+ msg = "Unhandled security scheme type: #{scheme['type']}"
567
+ out.push("// #{msg}")
568
+ warn(msg)
569
+ end
570
+ end
571
+ end
572
+ Gen.output.join(out)
573
+ %>
574
+
575
+ // Types for making requests.
576
+
577
+ export type String2String = Record<string, string>;
578
+
579
+ // Type for passing extra parameters in addition to operation parameters.
580
+ export type CallExtras = {
581
+ headers?: String2String;
582
+ query?: String2String;
583
+ }
584
+
585
+ const utf8encoder = new TextEncoder();
586
+ const applicationJsonUtf8 = 'application/json; charset=utf-8';
587
+
588
+ export type EncodedBodyType = string|ArrayBuffer|Uint8Array<ArrayBuffer>|Blob|DataView|File|FormData|URLSearchParams|ReadableStream;
589
+ // Returns body, Content-Type, Content-Length, and additional headers, if any.
590
+ export type EncoderFunction = (body: any) => [body: EncodedBodyType, contentType: string, contentLength: number, headers: String2String];
591
+
592
+ export function jsonEncode(body: any): [EncodedBodyType, string, number, String2String] {
593
+ const b = utf8encoder.encode(JSON.stringify(body));
594
+ return [b, applicationJsonUtf8, b.length, {}];
595
+ }
596
+
597
+ export function isEncodedBodyType(x: unknown): x is EncodedBodyType {
598
+ if (ArrayBuffer.isView(x)) return true; // DataView, TypedArray.
599
+ if (isString(x)) return true;
600
+ if (x instanceof ArrayBuffer) return true;
601
+ if (x instanceof Blob) return true; // Blob, File
602
+ if (x instanceof FormData) return true;
603
+ if (x instanceof URLSearchParams) return true;
604
+ if (x instanceof ReadableStream) return true;
605
+ return false;
606
+ }
607
+
608
+ export function baseEncodedBodyType(x: unknown): EncodedBodyType {
609
+ return x as EncodedBodyType;
610
+ }
611
+
612
+ // Operation parameter types.
613
+
614
+ <%=
615
+ encoderTypes = Set.new
616
+ json_encoder_names = Set.new
617
+ json_encoders = {}
618
+ out = []
619
+ schemes = Gen.doc.dig('components', 'securitySchemes')
620
+ Gen.doc['paths'].each do |path, path_item_object|
621
+ oos = OpenAPISourceTools::ApiObjects.operation_objects(path_item_object)
622
+ next if oos.empty?
623
+ oos.each do |method, operation_object|
624
+ operation_object[:args_name] = "#{LuckyCase.pascal_case(operation_object['operationId'])}Args"
625
+ Gen.x.schemas.types.push(operation_object[:args_name])
626
+ operation_object[:args_is] = "is#{operation_object[:args_name]}"
627
+ operation_object[:args_base] = "base#{operation_object[:args_name]}"
628
+ Gen.x.schemas.functions.push(operation_object[:args_is])
629
+ Gen.x.schemas.functions.push(operation_object[:args_base])
630
+ paraschemas = (operation_object['parameters'] || []).map do |pref|
631
+ p = Gen.h.dereference(pref)
632
+ p[:name] = LuckyCase.camel_case(p['name'])
633
+ s = Gen.h.dereference(p['schema'])
634
+ {
635
+ param: p,
636
+ schema: s
637
+ }
638
+ end
639
+ operation_object[:paraschemas] = paraschemas
640
+ # Get all common fields and then create all body-type specific variations
641
+ # using it to ensure the fields remain the same.
642
+ common = [ '// Parameters:' ]
643
+ is_func = []
644
+ base_func = []
645
+ base_opt_func = []
646
+ paraschemas.each do |ps|
647
+ p = ps[:param]
648
+ common.push("#{p[:name]}#{p['required'] ? '' : '?'}: #{ps[:schema][:name]};")
649
+ if p['required']
650
+ is_func.push("if (!#{ps[:schema][:is]}(y.#{p[:name]})) return false;")
651
+ base_func.push("#{p[:name]}: #{ps[:schema][:base]}(y.#{p[:name]})")
652
+ else
653
+ is_func.push("if (y.#{p[:name]} !== undefined && !#{ps[:schema][:is]}(y.#{p[:name]})) return false;")
654
+ base_opt_func.push("if (y.#{p[:name]} !== undefined) out.#{p[:name]} = #{ps[:schema][:base]}(y.#{p[:name]});")
655
+ end
656
+ end
657
+ common.pop unless common.last.end_with?(';') # Remove comment if no parameters.
658
+ # No security-related parameters, set like server URLs.
659
+ # Remove code below once serves no example value.
660
+ if false
661
+ common.push('// Security-related:')
662
+ secs = operation_object['security']
663
+ optional = secs.size > 1 || !(secs.find { |s| s.empty? }).nil?
664
+ secs.push({}) if secs.empty?
665
+ used = Set.new
666
+ operation_object[:security_params] = []
667
+ secs.each do |s|
668
+ next if s.empty?
669
+ s.keys.sort!.each do |sname|
670
+ next if used.include?(sname)
671
+ used.add(sname)
672
+ scheme = schemes[sname]
673
+ # API key adds a query or header parameter. Cookie is beyond the scope.
674
+ # http adds authentication that is placed to Authentication header.
675
+ # mutualTLS is beyond the scope.
676
+ # oauth2 specifies flow type, beyond the scope. Check.
677
+ # openIdConnect specifies URL and is beyond the scope.
678
+ common.push("#{sname}#{optional ? '?' : ''}: #{scheme[:name]};")
679
+ if optional
680
+ is_func.push("if (y.#{sname} !== undefined && !#{scheme[:is]}(y.#{sname})) return false;")
681
+ base_opt_func.push("if (y.#{sname} !== undefined) out.#{sname} = #{scheme[:base]}(y.#{sname});")
682
+ else
683
+ is_func.push("if (!#{scheme[:is]}(y.#{sname})) return false;")
684
+ base_func.push("#{sname}: #{scheme[:base]}(y.#{sname})")
685
+ end
686
+ operation_object[:security_params].push({
687
+ name: sname,
688
+ scheme: scheme,
689
+ optional: optional
690
+ })
691
+ end
692
+ common.pop unless common.last.end_with?(';') # Remove comment if no security members.
693
+ end
694
+ end
695
+ req_body = operation_object['requestBody']
696
+ if !req_body && common.empty? # Takes nothing whatsoever.
697
+ s = <<EOB
698
+ // No parameters for #{operation_object['operationId']}.
699
+ export type #{operation_object[:args_name]} = object;
700
+
701
+ export function #{operation_object[:args_is]}(x: unknown): x is #{operation_object[:args_name]} {
702
+ return isObject(x);
703
+ }
704
+
705
+ export function #{operation_object[:args_base]}(x: #{operation_object[:args_name]}): #{operation_object[:args_name]} {
706
+ return baseObject(x);
707
+ }
708
+ EOB
709
+ out.push(s)
710
+ operation_object[:parameterless] = true
711
+ next
712
+ end
713
+
714
+ # Proper pairing check after encoding can be done since we know the input type. Check
715
+ # string against what was in the API spec.
716
+ # Order of checking is specific match to body, instance generic, class specific match,
717
+ # class generic.
718
+
719
+ # Pass also schema name to encoder just to avoid run-time checks provided you trust
720
+ # the programmer? Maybe other uses? Over-designing?
721
+
722
+ body_types = {}
723
+ body_encodings = {}
724
+
725
+ comments = []
726
+ req = false
727
+ if req_body
728
+ req_body[:body_types] = body_types
729
+ req_body[:body_encodings] = body_encodings
730
+ req = req_body.fetch('required', false)
731
+ (req_body['content'] || {}).each do |media_type, mto|
732
+ mto[:source] = Gen.h.dereference(mto['schema']) || {
733
+ name: 'any',
734
+ is: 'isAny',
735
+ base: 'baseAny',
736
+ unknown: 'unknown2Any'
737
+ }
738
+ body_types[mto[:source][:name]] = mto[:source]
739
+ mto[:source][:body_name] = "body#{LuckyCase.pascal_case(mto[:source][:name])}"
740
+ mt = media_type.downcase
741
+ encs = body_encodings[mto[:source][:body_name]] || Set.new
742
+ encs.add(mt)
743
+ body_encodings[mto[:source][:body_name]] = encs
744
+ comments.push("// content type #{mt}, source type: #{mto[:source][:name]}.")
745
+ end
746
+ end
747
+ operation_object[:body_req] = req
748
+ body_props = nil
749
+ has_name = "hasBody#{operation_object[:args_name]}"
750
+ has_body = nil
751
+ base_name = "baseBody#{operation_object[:args_name]}"
752
+ base_body = nil
753
+ base_snip = nil
754
+ if body_types.key?('any')
755
+ bodies = body_types.keys.reject { |k| k == 'any' }.sort!
756
+ body_props = <<EOB
757
+ #{bodies.map { |name| "body#{name}?: #{name};" }.join("\n")}
758
+ body?: any|undefined;
759
+ encoder?: EncoderFunction;
760
+ encoded?: EncodedBodyType;
761
+ EOB
762
+ has_body = <<EOB
763
+ function #{has_name}(x: unknown): boolean {
764
+ const y = x as #{operation_object[:args_name]};
765
+ const hasEnc: boolean = y.encoded !== undefined;
766
+ const encOk: boolean = hasEnc && isEncodedBodyType(y.encoded);
767
+ #{bodies.size.times.map do |idx|
768
+ name = bodies[idx]
769
+ s = <<EOC
770
+ if (y.body#{name} !== undefined) {
771
+ if (!#{body_types[name][:is]}(y.body#{name})) return false;
772
+ #{((idx + 1)...bodies.size).each { |k| "if (y.body#{bodies[k]} !== undefined) throw new Error('#{operation_object[:args_name]} has two body members set: body#{name}, body#{bodies[k]}')" }.join("\n")}
773
+ if (y.body !== undefined && !Object.is(y.body, y.body#{name})) {
774
+ throw new Error('#{operation_object[:args_name]} body and body#{name} are set to different objects.')
775
+ }
776
+ return hasEnc ? encOk : true;
777
+ }
778
+ EOC
779
+ s
780
+ end.join("\n")}
781
+ if (y.body !== undefined) return hasEnc ? encOk : true;
782
+ return #{req ? 'false' : 'true'};
783
+ }
784
+ EOB
785
+ body_type = 'any'
786
+ base_body = <<EOB
787
+ export function #{base_name}(x: unknown): #{body_type} {
788
+ return x as #{body_type};
789
+ }
790
+ EOB
791
+ base_snip = <<EOB
792
+ if (y.body !== undefined) out.body = #{base_name}(y.body);
793
+ if (y.encoded !== undefined) out.encoded = baseEncodedBodyType(y.encoded);
794
+ EOB
795
+ elsif !body_types.empty?
796
+ bodies = body_types.keys.sort!
797
+ body_type = body_types.keys.sort!.join('|')
798
+ body_props = <<EOB
799
+ #{bodies.map { |name| "body#{name}?: #{name};" }.join("\n")}
800
+ body?: #{body_type}|undefined;
801
+ encoder?: EncoderFunction;
802
+ encoded?: EncodedBodyType;
803
+ EOB
804
+ has_body = <<EOB
805
+ function #{has_name}(x: unknown): boolean {
806
+ const y = x as #{operation_object[:args_name]};
807
+ if (y.encoded !== undefined)
808
+ return isEncodedBodyType(y.encoded);
809
+ #{bodies.size.times.map do |idx|
810
+ name = bodies[idx]
811
+ s = <<EOC
812
+ if (y.body#{name} !== undefined) {
813
+ if (!#{body_types[name][:is]}(y.body#{name})) return false;
814
+ #{((idx + 1)...bodies.size).to_a.map { |k| "if (y.body#{bodies[k]} !== undefined) throw new Error('#{operation_object[:args_name]} has two body members set: body#{name}, body#{bodies[k]}')" }.join("\n")}
815
+ if (y.body !== undefined && !Object.is(y.body, y.body#{name})) {
816
+ throw new Error('#{operation_object[:args_name]} body and body#{name} are set to different objects.')
817
+ }
818
+ return true;
819
+ }
820
+ EOC
821
+ s
822
+ end.join("\n")}
823
+ if (y.body !== undefined) {
824
+ #{bodies.map { |name| "if (#{body_types[name][:is]}(y.body)) return true;" }.join("\n") }
825
+ }
826
+ return #{req ? 'false' : 'true'};
827
+ }
828
+ EOB
829
+ base_body = <<EOB
830
+ export function #{base_name}(x: unknown): #{body_type}|undefined {
831
+ #{body_types.keys.sort!.map { |name| "if (#{body_types[name][:is]}(x)) return #{body_types[name][:base]}(x);" }.join("\n")}
832
+ }
833
+ EOB
834
+ base_snip = <<EOB
835
+ if (y.encoded !== undefined) out.encoded = baseEncodedBodyType(y.encoded); else
836
+ #{bodies.map { |name| <<EOB
837
+ if (y.body#{name} !== undefined) {
838
+ out.body#{name} = #{body_types[name][:base]}(y.body#{name});
839
+ if (y.body !== undefined && Object.is(y.body#{name}, y.body))
840
+ out.body = out.body#{name};
841
+ } else
842
+ EOB
843
+ }.join("\n")}
844
+ if (y.body !== undefined) out.body = #{base_name}(y.body);
845
+ EOB
846
+ end
847
+ out.concat([
848
+ "export type #{operation_object[:args_name]} = {",
849
+ ], common,
850
+ comments,
851
+ [
852
+ body_props,
853
+ '}',
854
+ has_body,
855
+ "\nexport function #{operation_object[:args_is]}(x: unknown): x is #{operation_object[:args_name]} {",
856
+ "if (!isObject(x)) return false;",
857
+ "const y = x as #{operation_object[:args_name]};"
858
+ ], is_func, [
859
+ body_types.empty? ? nil : "if (!#{has_name}(x)) return false;",
860
+ 'return true;',
861
+ '}',
862
+ base_body,
863
+ "\nexport function #{operation_object[:args_base]}(x: unknown): #{operation_object[:args_name]} {",
864
+ "const y = x as #{operation_object[:args_name]};",
865
+ "const out: #{operation_object[:args_name]} = {",
866
+ base_func.join(",\n"),
867
+ '}'
868
+ ], base_opt_func, [
869
+ base_snip,
870
+ 'return out;',
871
+ '}',
872
+ ''
873
+ ])
874
+ end
875
+ end
876
+ Gen.output.join(out)
877
+ %>
878
+
879
+ // Types containing Response information.
880
+
881
+ <%=
882
+ ignore = %w[content-type] # Get from configuration.
883
+ done = {}
884
+ out = []
885
+ Gen.doc.dig('components', 'responses').each do |name, response|
886
+ # First get info about possible headers type and body types.
887
+ headers = response['headers'] || {}
888
+ remain = headers.keys.reject { |h| ignore.include?(h.downcase) }
889
+ name2type = {}
890
+ remain.each do |h|
891
+ s = Gen.h.dereference(Gen.h.dereference(headers[h])['schema'])
892
+ name2type[h] = {
893
+ name: LuckyCase.camel_case(h),
894
+ type: s[:name],
895
+ schema: s
896
+ }
897
+ end
898
+ content = response['content'] || {}
899
+ content.each do |media_type, mto|
900
+ mt = media_type.downcase
901
+ if mt == 'application/json'
902
+ mto[:source] = 'any'
903
+ elsif mt == 'application/octet-stream'
904
+ mto[:source] = 'Blob'
905
+ elsif mt.start_with?('text/')
906
+ mto[:source] = 'string'
907
+ else
908
+ mto[:source] = 'Blob'
909
+ end
910
+ mto[:decoded] = Gen.h.dereference(mto['schema']) || {
911
+ name: 'any',
912
+ is: 'isAny',
913
+ base: 'baseAny',
914
+ unknown: 'unknown2Any'
915
+ }
916
+ end
917
+ if remain.empty? && content.empty?
918
+ response[:name] = 'object'
919
+ response[:is] = "isObject"
920
+ response[:base] = "baseObject"
921
+ response[:unknown] = "unknown2Object"
922
+ out.push("// Nothing for response #{name}.")
923
+ next
924
+ end
925
+ response[:name2type] = name2type
926
+ # Type with single property could be treated as the property directly but this
927
+ # leaves code unchanged if more parameters are added later.
928
+ # Same for no properties at all.
929
+ # Short-cuts can be added later.
930
+ response[:name] = "#{LuckyCase.pascal_case(name)}Response"
931
+ Gen.x.schemas.types.push(response[:name])
932
+ defs = []
933
+ is_func = []
934
+ base_func = []
935
+ name2type.each do |h, n2t|
936
+ defs.push("#{n2t[:name]}: #{n2t[:type]}|null;")
937
+ is_func.push("if (y.#{n2t[:name]} !== null && !#{n2t[:schema][:is]}(y.#{n2t[:name]})) return false;")
938
+ base_func.push("#{n2t[:name]}: x.#{n2t[:name]} === null ? null : #{n2t[:schema][:base]}(x.#{n2t[:name]})")
939
+ end
940
+ response[:is] = "is#{response[:name]}"
941
+ response[:base] = "base#{response[:name]}"
942
+ response[:unknown] = "unknown2#{response[:name]}"
943
+ Gen.x.schemas.functions.push(response[:is])
944
+ Gen.x.schemas.functions.push(response[:base])
945
+ Gen.x.schemas.functions.push(response[:unknown])
946
+ s = <<EOB
947
+ export type #{response[:name]} = {
948
+ #{defs.join("\n")}
949
+ }
950
+
951
+ export function #{response[:is]}(x: unknown): x is #{response[:name]} {
952
+ if (!isObject(x)) return false;
953
+ const y = x as #{response[:name]};
954
+ #{is_func.join("\n")}
955
+ return true;
956
+ }
957
+
958
+ export function #{response[:base]}(x: #{response[:name]}): #{response[:name]} {
959
+ return {
960
+ #{base_func.join(",\n")}
961
+ }
962
+ }
963
+
964
+ export function #{response[:unknown]}(x: unknown): #{response[:name]} {
965
+ if (#{response[:is]}(x))
966
+ return x as #{response[:name]};
967
+ throw unknown(x, '#{response[:name]}');
968
+ }
969
+ EOB
970
+ out.push(s)
971
+ # For each body type, if any, extend with the body property.
972
+ content.each do |media_type, mto|
973
+ if done.key?(mto[:decoded][:name])
974
+ out.push("// Content-type #{media_type} covered by #{done[mto[:decoded][:name]]}.\n")
975
+ next
976
+ end
977
+ mto[:name] = "#{response[:name]}#{LuckyCase.pascal_case(mto[:decoded][:name])}"
978
+ Gen.x.schemas.types.push(mto[:name])
979
+ mto[:is] = "is#{mto[:name]}"
980
+ mto[:base] = "base#{mto[:name]}"
981
+ mto[:unknown] = "unknown2#{mto[:name]}"
982
+ Gen.x.schemas.functions.push(mto[:is])
983
+ Gen.x.schemas.functions.push(mto[:base])
984
+ Gen.x.schemas.functions.push(mto[:unknown])
985
+ s = <<EOB
986
+ // For content-type #{media_type}
987
+ export type #{mto[:name]} = #{response[:name]} & {
988
+ body: #{mto[:decoded][:name]};
989
+ }
990
+
991
+ export function #{mto[:is]}(x: unknown): x is #{mto[:name]} {
992
+ const y = x as #{mto[:name]};
993
+ if (y.body === undefined || !#{mto[:decoded][:is]}(y.body)) return false;
994
+ if (!#{response[:is]}(y)) return false;
995
+ return true;
996
+ }
997
+
998
+ export function #{mto[:base]}(x: #{mto[:name]}): #{mto[:name]} {
999
+ return {
1000
+ ...#{response[:base]}(x),
1001
+ body: #{mto[:decoded][:base]}(x.body)
1002
+ }
1003
+ }
1004
+
1005
+ export function #{mto[:unknown]}(x: unknown): #{mto[:name]} {
1006
+ if (#{mto[:is]}(x))
1007
+ return x as #{mto[:name]};
1008
+ throw unknown(x, '#{mto[:name]}');
1009
+ }
1010
+ EOB
1011
+ out.push(s)
1012
+ done[mto[:decoded][:name]] = mto[:name]
1013
+ end
1014
+ end
1015
+ Gen.output.join(out)
1016
+ %>