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.
- checksums.yaml +7 -0
- data/LICENSE.txt +17 -0
- data/lib/openapi_generate_typescript_fetch/schema.rb +115 -0
- data/lib/openapi_generate_typescript_fetch/taskinfo.rb +52 -0
- data/lib/openapi_generate_typescript_fetch/tasks.rb +43 -0
- data/lib/openapi_generate_typescript_fetch/version.rb +9 -0
- data/lib/openapi_generate_typescript_fetch.rb +107 -0
- data/template/package.json.erb +48 -0
- data/template/src/callclasses.ts.erb +542 -0
- data/template/src/helpers.ts.erb +191 -0
- data/template/src/index.ts.erb +19 -0
- data/template/src/schemas.ts.erb +1016 -0
- data/template/src/servers.ts.erb +226 -0
- data/template/src/shared.ts.erb +114 -0
- data/template/test/callclasses.ts.erb +844 -0
- data/template/test/helpers.ts.erb +1214 -0
- data/template/test/makers.ts.erb +1237 -0
- data/template/test/schemas.ts.erb +664 -0
- data/template/test/servers.ts.erb +109 -0
- data/template/test/shared.ts.erb +107 -0
- data/template/tsconfig.json.erb +51 -0
- metadata +112 -0
|
@@ -0,0 +1,1214 @@
|
|
|
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 {String2String} from '../src/schemas.d.ts';
|
|
8
|
+
import type {Bodyful,Name2Functions,LoaderFunction,LoaderMap,SchemaFunctions,PatternFunctions,SchemaCheckers} from '../src/helpers.js';
|
|
9
|
+
import {addJsonBody,str2integer,str2float,isSchema,baseSchema,setExtras,query,headers,getLoader} from '../src/helpers.js';
|
|
10
|
+
|
|
11
|
+
describe('addJsonBody', function () {
|
|
12
|
+
it('add body and headers', function () {
|
|
13
|
+
const target: Bodyful = {};
|
|
14
|
+
const body = {key: 'value'};
|
|
15
|
+
const hdrs: String2String = {};
|
|
16
|
+
addJsonBody(target, body, hdrs);
|
|
17
|
+
const td = new TextDecoder();
|
|
18
|
+
expect(JSON.parse(td.decode(target.body))).to.deep.equal(body);
|
|
19
|
+
expect(hdrs['Content-Type']).to.equal('application/json');
|
|
20
|
+
expect(hdrs['Content-Length']).to.equal(target.body.length.toString());
|
|
21
|
+
})
|
|
22
|
+
it('no body with null', function () {
|
|
23
|
+
const target: Bodyful = {};
|
|
24
|
+
const hdrs: String2String = {};
|
|
25
|
+
addJsonBody(target, null, hdrs);
|
|
26
|
+
expect(target).not.to.have.property('body');
|
|
27
|
+
expect(hdrs).not.to.have.property('Content-Type');
|
|
28
|
+
expect(hdrs).not.to.have.property('Content-Length');
|
|
29
|
+
})
|
|
30
|
+
it('no body with undefined', function () {
|
|
31
|
+
const target: Bodyful = {};
|
|
32
|
+
const hdrs: String2String = {};
|
|
33
|
+
addJsonBody(target, undefined, hdrs);
|
|
34
|
+
expect(target).not.to.have.property('body');
|
|
35
|
+
expect(hdrs).not.to.have.property('Content-Type');
|
|
36
|
+
expect(hdrs).not.to.have.property('Content-Length');
|
|
37
|
+
})
|
|
38
|
+
it('handle empty object body', function () {
|
|
39
|
+
const target: Bodyful = {};
|
|
40
|
+
const body = {};
|
|
41
|
+
const hdrs: String2String = {};
|
|
42
|
+
addJsonBody(target, body, hdrs);
|
|
43
|
+
const td = new TextDecoder();
|
|
44
|
+
expect(JSON.parse(td.decode(target.body))).to.deep.equal({});
|
|
45
|
+
expect(hdrs['Content-Type']).to.equal('application/json');
|
|
46
|
+
expect(hdrs['Content-Length']).to.equal(target.body.length.toString());
|
|
47
|
+
})
|
|
48
|
+
it('preserves existing headers but overwrites Content-Type and Content-Length', function () {
|
|
49
|
+
const target: Bodyful = {};
|
|
50
|
+
const body = {test: 'value'};
|
|
51
|
+
const hdrs: String2String = {
|
|
52
|
+
'Authorization': 'Bearer token',
|
|
53
|
+
'Content-Type': 'text/plain', // This should be overwritten
|
|
54
|
+
'Content-Length': '999' // This should be overwritten
|
|
55
|
+
};
|
|
56
|
+
addJsonBody(target, body, hdrs);
|
|
57
|
+
expect(hdrs['Authorization']).to.equal('Bearer token');
|
|
58
|
+
expect(hdrs['Content-Type']).to.equal('application/json');
|
|
59
|
+
expect(hdrs['Content-Length']).to.equal(target.body.length.toString());
|
|
60
|
+
expect(hdrs['Content-Length']).not.to.equal('999');
|
|
61
|
+
})
|
|
62
|
+
it('handle primitive values as body', function () {
|
|
63
|
+
const target1: Bodyful = {};
|
|
64
|
+
const hdrs1: String2String = {};
|
|
65
|
+
addJsonBody(target1, 'string value', hdrs1);
|
|
66
|
+
const td = new TextDecoder();
|
|
67
|
+
expect(JSON.parse(td.decode(target1.body))).to.equal('string value');
|
|
68
|
+
|
|
69
|
+
const target2: Bodyful = {};
|
|
70
|
+
const hdrs2: String2String = {};
|
|
71
|
+
addJsonBody(target2, 42, hdrs2);
|
|
72
|
+
expect(JSON.parse(td.decode(target2.body))).to.equal(42);
|
|
73
|
+
|
|
74
|
+
const target3: Bodyful = {};
|
|
75
|
+
const hdrs3: String2String = {};
|
|
76
|
+
addJsonBody(target3, true, hdrs3);
|
|
77
|
+
expect(JSON.parse(td.decode(target3.body))).to.equal(true);
|
|
78
|
+
})
|
|
79
|
+
})
|
|
80
|
+
|
|
81
|
+
describe('str2integer', function () {
|
|
82
|
+
it('convert string to integer', function () {
|
|
83
|
+
expect(str2integer('10')).to.equal(10);
|
|
84
|
+
})
|
|
85
|
+
it('return null for null input', function () {
|
|
86
|
+
expect(str2integer(null)).to.be.null;
|
|
87
|
+
})
|
|
88
|
+
it('handle edge cases and invalid inputs', function () {
|
|
89
|
+
// Valid cases
|
|
90
|
+
expect(str2integer('0')).to.equal(0);
|
|
91
|
+
expect(str2integer('-5')).to.equal(-5);
|
|
92
|
+
expect(str2integer('123')).to.equal(123);
|
|
93
|
+
|
|
94
|
+
// Whitespace handling
|
|
95
|
+
expect(str2integer(' 42 ')).to.equal(42);
|
|
96
|
+
|
|
97
|
+
// Invalid inputs - parseInt behavior
|
|
98
|
+
expect(str2integer('abc')).to.be.NaN;
|
|
99
|
+
expect(str2integer('')).to.be.NaN;
|
|
100
|
+
expect(str2integer('12.5')).to.equal(12); // parseInt truncates
|
|
101
|
+
expect(str2integer('12abc')).to.equal(12); // parseInt parses partial
|
|
102
|
+
expect(str2integer('abc123')).to.be.NaN;
|
|
103
|
+
|
|
104
|
+
// Special cases
|
|
105
|
+
expect(str2integer('Infinity')).to.be.NaN;
|
|
106
|
+
expect(str2integer('-Infinity')).to.be.NaN;
|
|
107
|
+
expect(str2integer('NaN')).to.be.NaN;
|
|
108
|
+
})
|
|
109
|
+
})
|
|
110
|
+
|
|
111
|
+
describe('str2float', function () {
|
|
112
|
+
it('convert string to float', function () {
|
|
113
|
+
expect(str2float('10.5')).to.equal(10.5);
|
|
114
|
+
})
|
|
115
|
+
it('return null for null input', function () {
|
|
116
|
+
expect(str2float(null)).to.be.null;
|
|
117
|
+
})
|
|
118
|
+
it('handle edge cases and invalid inputs', function () {
|
|
119
|
+
// Valid cases
|
|
120
|
+
expect(str2float('0')).to.equal(0);
|
|
121
|
+
expect(str2float('-5.5')).to.equal(-5.5);
|
|
122
|
+
expect(str2float('123.456')).to.equal(123.456);
|
|
123
|
+
expect(str2float('123')).to.equal(123);
|
|
124
|
+
|
|
125
|
+
// Scientific notation
|
|
126
|
+
expect(str2float('1.23e5')).to.equal(123000);
|
|
127
|
+
expect(str2float('1.23e-5')).to.equal(0.0000123);
|
|
128
|
+
|
|
129
|
+
// Whitespace handling
|
|
130
|
+
expect(str2float(' 42.5 ')).to.equal(42.5);
|
|
131
|
+
|
|
132
|
+
// Invalid inputs
|
|
133
|
+
expect(str2float('abc')).to.be.NaN;
|
|
134
|
+
expect(str2float('')).to.be.NaN;
|
|
135
|
+
expect(str2float('12.5abc')).to.equal(12.5); // parseFloat parses partial
|
|
136
|
+
expect(str2float('abc12.5')).to.be.NaN;
|
|
137
|
+
|
|
138
|
+
// Special values
|
|
139
|
+
expect(str2float('Infinity')).to.equal(Infinity);
|
|
140
|
+
expect(str2float('-Infinity')).to.equal(-Infinity);
|
|
141
|
+
expect(str2float('NaN')).to.be.NaN;
|
|
142
|
+
})
|
|
143
|
+
})
|
|
144
|
+
|
|
145
|
+
describe('isSchema', function () {
|
|
146
|
+
// From claude-3-7-sonnet-20250219 with minor modifications.
|
|
147
|
+
it('validates required fields correctly', function () {
|
|
148
|
+
// Setup schema checkers with required fields
|
|
149
|
+
const checkers: SchemaCheckers = {
|
|
150
|
+
requiredKeys: ['name'],
|
|
151
|
+
required: {
|
|
152
|
+
'name': {
|
|
153
|
+
base: (x) => x,
|
|
154
|
+
is: (x) => typeof x === 'string'
|
|
155
|
+
}
|
|
156
|
+
},
|
|
157
|
+
optional: {},
|
|
158
|
+
patterns: []
|
|
159
|
+
};
|
|
160
|
+
|
|
161
|
+
// Valid case - has required field with correct type
|
|
162
|
+
expect(isSchema({ name: 'test' }, checkers)).to.be.true;
|
|
163
|
+
|
|
164
|
+
// Invalid case - has required field with wrong type
|
|
165
|
+
expect(isSchema({ name: 123 }, checkers)).to.be.false;
|
|
166
|
+
|
|
167
|
+
// Invalid case - missing required field
|
|
168
|
+
expect(isSchema({}, checkers)).to.be.false;
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
it('validates optional fields correctly', function () {
|
|
172
|
+
// Setup schema checkers with optional fields
|
|
173
|
+
const checkers: SchemaCheckers = {
|
|
174
|
+
requiredKeys: [],
|
|
175
|
+
required: {},
|
|
176
|
+
optional: {
|
|
177
|
+
'age': {
|
|
178
|
+
base: (x) => x,
|
|
179
|
+
is: (x) => typeof x === 'number'
|
|
180
|
+
}
|
|
181
|
+
},
|
|
182
|
+
patterns: []
|
|
183
|
+
};
|
|
184
|
+
|
|
185
|
+
// Valid case - has optional field with correct type
|
|
186
|
+
expect(isSchema({ age: 25 }, checkers)).to.be.true;
|
|
187
|
+
|
|
188
|
+
// Valid case - missing optional field
|
|
189
|
+
expect(isSchema({}, checkers)).to.be.true;
|
|
190
|
+
|
|
191
|
+
// Invalid case - has optional field with wrong type
|
|
192
|
+
expect(isSchema({ age: '25' }, checkers)).to.be.false;
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
it('validates pattern fields correctly', function () {
|
|
196
|
+
// Setup schema checkers with pattern fields
|
|
197
|
+
const checkers: SchemaCheckers = {
|
|
198
|
+
requiredKeys: [],
|
|
199
|
+
required: {},
|
|
200
|
+
optional: {},
|
|
201
|
+
patterns: [
|
|
202
|
+
{
|
|
203
|
+
re: new RegExp('^prefix_.*$'),
|
|
204
|
+
funcs: {
|
|
205
|
+
base: (x) => x,
|
|
206
|
+
is: (x) => typeof x === 'boolean'
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
]
|
|
210
|
+
};
|
|
211
|
+
|
|
212
|
+
// Valid case - has pattern field with correct type
|
|
213
|
+
expect(isSchema({ prefix_field: true }, checkers)).to.be.true;
|
|
214
|
+
|
|
215
|
+
// Invalid case - has pattern field with wrong type
|
|
216
|
+
expect(isSchema({ prefix_field: 'true' }, checkers)).to.be.false;
|
|
217
|
+
|
|
218
|
+
// Valid case - no pattern fields
|
|
219
|
+
expect(isSchema({}, checkers)).to.be.true;
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
it('validates additional fields correctly', function () {
|
|
223
|
+
// Setup schema checkers with additional fields
|
|
224
|
+
const checkers: SchemaCheckers = {
|
|
225
|
+
requiredKeys: [],
|
|
226
|
+
required: {},
|
|
227
|
+
optional: {},
|
|
228
|
+
patterns: [],
|
|
229
|
+
additional: {
|
|
230
|
+
is: (x) => typeof x === 'number'
|
|
231
|
+
}
|
|
232
|
+
};
|
|
233
|
+
|
|
234
|
+
// Valid case - has additional field with correct type
|
|
235
|
+
expect(isSchema({ random_field: 42 }, checkers)).to.be.true;
|
|
236
|
+
|
|
237
|
+
// Invalid case - has additional field with wrong type
|
|
238
|
+
expect(isSchema({ random_field: 'string' }, checkers)).to.be.false;
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
it('validates complex schemas correctly', function () {
|
|
242
|
+
// Setup complex schema checkers
|
|
243
|
+
const checkers: SchemaCheckers = {
|
|
244
|
+
requiredKeys: ['id'],
|
|
245
|
+
required: {
|
|
246
|
+
'id': {
|
|
247
|
+
base: (x) => x,
|
|
248
|
+
is: (x) => typeof x === 'number'
|
|
249
|
+
}
|
|
250
|
+
},
|
|
251
|
+
optional: {
|
|
252
|
+
'name': {
|
|
253
|
+
base: (x) => x,
|
|
254
|
+
is: (x) => typeof x === 'string'
|
|
255
|
+
}
|
|
256
|
+
},
|
|
257
|
+
patterns: [
|
|
258
|
+
{
|
|
259
|
+
re: new RegExp('^meta_.*$'),
|
|
260
|
+
funcs: {
|
|
261
|
+
base: (x) => x,
|
|
262
|
+
is: (x) => typeof x === 'string'
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
],
|
|
266
|
+
additional: {
|
|
267
|
+
is: (x) => typeof x === 'boolean'
|
|
268
|
+
}
|
|
269
|
+
};
|
|
270
|
+
|
|
271
|
+
// Valid case - complex object with all types of fields
|
|
272
|
+
expect(isSchema({
|
|
273
|
+
id: 1,
|
|
274
|
+
name: 'test',
|
|
275
|
+
meta_created: '2023-01-01',
|
|
276
|
+
random_field: true
|
|
277
|
+
}, checkers)).to.be.true;
|
|
278
|
+
|
|
279
|
+
// Invalid case - missing required field
|
|
280
|
+
expect(isSchema({
|
|
281
|
+
name: 'test',
|
|
282
|
+
meta_created: '2023-01-01',
|
|
283
|
+
random_field: true
|
|
284
|
+
}, checkers)).to.be.false;
|
|
285
|
+
|
|
286
|
+
// Invalid case - wrong type for pattern field
|
|
287
|
+
expect(isSchema({
|
|
288
|
+
id: 1,
|
|
289
|
+
name: 'test',
|
|
290
|
+
meta_created: 123,
|
|
291
|
+
random_field: true
|
|
292
|
+
}, checkers)).to.be.false;
|
|
293
|
+
|
|
294
|
+
// Invalid case - wrong type for additional field
|
|
295
|
+
expect(isSchema({
|
|
296
|
+
id: 1,
|
|
297
|
+
name: 'test',
|
|
298
|
+
meta_created: '2023-01-01',
|
|
299
|
+
random_field: 'not-boolean'
|
|
300
|
+
}, checkers)).to.be.false;
|
|
301
|
+
});
|
|
302
|
+
|
|
303
|
+
it('handles multiple pattern matches correctly', function () {
|
|
304
|
+
const checkers: SchemaCheckers = {
|
|
305
|
+
requiredKeys: [],
|
|
306
|
+
required: {},
|
|
307
|
+
optional: {},
|
|
308
|
+
patterns: [
|
|
309
|
+
{
|
|
310
|
+
re: new RegExp('^test_.*$'),
|
|
311
|
+
funcs: {
|
|
312
|
+
base: (x) => `pattern1:${x}`,
|
|
313
|
+
is: (x) => typeof x === 'string'
|
|
314
|
+
}
|
|
315
|
+
},
|
|
316
|
+
{
|
|
317
|
+
re: new RegExp('.*_suffix$'),
|
|
318
|
+
funcs: {
|
|
319
|
+
base: (x) => `pattern2:${x}`,
|
|
320
|
+
is: (x) => typeof x === 'string'
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
]
|
|
324
|
+
};
|
|
325
|
+
|
|
326
|
+
// Field matches first pattern only
|
|
327
|
+
expect(isSchema({ test_field: 'value' }, checkers)).to.be.true;
|
|
328
|
+
// Field matches both patterns - should validate against the first match
|
|
329
|
+
expect(isSchema({ test_field_suffix: 'value' }, checkers)).to.be.true;
|
|
330
|
+
// Invalid type for pattern
|
|
331
|
+
expect(isSchema({ test_field: 123 }, checkers)).to.be.false;
|
|
332
|
+
});
|
|
333
|
+
|
|
334
|
+
it('handles empty and edge case schema configurations', function () {
|
|
335
|
+
// Empty checkers
|
|
336
|
+
const emptyCheckers: SchemaCheckers = {
|
|
337
|
+
requiredKeys: [],
|
|
338
|
+
required: {},
|
|
339
|
+
optional: {},
|
|
340
|
+
patterns: []
|
|
341
|
+
};
|
|
342
|
+
expect(isSchema({}, emptyCheckers)).to.be.true;
|
|
343
|
+
expect(isSchema({ any_field: 'any_value' }, emptyCheckers)).to.be.true;
|
|
344
|
+
|
|
345
|
+
// Only required keys, no validation functions
|
|
346
|
+
const noFuncCheckers: SchemaCheckers = {
|
|
347
|
+
requiredKeys: ['id'],
|
|
348
|
+
required: {},
|
|
349
|
+
optional: {},
|
|
350
|
+
patterns: []
|
|
351
|
+
};
|
|
352
|
+
expect(isSchema({ id: 'anything' }, noFuncCheckers)).to.be.true;
|
|
353
|
+
expect(isSchema({}, noFuncCheckers)).to.be.false;
|
|
354
|
+
});
|
|
355
|
+
|
|
356
|
+
it('handles objects with special properties', function () {
|
|
357
|
+
const checkers: SchemaCheckers = {
|
|
358
|
+
requiredKeys: [],
|
|
359
|
+
required: {},
|
|
360
|
+
optional: {},
|
|
361
|
+
patterns: [],
|
|
362
|
+
additional: {
|
|
363
|
+
is: (x) => true // Accept anything
|
|
364
|
+
}
|
|
365
|
+
};
|
|
366
|
+
|
|
367
|
+
// Objects with prototype properties should still work
|
|
368
|
+
const obj = Object.create({ inheritedProp: 'inherited' });
|
|
369
|
+
obj.ownProp = 'own';
|
|
370
|
+
expect(isSchema(obj, checkers)).to.be.true;
|
|
371
|
+
|
|
372
|
+
// Object with null prototype
|
|
373
|
+
const nullProtoObj = Object.create(null);
|
|
374
|
+
nullProtoObj.prop = 'value';
|
|
375
|
+
expect(isSchema(nullProtoObj, checkers)).to.be.true;
|
|
376
|
+
});
|
|
377
|
+
|
|
378
|
+
it('validates field precedence correctly', function () {
|
|
379
|
+
// Test precedence: required > optional > patterns > additional
|
|
380
|
+
const checkers: SchemaCheckers = {
|
|
381
|
+
requiredKeys: [],
|
|
382
|
+
required: {
|
|
383
|
+
'field': {
|
|
384
|
+
base: (x) => 'required',
|
|
385
|
+
is: (x) => typeof x === 'string'
|
|
386
|
+
}
|
|
387
|
+
},
|
|
388
|
+
optional: {
|
|
389
|
+
'field': { // Same field name - should be ignored in favor of required
|
|
390
|
+
base: (x) => 'optional',
|
|
391
|
+
is: (x) => typeof x === 'number'
|
|
392
|
+
}
|
|
393
|
+
},
|
|
394
|
+
patterns: [
|
|
395
|
+
{
|
|
396
|
+
re: new RegExp('^field$'),
|
|
397
|
+
funcs: {
|
|
398
|
+
base: (x) => 'pattern',
|
|
399
|
+
is: (x) => typeof x === 'boolean'
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
],
|
|
403
|
+
additional: {
|
|
404
|
+
is: (x) => typeof x === 'object'
|
|
405
|
+
}
|
|
406
|
+
};
|
|
407
|
+
|
|
408
|
+
// Should validate as string (required), not number (optional) or boolean (pattern)
|
|
409
|
+
expect(isSchema({ field: 'test' }, checkers)).to.be.true;
|
|
410
|
+
expect(isSchema({ field: 123 }, checkers)).to.be.false;
|
|
411
|
+
expect(isSchema({ field: true }, checkers)).to.be.false;
|
|
412
|
+
});
|
|
413
|
+
});
|
|
414
|
+
|
|
415
|
+
describe('baseSchema', function () {
|
|
416
|
+
// From claude-3-7-sonnet-20250219 with minor modifications.
|
|
417
|
+
it('processes required fields correctly', function () {
|
|
418
|
+
// Setup schema checkers with required fields
|
|
419
|
+
const checkers: SchemaCheckers = {
|
|
420
|
+
requiredKeys: [],
|
|
421
|
+
required: {
|
|
422
|
+
'name': {
|
|
423
|
+
is: (x) => typeof x === 'string',
|
|
424
|
+
base: (x) => x.toUpperCase()
|
|
425
|
+
}
|
|
426
|
+
},
|
|
427
|
+
optional: {},
|
|
428
|
+
patterns: []
|
|
429
|
+
};
|
|
430
|
+
|
|
431
|
+
// Test with required field
|
|
432
|
+
const result = baseSchema({ name: 'test' }, checkers);
|
|
433
|
+
expect(result).to.deep.equal({ name: 'TEST' });
|
|
434
|
+
});
|
|
435
|
+
|
|
436
|
+
it('processes optional fields correctly', function () {
|
|
437
|
+
// Setup schema checkers with optional fields
|
|
438
|
+
const checkers: SchemaCheckers = {
|
|
439
|
+
requiredKeys: [],
|
|
440
|
+
required: {},
|
|
441
|
+
optional: {
|
|
442
|
+
'age': {
|
|
443
|
+
is: (x) => typeof x === 'number',
|
|
444
|
+
base: (x) => x * 2
|
|
445
|
+
}
|
|
446
|
+
},
|
|
447
|
+
patterns: []
|
|
448
|
+
};
|
|
449
|
+
|
|
450
|
+
// Test with optional field
|
|
451
|
+
const result = baseSchema({ age: 25 }, checkers);
|
|
452
|
+
expect(result).to.deep.equal({ age: 50 });
|
|
453
|
+
|
|
454
|
+
// Test without optional field
|
|
455
|
+
const emptyResult = baseSchema({}, checkers);
|
|
456
|
+
expect(emptyResult).to.deep.equal({});
|
|
457
|
+
});
|
|
458
|
+
|
|
459
|
+
it('processes pattern fields correctly', function () {
|
|
460
|
+
// Setup schema checkers with pattern fields
|
|
461
|
+
const checkers: SchemaCheckers = {
|
|
462
|
+
requiredKeys: [],
|
|
463
|
+
required: {},
|
|
464
|
+
optional: {},
|
|
465
|
+
patterns: [
|
|
466
|
+
{
|
|
467
|
+
re: new RegExp('^prefix_.*$'),
|
|
468
|
+
funcs: {
|
|
469
|
+
is: (x) => typeof x === 'boolean',
|
|
470
|
+
base: (x) => !x // Invert boolean
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
]
|
|
474
|
+
};
|
|
475
|
+
|
|
476
|
+
// Test with pattern field
|
|
477
|
+
const result = baseSchema({ prefix_field: true }, checkers);
|
|
478
|
+
expect(result).to.deep.equal({ prefix_field: false });
|
|
479
|
+
});
|
|
480
|
+
|
|
481
|
+
it('processes additional fields correctly', function () {
|
|
482
|
+
// Setup schema checkers with additional fields
|
|
483
|
+
const checkers: SchemaCheckers = {
|
|
484
|
+
requiredKeys: [],
|
|
485
|
+
required: {},
|
|
486
|
+
optional: {},
|
|
487
|
+
patterns: [],
|
|
488
|
+
additional: {
|
|
489
|
+
is: (x) => typeof x === 'number',
|
|
490
|
+
base: (x) => x + 10
|
|
491
|
+
}
|
|
492
|
+
};
|
|
493
|
+
|
|
494
|
+
// Test with additional field
|
|
495
|
+
const result = baseSchema({ random_field: 42 }, checkers);
|
|
496
|
+
expect(result).to.deep.equal({ random_field: 52 });
|
|
497
|
+
});
|
|
498
|
+
|
|
499
|
+
it('handles additional fields without base function', function () {
|
|
500
|
+
// Setup schema checkers with additional fields but no base function
|
|
501
|
+
const checkers: SchemaCheckers = {
|
|
502
|
+
requiredKeys: [],
|
|
503
|
+
required: {},
|
|
504
|
+
optional: {},
|
|
505
|
+
patterns: [],
|
|
506
|
+
additional: {
|
|
507
|
+
is: (x) => typeof x === 'number'
|
|
508
|
+
// No base function
|
|
509
|
+
}
|
|
510
|
+
};
|
|
511
|
+
|
|
512
|
+
// Test with additional field
|
|
513
|
+
const result = baseSchema({ random_field: 42 }, checkers);
|
|
514
|
+
expect(result).to.deep.equal({}); // Should not include the field
|
|
515
|
+
});
|
|
516
|
+
|
|
517
|
+
it('processes complex schemas correctly', function () {
|
|
518
|
+
// Setup complex schema checkers
|
|
519
|
+
const checkers: SchemaCheckers = {
|
|
520
|
+
requiredKeys: [],
|
|
521
|
+
required: {
|
|
522
|
+
'id': {
|
|
523
|
+
is: (x) => typeof x === 'number',
|
|
524
|
+
base: (x) => x.toString()
|
|
525
|
+
}
|
|
526
|
+
},
|
|
527
|
+
optional: {
|
|
528
|
+
'name': {
|
|
529
|
+
is: (x) => typeof x === 'string',
|
|
530
|
+
base: (x) => x.toUpperCase()
|
|
531
|
+
}
|
|
532
|
+
},
|
|
533
|
+
patterns: [
|
|
534
|
+
{
|
|
535
|
+
re: new RegExp('^meta_.*$'),
|
|
536
|
+
funcs: {
|
|
537
|
+
is: (x) => typeof x === 'string',
|
|
538
|
+
base: (x) => x.substring(0, 10) // First 10 chars
|
|
539
|
+
}
|
|
540
|
+
}
|
|
541
|
+
],
|
|
542
|
+
additional: {
|
|
543
|
+
is: (x) => typeof x === 'boolean',
|
|
544
|
+
base: (x) => !x // Invert boolean
|
|
545
|
+
}
|
|
546
|
+
};
|
|
547
|
+
|
|
548
|
+
// Test with complex object
|
|
549
|
+
const result = baseSchema({
|
|
550
|
+
id: 1,
|
|
551
|
+
name: 'test',
|
|
552
|
+
meta_created: '2023-01-01T12:34:56Z',
|
|
553
|
+
random_field: true
|
|
554
|
+
}, checkers);
|
|
555
|
+
|
|
556
|
+
expect(result).to.deep.equal({
|
|
557
|
+
id: '1',
|
|
558
|
+
name: 'TEST',
|
|
559
|
+
meta_created: '2023-01-01',
|
|
560
|
+
random_field: false
|
|
561
|
+
});
|
|
562
|
+
});
|
|
563
|
+
|
|
564
|
+
it('handles multiple pattern matches and precedence', function () {
|
|
565
|
+
const checkers: SchemaCheckers = {
|
|
566
|
+
requiredKeys: [],
|
|
567
|
+
required: {},
|
|
568
|
+
optional: {},
|
|
569
|
+
patterns: [
|
|
570
|
+
{
|
|
571
|
+
re: new RegExp('^test_.*$'),
|
|
572
|
+
funcs: {
|
|
573
|
+
is: (x) => typeof x === 'string',
|
|
574
|
+
base: (x) => `first:${x}`
|
|
575
|
+
}
|
|
576
|
+
},
|
|
577
|
+
{
|
|
578
|
+
re: new RegExp('.*_suffix$'),
|
|
579
|
+
funcs: {
|
|
580
|
+
is: (x) => typeof x === 'string',
|
|
581
|
+
base: (x) => `second:${x}`
|
|
582
|
+
}
|
|
583
|
+
}
|
|
584
|
+
]
|
|
585
|
+
};
|
|
586
|
+
|
|
587
|
+
// Should use first matching pattern
|
|
588
|
+
const result = baseSchema({ test_field_suffix: 'value' }, checkers);
|
|
589
|
+
expect(result).to.deep.equal({ test_field_suffix: 'first:value' });
|
|
590
|
+
});
|
|
591
|
+
|
|
592
|
+
it('handles field precedence in processing', function () {
|
|
593
|
+
const checkers: SchemaCheckers = {
|
|
594
|
+
requiredKeys: [],
|
|
595
|
+
required: {
|
|
596
|
+
'field': {
|
|
597
|
+
is: (x) => typeof x === 'string',
|
|
598
|
+
base: (x) => `required:${x}`
|
|
599
|
+
}
|
|
600
|
+
},
|
|
601
|
+
optional: {
|
|
602
|
+
'field': { // Same field - should be ignored
|
|
603
|
+
is: (x) => typeof x === 'string',
|
|
604
|
+
base: (x) => `optional:${x}`
|
|
605
|
+
}
|
|
606
|
+
},
|
|
607
|
+
patterns: [
|
|
608
|
+
{
|
|
609
|
+
re: new RegExp('^field$'),
|
|
610
|
+
funcs: {
|
|
611
|
+
is: (x) => typeof x === 'string',
|
|
612
|
+
base: (x) => `pattern:${x}`
|
|
613
|
+
}
|
|
614
|
+
}
|
|
615
|
+
],
|
|
616
|
+
additional: {
|
|
617
|
+
is: (x) => typeof x === 'string',
|
|
618
|
+
base: (x) => `additional:${x}`
|
|
619
|
+
}
|
|
620
|
+
};
|
|
621
|
+
|
|
622
|
+
const result = baseSchema({ field: 'test' }, checkers);
|
|
623
|
+
expect(result).to.deep.equal({ field: 'required:test' });
|
|
624
|
+
});
|
|
625
|
+
|
|
626
|
+
it('handles empty configurations and edge cases', function () {
|
|
627
|
+
// Empty checkers
|
|
628
|
+
const emptyCheckers: SchemaCheckers = {
|
|
629
|
+
requiredKeys: [],
|
|
630
|
+
required: {},
|
|
631
|
+
optional: {},
|
|
632
|
+
patterns: []
|
|
633
|
+
};
|
|
634
|
+
expect(baseSchema({ field: 'value' }, emptyCheckers)).to.deep.equal({});
|
|
635
|
+
|
|
636
|
+
// Checkers with empty patterns array
|
|
637
|
+
const noPatternCheckers: SchemaCheckers = {
|
|
638
|
+
requiredKeys: [],
|
|
639
|
+
required: {},
|
|
640
|
+
optional: {},
|
|
641
|
+
patterns: [],
|
|
642
|
+
additional: {
|
|
643
|
+
is: (x) => true,
|
|
644
|
+
base: (x) => `processed:${x}`
|
|
645
|
+
}
|
|
646
|
+
};
|
|
647
|
+
const result = baseSchema({ field: 'value' }, noPatternCheckers);
|
|
648
|
+
expect(result).to.deep.equal({ field: 'processed:value' });
|
|
649
|
+
});
|
|
650
|
+
|
|
651
|
+
it('handles objects with special properties correctly', function () {
|
|
652
|
+
const checkers: SchemaCheckers = {
|
|
653
|
+
requiredKeys: [],
|
|
654
|
+
required: {},
|
|
655
|
+
optional: {},
|
|
656
|
+
patterns: [],
|
|
657
|
+
additional: {
|
|
658
|
+
is: (x) => true,
|
|
659
|
+
base: (x) => `processed:${x}`
|
|
660
|
+
}
|
|
661
|
+
};
|
|
662
|
+
|
|
663
|
+
// Object with null prototype
|
|
664
|
+
const nullProtoObj = Object.create(null);
|
|
665
|
+
nullProtoObj.prop = 'value';
|
|
666
|
+
const result = baseSchema(nullProtoObj, checkers);
|
|
667
|
+
expect(result).to.deep.equal({ prop: 'processed:value' });
|
|
668
|
+
});
|
|
669
|
+
});
|
|
670
|
+
|
|
671
|
+
// Integration tests combining multiple functions
|
|
672
|
+
describe('Integration Tests', function () {
|
|
673
|
+
it('combines addJsonBody with realistic API request scenario', function () {
|
|
674
|
+
// Simulate preparing an API request with complex data
|
|
675
|
+
const target: Bodyful = {};
|
|
676
|
+
const requestData = {
|
|
677
|
+
user: {
|
|
678
|
+
id: str2integer('123'),
|
|
679
|
+
score: str2float('98.5')
|
|
680
|
+
},
|
|
681
|
+
metadata: {
|
|
682
|
+
timestamp: new Date().toISOString(),
|
|
683
|
+
version: '1.0'
|
|
684
|
+
}
|
|
685
|
+
};
|
|
686
|
+
const headers: String2String = {
|
|
687
|
+
'Authorization': 'Bearer token123',
|
|
688
|
+
'X-Client-Version': '2.1.0'
|
|
689
|
+
};
|
|
690
|
+
|
|
691
|
+
addJsonBody(target, requestData, headers);
|
|
692
|
+
|
|
693
|
+
const td = new TextDecoder();
|
|
694
|
+
const decodedBody = JSON.parse(td.decode(target.body));
|
|
695
|
+
expect(decodedBody.user.id).to.equal(123);
|
|
696
|
+
expect(decodedBody.user.score).to.equal(98.5);
|
|
697
|
+
expect(headers['Content-Type']).to.equal('application/json');
|
|
698
|
+
expect(headers['Authorization']).to.equal('Bearer token123');
|
|
699
|
+
});
|
|
700
|
+
|
|
701
|
+
it('validates and processes API response with schema functions', function () {
|
|
702
|
+
// Simulate processing an API response
|
|
703
|
+
const apiResponse = {
|
|
704
|
+
user_id: 42,
|
|
705
|
+
user_name: 'john_doe',
|
|
706
|
+
meta_created: '2023-01-01T10:30:00Z',
|
|
707
|
+
meta_updated: '2023-01-02T15:45:00Z',
|
|
708
|
+
is_active: true,
|
|
709
|
+
score: 98.5
|
|
710
|
+
};
|
|
711
|
+
|
|
712
|
+
const checkers: SchemaCheckers = {
|
|
713
|
+
requiredKeys: ['user_id', 'user_name'],
|
|
714
|
+
required: {
|
|
715
|
+
'user_id': {
|
|
716
|
+
is: (x) => typeof x === 'number',
|
|
717
|
+
base: (x) => x.toString()
|
|
718
|
+
},
|
|
719
|
+
'user_name': {
|
|
720
|
+
is: (x) => typeof x === 'string',
|
|
721
|
+
base: (x) => x.replace('_', ' ').toUpperCase()
|
|
722
|
+
}
|
|
723
|
+
},
|
|
724
|
+
optional: {
|
|
725
|
+
'score': {
|
|
726
|
+
is: (x) => typeof x === 'number',
|
|
727
|
+
base: (x) => Math.round(x)
|
|
728
|
+
}
|
|
729
|
+
},
|
|
730
|
+
patterns: [
|
|
731
|
+
{
|
|
732
|
+
re: new RegExp('^meta_.*$'),
|
|
733
|
+
funcs: {
|
|
734
|
+
is: (x) => typeof x === 'string',
|
|
735
|
+
base: (x) => new Date(x).getTime() // Convert to timestamp
|
|
736
|
+
}
|
|
737
|
+
}
|
|
738
|
+
],
|
|
739
|
+
additional: {
|
|
740
|
+
is: (x) => typeof x === 'boolean',
|
|
741
|
+
base: (x) => x ? 1 : 0 // Convert boolean to number
|
|
742
|
+
}
|
|
743
|
+
};
|
|
744
|
+
|
|
745
|
+
// First validate the response
|
|
746
|
+
expect(isSchema(apiResponse, checkers)).to.be.true;
|
|
747
|
+
|
|
748
|
+
// Then process it
|
|
749
|
+
const processed = baseSchema(apiResponse, checkers);
|
|
750
|
+
expect(processed).to.deep.equal({
|
|
751
|
+
user_id: '42',
|
|
752
|
+
user_name: 'JOHN DOE',
|
|
753
|
+
score: 99,
|
|
754
|
+
meta_created: new Date('2023-01-01T10:30:00Z').getTime(),
|
|
755
|
+
meta_updated: new Date('2023-01-02T15:45:00Z').getTime(),
|
|
756
|
+
is_active: 1
|
|
757
|
+
});
|
|
758
|
+
});
|
|
759
|
+
|
|
760
|
+
it('handles string conversion in realistic data transformation pipeline', function () {
|
|
761
|
+
// Simulate processing query parameters or form data
|
|
762
|
+
const queryParams = {
|
|
763
|
+
page: '1',
|
|
764
|
+
limit: '25',
|
|
765
|
+
score_min: '75.5',
|
|
766
|
+
score_max: '100.0',
|
|
767
|
+
active: 'true'
|
|
768
|
+
};
|
|
769
|
+
|
|
770
|
+
// Transform query parameters to proper types
|
|
771
|
+
const processedParams = {
|
|
772
|
+
page: str2integer(queryParams.page),
|
|
773
|
+
limit: str2integer(queryParams.limit),
|
|
774
|
+
score_min: str2float(queryParams.score_min),
|
|
775
|
+
score_max: str2float(queryParams.score_max),
|
|
776
|
+
active: queryParams.active === 'true'
|
|
777
|
+
};
|
|
778
|
+
|
|
779
|
+
expect(processedParams).to.deep.equal({
|
|
780
|
+
page: 1,
|
|
781
|
+
limit: 25,
|
|
782
|
+
score_min: 75.5,
|
|
783
|
+
score_max: 100.0,
|
|
784
|
+
active: true
|
|
785
|
+
});
|
|
786
|
+
|
|
787
|
+
// Use processed params in request body
|
|
788
|
+
const target: Bodyful = {};
|
|
789
|
+
const headers: String2String = {};
|
|
790
|
+
addJsonBody(target, processedParams, headers);
|
|
791
|
+
|
|
792
|
+
const td = new TextDecoder();
|
|
793
|
+
const decodedBody = JSON.parse(td.decode(target.body));
|
|
794
|
+
expect(decodedBody.page).to.equal(1);
|
|
795
|
+
expect(decodedBody.score_min).to.equal(75.5);
|
|
796
|
+
});
|
|
797
|
+
|
|
798
|
+
it('handles malformed data gracefully in integration scenario', function () {
|
|
799
|
+
// Test with invalid string inputs that would produce NaN
|
|
800
|
+
const malformedData = {
|
|
801
|
+
id: str2integer('not-a-number'),
|
|
802
|
+
score: str2float('invalid-float'),
|
|
803
|
+
valid_field: 'test'
|
|
804
|
+
};
|
|
805
|
+
|
|
806
|
+
expect(malformedData.id).to.be.NaN;
|
|
807
|
+
expect(malformedData.score).to.be.NaN;
|
|
808
|
+
|
|
809
|
+
// Schema validation should handle NaN values appropriately
|
|
810
|
+
const checkers: SchemaCheckers = {
|
|
811
|
+
requiredKeys: [],
|
|
812
|
+
required: {
|
|
813
|
+
'id': {
|
|
814
|
+
is: (x) => typeof x === 'number' && !isNaN(x),
|
|
815
|
+
base: (x) => x
|
|
816
|
+
}
|
|
817
|
+
},
|
|
818
|
+
optional: {},
|
|
819
|
+
patterns: [],
|
|
820
|
+
additional: {
|
|
821
|
+
is: (x) => typeof x === 'string',
|
|
822
|
+
base: (x) => x
|
|
823
|
+
}
|
|
824
|
+
};
|
|
825
|
+
|
|
826
|
+
// Should be invalid due to NaN id
|
|
827
|
+
expect(isSchema(malformedData, checkers)).to.be.false;
|
|
828
|
+
|
|
829
|
+
// But if we remove the invalid fields, it should work
|
|
830
|
+
const cleanData = { valid_field: 'test' };
|
|
831
|
+
expect(isSchema(cleanData, checkers)).to.be.true;
|
|
832
|
+
expect(baseSchema(cleanData, checkers)).to.deep.equal({ valid_field: 'test' });
|
|
833
|
+
});
|
|
834
|
+
});
|
|
835
|
+
|
|
836
|
+
describe('setExtras', function () {
|
|
837
|
+
it('returns empty object when thisExtras is undefined and extras is empty', function () {
|
|
838
|
+
const result = setExtras(undefined, {});
|
|
839
|
+
expect(result).to.deep.equal({});
|
|
840
|
+
});
|
|
841
|
+
|
|
842
|
+
it('returns empty object when both thisExtras and extras have no query or headers', function () {
|
|
843
|
+
const result = setExtras({}, {});
|
|
844
|
+
expect(result).to.deep.equal({});
|
|
845
|
+
});
|
|
846
|
+
|
|
847
|
+
it('includes query when only extras has query', function () {
|
|
848
|
+
const result = setExtras(undefined, { query: { key: 'value' } });
|
|
849
|
+
expect(result.query).to.deep.equal({ key: 'value' });
|
|
850
|
+
expect(result.headers).to.be.undefined;
|
|
851
|
+
});
|
|
852
|
+
|
|
853
|
+
it('includes query when only thisExtras has query', function () {
|
|
854
|
+
const result = setExtras({ query: { key: 'value' } }, {});
|
|
855
|
+
expect(result.query).to.deep.equal({ key: 'value' });
|
|
856
|
+
});
|
|
857
|
+
|
|
858
|
+
it('merges query from both thisExtras and extras, extras overrides thisExtras', function () {
|
|
859
|
+
const result = setExtras(
|
|
860
|
+
{ query: { a: 'from-this', b: 'from-this' } },
|
|
861
|
+
{ query: { b: 'from-extras', c: 'from-extras' } }
|
|
862
|
+
);
|
|
863
|
+
expect(result.query).to.deep.equal({ a: 'from-this', b: 'from-extras', c: 'from-extras' });
|
|
864
|
+
});
|
|
865
|
+
|
|
866
|
+
it('includes headers when only extras has headers', function () {
|
|
867
|
+
const result = setExtras(undefined, { headers: { Authorization: 'Bearer token' } });
|
|
868
|
+
expect(result.headers).to.deep.equal({ Authorization: 'Bearer token' });
|
|
869
|
+
expect(result.query).to.be.undefined;
|
|
870
|
+
});
|
|
871
|
+
|
|
872
|
+
it('includes headers when only thisExtras has headers', function () {
|
|
873
|
+
const result = setExtras({ headers: { Authorization: 'Bearer token' } }, {});
|
|
874
|
+
expect(result.headers).to.deep.equal({ Authorization: 'Bearer token' });
|
|
875
|
+
});
|
|
876
|
+
|
|
877
|
+
it('merges headers from both thisExtras and extras, extras overrides thisExtras', function () {
|
|
878
|
+
const result = setExtras(
|
|
879
|
+
{ headers: { Accept: 'text/html', Authorization: 'old-token' } },
|
|
880
|
+
{ headers: { Authorization: 'new-token', 'X-Custom': 'value' } }
|
|
881
|
+
);
|
|
882
|
+
expect(result.headers).to.deep.equal({ Accept: 'text/html', Authorization: 'new-token', 'X-Custom': 'value' });
|
|
883
|
+
});
|
|
884
|
+
|
|
885
|
+
it('merges both query and headers together', function () {
|
|
886
|
+
const result = setExtras(
|
|
887
|
+
{ query: { page: '1' }, headers: { Accept: 'application/json' } },
|
|
888
|
+
{ query: { limit: '10' }, headers: { Authorization: 'Bearer token' } }
|
|
889
|
+
);
|
|
890
|
+
expect(result.query).to.deep.equal({ page: '1', limit: '10' });
|
|
891
|
+
expect(result.headers).to.deep.equal({ Accept: 'application/json', Authorization: 'Bearer token' });
|
|
892
|
+
});
|
|
893
|
+
|
|
894
|
+
it('does not include query key when neither side has it', function () {
|
|
895
|
+
const result = setExtras(
|
|
896
|
+
{ headers: { Accept: 'application/json' } },
|
|
897
|
+
{ headers: { Authorization: 'Bearer token' } }
|
|
898
|
+
);
|
|
899
|
+
expect(result).not.to.have.property('query');
|
|
900
|
+
});
|
|
901
|
+
|
|
902
|
+
it('does not include headers key when neither side has it', function () {
|
|
903
|
+
const result = setExtras(
|
|
904
|
+
{ query: { page: '1' } },
|
|
905
|
+
{ query: { limit: '10' } }
|
|
906
|
+
);
|
|
907
|
+
expect(result).not.to.have.property('headers');
|
|
908
|
+
});
|
|
909
|
+
|
|
910
|
+
it('does not mutate the inputs', function () {
|
|
911
|
+
const thisExtras = { query: { a: '1' } };
|
|
912
|
+
const extras = { query: { b: '2' } };
|
|
913
|
+
setExtras(thisExtras, extras);
|
|
914
|
+
expect(thisExtras).to.deep.equal({ query: { a: '1' } });
|
|
915
|
+
expect(extras).to.deep.equal({ query: { b: '2' } });
|
|
916
|
+
});
|
|
917
|
+
});
|
|
918
|
+
|
|
919
|
+
describe('query', function () {
|
|
920
|
+
it('appends nothing when both classExtras and thisExtras are undefined', function () {
|
|
921
|
+
const out: Array<string> = [];
|
|
922
|
+
query(out, undefined, undefined, []);
|
|
923
|
+
expect(out).to.deep.equal([]);
|
|
924
|
+
});
|
|
925
|
+
|
|
926
|
+
it('appends nothing when both classExtras and thisExtras have no query', function () {
|
|
927
|
+
const out: Array<string> = [];
|
|
928
|
+
query(out, {}, {}, []);
|
|
929
|
+
expect(out).to.deep.equal([]);
|
|
930
|
+
});
|
|
931
|
+
|
|
932
|
+
it('appends key=value from classExtras.query', function () {
|
|
933
|
+
const out: Array<string> = [];
|
|
934
|
+
query(out, { query: { key: 'value' } }, undefined, []);
|
|
935
|
+
expect(out).to.deep.equal(['key=value']);
|
|
936
|
+
});
|
|
937
|
+
|
|
938
|
+
it('appends key=value from thisExtras.query', function () {
|
|
939
|
+
const out: Array<string> = [];
|
|
940
|
+
query(out, undefined, { query: { key: 'value' } }, []);
|
|
941
|
+
expect(out).to.deep.equal(['key=value']);
|
|
942
|
+
});
|
|
943
|
+
|
|
944
|
+
it('thisExtras.query overrides classExtras.query for the same key', function () {
|
|
945
|
+
const out: Array<string> = [];
|
|
946
|
+
query(out, { query: { key: 'class-value' } }, { query: { key: 'this-value' } }, []);
|
|
947
|
+
expect(out).to.include('key=this-value');
|
|
948
|
+
expect(out).not.to.include('key=class-value');
|
|
949
|
+
});
|
|
950
|
+
|
|
951
|
+
it('merges keys from both classExtras and thisExtras', function () {
|
|
952
|
+
const out: Array<string> = [];
|
|
953
|
+
query(out, { query: { a: '1' } }, { query: { b: '2' } }, []);
|
|
954
|
+
expect(out).to.include('a=1');
|
|
955
|
+
expect(out).to.include('b=2');
|
|
956
|
+
});
|
|
957
|
+
|
|
958
|
+
it('skips forbidden keys', function () {
|
|
959
|
+
const out: Array<string> = [];
|
|
960
|
+
query(out, { query: { skip: 'me', keep: 'this' } }, undefined, ['skip']);
|
|
961
|
+
expect(out).to.deep.equal(['keep=this']);
|
|
962
|
+
});
|
|
963
|
+
|
|
964
|
+
it('skips all entries when all keys are forbidden', function () {
|
|
965
|
+
const out: Array<string> = [];
|
|
966
|
+
query(out, { query: { a: '1', b: '2' } }, undefined, ['a', 'b']);
|
|
967
|
+
expect(out).to.deep.equal([]);
|
|
968
|
+
});
|
|
969
|
+
|
|
970
|
+
it('URI-encodes special characters in keys and values', function () {
|
|
971
|
+
const out: Array<string> = [];
|
|
972
|
+
query(out, { query: { 'key with spaces': 'value&special=chars' } }, undefined, []);
|
|
973
|
+
expect(out).to.deep.equal(['key%20with%20spaces=value%26special%3Dchars']);
|
|
974
|
+
});
|
|
975
|
+
|
|
976
|
+
it('preserves existing entries in the out array', function () {
|
|
977
|
+
const out: Array<string> = ['existing=entry'];
|
|
978
|
+
query(out, { query: { new: 'entry' } }, undefined, []);
|
|
979
|
+
expect(out).to.deep.equal(['existing=entry', 'new=entry']);
|
|
980
|
+
});
|
|
981
|
+
});
|
|
982
|
+
|
|
983
|
+
describe('headers', function () {
|
|
984
|
+
it('returns the same Headers object passed in', function () {
|
|
985
|
+
const out = new Headers();
|
|
986
|
+
const result = headers(out, undefined, undefined, []);
|
|
987
|
+
expect(result).to.equal(out);
|
|
988
|
+
});
|
|
989
|
+
|
|
990
|
+
it('returns unchanged Headers when both classExtras and thisExtras are undefined', function () {
|
|
991
|
+
const out = new Headers();
|
|
992
|
+
headers(out, undefined, undefined, []);
|
|
993
|
+
expect(Array.from(out.entries())).to.deep.equal([]);
|
|
994
|
+
});
|
|
995
|
+
|
|
996
|
+
it('returns unchanged Headers when both classExtras and thisExtras have no headers', function () {
|
|
997
|
+
const out = new Headers();
|
|
998
|
+
headers(out, {}, {}, []);
|
|
999
|
+
expect(Array.from(out.entries())).to.deep.equal([]);
|
|
1000
|
+
});
|
|
1001
|
+
|
|
1002
|
+
it('sets headers from classExtras.headers', function () {
|
|
1003
|
+
const out = new Headers();
|
|
1004
|
+
headers(out, { headers: { 'X-Custom': 'value' } }, undefined, []);
|
|
1005
|
+
expect(out.get('x-custom')).to.equal('value');
|
|
1006
|
+
});
|
|
1007
|
+
|
|
1008
|
+
it('sets headers from thisExtras.headers', function () {
|
|
1009
|
+
const out = new Headers();
|
|
1010
|
+
headers(out, undefined, { headers: { 'X-Custom': 'value' } }, []);
|
|
1011
|
+
expect(out.get('x-custom')).to.equal('value');
|
|
1012
|
+
});
|
|
1013
|
+
|
|
1014
|
+
it('thisExtras.headers overrides classExtras.headers for the same key', function () {
|
|
1015
|
+
const out = new Headers();
|
|
1016
|
+
headers(out, { headers: { Authorization: 'class-token' } }, { headers: { Authorization: 'this-token' } }, []);
|
|
1017
|
+
expect(out.get('authorization')).to.equal('this-token');
|
|
1018
|
+
});
|
|
1019
|
+
|
|
1020
|
+
it('merges keys from both classExtras and thisExtras', function () {
|
|
1021
|
+
const out = new Headers();
|
|
1022
|
+
headers(out, { headers: { Accept: 'application/json' } }, { headers: { Authorization: 'Bearer token' } }, []);
|
|
1023
|
+
expect(out.get('accept')).to.equal('application/json');
|
|
1024
|
+
expect(out.get('authorization')).to.equal('Bearer token');
|
|
1025
|
+
});
|
|
1026
|
+
|
|
1027
|
+
it('skips forbidden keys', function () {
|
|
1028
|
+
const out = new Headers();
|
|
1029
|
+
headers(out, { headers: { 'Content-Type': 'text/plain', 'X-Custom': 'value' } }, undefined, ['Content-Type']);
|
|
1030
|
+
expect(out.get('content-type')).to.be.null;
|
|
1031
|
+
expect(out.get('x-custom')).to.equal('value');
|
|
1032
|
+
});
|
|
1033
|
+
|
|
1034
|
+
it('skips all entries when all keys are forbidden', function () {
|
|
1035
|
+
const out = new Headers();
|
|
1036
|
+
headers(out, { headers: { 'Content-Type': 'text/plain' } }, undefined, ['Content-Type']);
|
|
1037
|
+
expect(Array.from(out.entries())).to.deep.equal([]);
|
|
1038
|
+
});
|
|
1039
|
+
|
|
1040
|
+
it('preserves existing headers on the Headers object', function () {
|
|
1041
|
+
const out = new Headers({ 'X-Pre-Existing': 'pre-existing' });
|
|
1042
|
+
headers(out, { headers: { 'X-New': 'new' } }, undefined, []);
|
|
1043
|
+
expect(out.get('x-pre-existing')).to.equal('pre-existing');
|
|
1044
|
+
expect(out.get('x-new')).to.equal('new');
|
|
1045
|
+
});
|
|
1046
|
+
});
|
|
1047
|
+
|
|
1048
|
+
describe('getLoader', function () {
|
|
1049
|
+
it('returns blob loader when contentType is null', function () {
|
|
1050
|
+
const classLoaders: LoaderMap = new Map();
|
|
1051
|
+
const loader = getLoader(null, undefined, undefined, classLoaders);
|
|
1052
|
+
// Verify it's the blob loader by calling it with a mock Response
|
|
1053
|
+
const mockBlob = new Blob(['test']);
|
|
1054
|
+
const mockResponse = new Response(mockBlob);
|
|
1055
|
+
return loader(mockResponse).then(result => {
|
|
1056
|
+
expect(result).to.be.instanceOf(Blob);
|
|
1057
|
+
});
|
|
1058
|
+
});
|
|
1059
|
+
|
|
1060
|
+
// Cases where classArgsLoader is undefined.
|
|
1061
|
+
|
|
1062
|
+
it('returns loader from loaders map when content type matches', function () {
|
|
1063
|
+
const expectedResult = { parsed: true };
|
|
1064
|
+
const customLoader: LoaderFunction = (_response) => Promise.resolve(expectedResult);
|
|
1065
|
+
const loaders: LoaderMap = new Map([['application/json', customLoader]]);
|
|
1066
|
+
const classLoaders: LoaderMap = new Map();
|
|
1067
|
+
const loader = getLoader('application/json', loaders, undefined, classLoaders);
|
|
1068
|
+
expect(loader).to.equal(customLoader);
|
|
1069
|
+
});
|
|
1070
|
+
|
|
1071
|
+
it('returns loader from classLoaders when not in others', function () {
|
|
1072
|
+
const expectedResult = { parsed: true };
|
|
1073
|
+
const classLoader: LoaderFunction = (_response) => Promise.resolve(expectedResult);
|
|
1074
|
+
const loaders: LoaderMap = new Map();
|
|
1075
|
+
const classLoaders: LoaderMap = new Map([['application/json', classLoader]]);
|
|
1076
|
+
const loader = getLoader('application/json', loaders, undefined, classLoaders);
|
|
1077
|
+
expect(loader).to.equal(classLoader);
|
|
1078
|
+
});
|
|
1079
|
+
|
|
1080
|
+
it('loaders map takes precedence over classLoaders', function () {
|
|
1081
|
+
const loaderResult = { source: 'loaders' };
|
|
1082
|
+
const classLoaderResult = { source: 'classLoaders' };
|
|
1083
|
+
const customLoader: LoaderFunction = (_response) => Promise.resolve(loaderResult);
|
|
1084
|
+
const classLoader: LoaderFunction = (_response) => Promise.resolve(classLoaderResult);
|
|
1085
|
+
const loaders: LoaderMap = new Map([['application/json', customLoader]]);
|
|
1086
|
+
const classLoaders: LoaderMap = new Map([['application/json', classLoader]]);
|
|
1087
|
+
const loader = getLoader('application/json', loaders, undefined, classLoaders);
|
|
1088
|
+
expect(loader).to.equal(customLoader);
|
|
1089
|
+
});
|
|
1090
|
+
|
|
1091
|
+
it('returns blob loader when content type is unknown in both maps', function () {
|
|
1092
|
+
const loaders: LoaderMap = new Map([['application/json', (_r) => Promise.resolve({})]]);
|
|
1093
|
+
const classLoaders: LoaderMap = new Map([['text/plain', (_r) => Promise.resolve({})]]);
|
|
1094
|
+
const loader = getLoader('text/html', loaders, undefined, classLoaders);
|
|
1095
|
+
const mockResponse = new Response(new Blob(['test']));
|
|
1096
|
+
return loader(mockResponse).then(result => {
|
|
1097
|
+
expect(result).to.be.instanceOf(Blob);
|
|
1098
|
+
});
|
|
1099
|
+
});
|
|
1100
|
+
|
|
1101
|
+
it('returns blob loader when loaders is undefined and content type not in classLoaders', function () {
|
|
1102
|
+
const classLoaders: LoaderMap = new Map([['application/json', (_r) => Promise.resolve({})]]);
|
|
1103
|
+
const loader = getLoader('text/html', undefined, undefined, classLoaders);
|
|
1104
|
+
const mockResponse = new Response(new Blob(['test']));
|
|
1105
|
+
return loader(mockResponse).then(result => {
|
|
1106
|
+
expect(result).to.be.instanceOf(Blob);
|
|
1107
|
+
});
|
|
1108
|
+
});
|
|
1109
|
+
|
|
1110
|
+
it('falls through to classLoaders when loaders is undefined', function () {
|
|
1111
|
+
const classLoader: LoaderFunction = (_response) => Promise.resolve('class-result');
|
|
1112
|
+
const classLoaders: LoaderMap = new Map([['text/plain', classLoader]]);
|
|
1113
|
+
const loader = getLoader('text/plain', undefined, undefined, classLoaders);
|
|
1114
|
+
expect(loader).to.equal(classLoader);
|
|
1115
|
+
});
|
|
1116
|
+
|
|
1117
|
+
it('matches content type case-insensitively (lowercased)', function () {
|
|
1118
|
+
const customLoader: LoaderFunction = (_response) => Promise.resolve({});
|
|
1119
|
+
const loaders: LoaderMap = new Map([['application/json', customLoader]]);
|
|
1120
|
+
const classLoaders: LoaderMap = new Map();
|
|
1121
|
+
// Content type with mixed case should be lowercased before lookup
|
|
1122
|
+
const loader = getLoader('Application/JSON', loaders, undefined, classLoaders);
|
|
1123
|
+
expect(loader).to.equal(customLoader);
|
|
1124
|
+
});
|
|
1125
|
+
|
|
1126
|
+
it('returns the loader that calls response.blob() for the blob loader', function () {
|
|
1127
|
+
const classLoaders: LoaderMap = new Map();
|
|
1128
|
+
const loader = getLoader(null, undefined, undefined, classLoaders);
|
|
1129
|
+
const mockBlob = new Blob(['hello']);
|
|
1130
|
+
const mockResponse = new Response(mockBlob);
|
|
1131
|
+
return loader(mockResponse).then(result => {
|
|
1132
|
+
expect(result).to.be.instanceOf(Blob);
|
|
1133
|
+
});
|
|
1134
|
+
});
|
|
1135
|
+
|
|
1136
|
+
it('returns loader content type has matching charset', function () {
|
|
1137
|
+
const expectedResult = { parsed: true };
|
|
1138
|
+
const customLoader: LoaderFunction = (_response) => Promise.resolve(expectedResult);
|
|
1139
|
+
const loaders: LoaderMap = new Map([['application/json; charset=utf-8', customLoader]]);
|
|
1140
|
+
const classLoaders: LoaderMap = new Map();
|
|
1141
|
+
let loader = getLoader('application/json; charset=utf-8', loaders, undefined, classLoaders);
|
|
1142
|
+
expect(loader).to.equal(customLoader);
|
|
1143
|
+
loader = getLoader('application/JSON ; charset=UTF-8', loaders, undefined, classLoaders);
|
|
1144
|
+
expect(loader).to.equal(customLoader);
|
|
1145
|
+
loader = getLoader('application/JSON;charset=UTF-8', loaders, undefined, classLoaders);
|
|
1146
|
+
expect(loader).to.equal(customLoader);
|
|
1147
|
+
});
|
|
1148
|
+
|
|
1149
|
+
// Cases where classArgsLoader is defined.
|
|
1150
|
+
|
|
1151
|
+
it('returns loader from classArgsLoaders when not in loaders', function () {
|
|
1152
|
+
const unexpectedResult = { parsed: false };
|
|
1153
|
+
const expectedResult = { parsed: true };
|
|
1154
|
+
const classLoader: LoaderFunction = (_response) => Promise.resolve(unexpectedResult);
|
|
1155
|
+
const classArgsLoader: LoaderFunction = (_response) => Promise.resolve(expectedResult);
|
|
1156
|
+
const loaders: LoaderMap = new Map();
|
|
1157
|
+
const classArgsLoaders: LoaderMap = new Map([['application/json', classArgsLoader]]);
|
|
1158
|
+
const classLoaders: LoaderMap = new Map([['application/json', classLoader]]);
|
|
1159
|
+
const loader = getLoader('application/json', loaders, classArgsLoaders, classLoaders);
|
|
1160
|
+
expect(loader).to.equal(classArgsLoader);
|
|
1161
|
+
});
|
|
1162
|
+
|
|
1163
|
+
it('loaders map takes precedence over classLoaders', function () {
|
|
1164
|
+
const loaderResult = { source: 'loaders' };
|
|
1165
|
+
const classLoaderResult = { source: 'classLoaders' };
|
|
1166
|
+
const customLoader: LoaderFunction = (_response) => Promise.resolve(loaderResult);
|
|
1167
|
+
const classArgsLoader: LoaderFunction = (_response) => Promise.resolve(classLoaderResult);
|
|
1168
|
+
const classLoader: LoaderFunction = (_response) => Promise.resolve(classLoaderResult);
|
|
1169
|
+
const loaders: LoaderMap = new Map([['application/json', customLoader]]);
|
|
1170
|
+
const classLoaders: LoaderMap = new Map([['application/json', classLoader]]);
|
|
1171
|
+
const classArgsLoaders: LoaderMap = new Map([['application/json', classArgsLoader]]);
|
|
1172
|
+
const loader = getLoader('application/json', loaders, classArgsLoaders, classLoaders);
|
|
1173
|
+
expect(loader).to.equal(customLoader);
|
|
1174
|
+
});
|
|
1175
|
+
|
|
1176
|
+
it('returns blob loader when content type is unknown in all maps', function () {
|
|
1177
|
+
const loaders: LoaderMap = new Map([['application/json', (_r) => Promise.resolve({})]]);
|
|
1178
|
+
const classArgsLoaders: LoaderMap = new Map([['application/xml', (_r) => Promise.resolve({})]]);
|
|
1179
|
+
const classLoaders: LoaderMap = new Map([['text/plain', (_r) => Promise.resolve({})]]);
|
|
1180
|
+
const loader = getLoader('text/html', loaders, classArgsLoaders, classLoaders);
|
|
1181
|
+
const mockResponse = new Response(new Blob(['test']));
|
|
1182
|
+
return loader(mockResponse).then(result => {
|
|
1183
|
+
expect(result).to.be.instanceOf(Blob);
|
|
1184
|
+
});
|
|
1185
|
+
});
|
|
1186
|
+
|
|
1187
|
+
it('returns blob loader when loaders is undefined and content type not in class(Args)Loaders', function () {
|
|
1188
|
+
const classLoaders: LoaderMap = new Map([['application/json', (_r) => Promise.resolve({})]]);
|
|
1189
|
+
const classArgsLoaders: LoaderMap = new Map([['application/xml', (_r) => Promise.resolve({})]]);
|
|
1190
|
+
const loader = getLoader('text/html', undefined, classArgsLoaders, classLoaders);
|
|
1191
|
+
const mockResponse = new Response(new Blob(['test']));
|
|
1192
|
+
return loader(mockResponse).then(result => {
|
|
1193
|
+
expect(result).to.be.instanceOf(Blob);
|
|
1194
|
+
});
|
|
1195
|
+
});
|
|
1196
|
+
|
|
1197
|
+
it('falls through to classArgsLoaders when loaders is undefined', function () {
|
|
1198
|
+
const classArgsLoader: LoaderFunction = (_response) => Promise.resolve('args-result');
|
|
1199
|
+
const classLoader: LoaderFunction = (_response) => Promise.resolve('class-result');
|
|
1200
|
+
const classArglLoaders: LoaderMap = new Map([['text/plain', classArgsLoader]]);
|
|
1201
|
+
const classLoaders: LoaderMap = new Map([['text/plain', classLoader]]);
|
|
1202
|
+
const loader = getLoader('text/plain', undefined, classArglLoaders, classLoaders);
|
|
1203
|
+
expect(loader).to.equal(classArgsLoader);
|
|
1204
|
+
});
|
|
1205
|
+
|
|
1206
|
+
it('matches content type case-insensitively (lowercased)', function () {
|
|
1207
|
+
const customLoader: LoaderFunction = (_response) => Promise.resolve({});
|
|
1208
|
+
const loaders: LoaderMap = new Map([['application/json', customLoader]]);
|
|
1209
|
+
const classLoaders: LoaderMap = new Map();
|
|
1210
|
+
// Content type with mixed case should be lowercased before lookup
|
|
1211
|
+
const loader = getLoader('Application/JSON', loaders, classLoaders, classLoaders);
|
|
1212
|
+
expect(loader).to.equal(customLoader);
|
|
1213
|
+
});
|
|
1214
|
+
});
|