@api-client/core 0.5.17 → 0.5.18

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,799 @@
1
+ /* eslint-disable class-methods-use-this */
2
+ /* eslint-disable max-classes-per-file */
3
+ import { UrlEncoder } from './UrlEncoder.js';
4
+
5
+ class Tokenizer {
6
+ protected tokens: string;
7
+
8
+ /**
9
+ * This is set to the next token to be read.
10
+ */
11
+ index = 0;
12
+
13
+ size: number;
14
+
15
+ get ended(): boolean {
16
+ return this.index === this.size - 1;
17
+ }
18
+
19
+ constructor(tokens: string) {
20
+ this.tokens = tokens;
21
+ this.size = tokens.length;
22
+ }
23
+
24
+ /**
25
+ * Reads characters until one of the passed characters (not included).
26
+ * @param chars The list of characters to search for.
27
+ * @returns The string between the current index and the found character or the end of tokens.
28
+ */
29
+ untilChar(...chars: string[]): string {
30
+ const { tokens, index, size } = this;
31
+ let result = '';
32
+ for (let i = index; i < size; i++) {
33
+ const token = tokens[i];
34
+ this.index = i;
35
+ if (chars.includes(token)) {
36
+ return result;
37
+ }
38
+ result += token;
39
+ }
40
+ return result;
41
+ }
42
+
43
+ /**
44
+ * Reads the next token and changes the state.
45
+ * @returns The next token.
46
+ */
47
+ next(): string | undefined {
48
+ const { ended, tokens } = this;
49
+ if (ended) {
50
+ return undefined;
51
+ }
52
+ const next = tokens[this.index];
53
+ this.index += 1;
54
+ return next;
55
+ }
56
+
57
+ /**
58
+ * Reads the next character without changing the state.
59
+ */
60
+ getNext(): string | undefined {
61
+ const { ended, tokens, index } = this;
62
+ if (ended) {
63
+ return undefined;
64
+ }
65
+ return tokens[index + 1];
66
+ }
67
+ }
68
+
69
+ enum PartType {
70
+ /**
71
+ * Literal string, we do not process these
72
+ */
73
+ literal,
74
+ /**
75
+ * URI template expression
76
+ */
77
+ template,
78
+ /**
79
+ * query parameter
80
+ */
81
+ param,
82
+ }
83
+
84
+ enum State {
85
+ /**
86
+ * Processing a literal
87
+ */
88
+ literal,
89
+ /**
90
+ * Processing a query parameter
91
+ */
92
+ param,
93
+ /**
94
+ * Processing a template expression
95
+ */
96
+ expression,
97
+ }
98
+
99
+ enum DataType {
100
+ // undefined/null
101
+ nil,
102
+ // string
103
+ string,
104
+ // object
105
+ object,
106
+ // array
107
+ array,
108
+ }
109
+
110
+ /**
111
+ * The operator definition for a template expression
112
+ */
113
+ interface IOperator {
114
+ /**
115
+ * The value of the operator for the variable.
116
+ * Can be one of `+`, `#`, `.`, `/`, `;`, `?`, or `&`,
117
+ * THere are additional reserved characters for future use: `=`, `,`, `!`, `@` and `|`.
118
+ *
119
+ * This is not set when the expression has no operator.
120
+ */
121
+ operator?: string;
122
+ prefix?: string;
123
+ separator: string;
124
+ named?: boolean;
125
+ reserved?: boolean;
126
+ emptyNameSeparator?: boolean
127
+ }
128
+
129
+ /**
130
+ * Variable definition of a template expression
131
+ */
132
+ interface IVariable {
133
+ /**
134
+ * The variable name, cleaned of any operators
135
+ */
136
+ name: string;
137
+ /**
138
+ * Whether the variable "explode" values as described in the spec.
139
+ */
140
+ explode: boolean;
141
+ /**
142
+ * The value length to insert. For expressions like `{var:20}`
143
+ */
144
+ maxLength?: number;
145
+ }
146
+
147
+ export interface IUrlPart {
148
+ type: PartType;
149
+ expression: string;
150
+ }
151
+
152
+ /**
153
+ * A part that defines a query parameter.
154
+ */
155
+ export interface IUrlParamPart extends IUrlPart {
156
+ /**
157
+ * The name of the query parameter.
158
+ */
159
+ name: string;
160
+ /**
161
+ * The name value of the query parameter.
162
+ */
163
+ value: string;
164
+ /**
165
+ * Whether the parameter should be considered when creating the URL.
166
+ */
167
+ enabled: boolean;
168
+ }
169
+
170
+ /**
171
+ * A part that defines a template expression
172
+ */
173
+ export interface IUrlExpressionPart extends IUrlPart {
174
+ operator: IOperator;
175
+ variables: IVariable[];
176
+ }
177
+
178
+ interface IData {
179
+ /**
180
+ * type of data 0: undefined/null, 1: string, 2: object, 3: array
181
+ */
182
+ type: DataType;
183
+ /**
184
+ * The read values, except undefined/null.
185
+ * The value is always set. Name is set when the read value is an object.
186
+ */
187
+ values: { value: string, name?: string }[];
188
+ }
189
+
190
+ export interface IUrlExpandOptions {
191
+ /**
192
+ * When set it ignores replacing the value in the template when the variable is missing.
193
+ */
194
+ ignoreMissing?: boolean;
195
+ /**
196
+ * When set it throws errors when missing variables.
197
+ */
198
+ strict?: boolean;
199
+ }
200
+
201
+ export type UrlPart = IUrlPart | IUrlParamPart | IUrlExpressionPart;
202
+
203
+ class SearchParams {
204
+ /**
205
+ * A reference to the URL processor's parts.
206
+ */
207
+ parts: UrlPart[] = [];
208
+
209
+ constructor(parts: UrlPart[]) {
210
+ this.parts = parts;
211
+ }
212
+
213
+ /**
214
+ * Iterates over each query parameter, in order.
215
+ */
216
+ * [Symbol.iterator](): Generator<IUrlParamPart> {
217
+ for (const part of this.parts) {
218
+ if (part.type === PartType.param) {
219
+ yield part as IUrlParamPart;
220
+ }
221
+ }
222
+ }
223
+
224
+ /**
225
+ * Reads parts, in order, that are query parameters.
226
+ */
227
+ list(): IUrlParamPart[] {
228
+ return this.parts.filter(i => i.type === PartType.param) as IUrlParamPart[];
229
+ }
230
+
231
+ /**
232
+ * Adds a new query parameter to the list.
233
+ *
234
+ * @param name The name of the parameter.
235
+ * @param value The value of the parameter.
236
+ */
237
+ append(name: string, value = ''): void {
238
+ const part: IUrlParamPart = {
239
+ type: PartType.param,
240
+ expression: `${name}=${value}`,
241
+ name,
242
+ value,
243
+ enabled: true,
244
+ };
245
+ this.parts.push(part);
246
+ }
247
+
248
+ /**
249
+ * Replaces the param part in the parts list.
250
+ *
251
+ * @param index The index of the parameter as returned by the `getParameters()` method.
252
+ * @param param The param to set.
253
+ */
254
+ update(index: number, param: IUrlParamPart): void {
255
+ if (param.type !== PartType.param) {
256
+ throw new Error(`Invalid query parameter definition`);
257
+ }
258
+ // eslint-disable-next-line no-param-reassign
259
+ param.expression = `${param.name}=${param.value || ''}`;
260
+ let current = 0;
261
+ for (let i = 0, len = this.parts.length; i < len; i++) {
262
+ const part = this.parts[i];
263
+ if (part.type === PartType.param) {
264
+ if (current === index) {
265
+ this.parts[i] = param;
266
+ return;
267
+ }
268
+ current += 1;
269
+ }
270
+ }
271
+ throw new Error(`Missing query parameter at position ${index}.`);
272
+ }
273
+
274
+ /**
275
+ * Replaces all parameters with the name with the passed value.
276
+ * It insets the param at the first occurrence of the current param in the parts list.
277
+ * If not exists, it adds it as a last.
278
+ *
279
+ * @param name The name of the parameter
280
+ * @param value the value of the parameter
281
+ */
282
+ set(name: string, value: string): void {
283
+ let firstIndex = -1;
284
+ for (let i = this.parts.length - 1; i >= 0; i--) {
285
+ const part = this.parts[i];
286
+ if (part.type === PartType.param) {
287
+ const typed = part as IUrlParamPart;
288
+ if (typed.name === name) {
289
+ this.parts.splice(i, 1);
290
+ firstIndex = i;
291
+ }
292
+ }
293
+ }
294
+ const part: IUrlParamPart = {
295
+ type: PartType.param,
296
+ expression: `${name}=${value}`,
297
+ name,
298
+ value,
299
+ enabled: true,
300
+ };
301
+ if (firstIndex === -1) {
302
+ this.parts.push(part);
303
+ } else {
304
+ this.parts.splice(firstIndex, 0, part);
305
+ }
306
+ }
307
+
308
+ /**
309
+ * Removes a query parameter from the parameters list.
310
+ *
311
+ * @param index The index of the parameter as returned by the `getParameters()` method.
312
+ */
313
+ delete(index: number): void {
314
+ let current = 0;
315
+ for (let i = 0, len = this.parts.length; i < len; i++) {
316
+ const part = this.parts[i];
317
+ if (part.type === PartType.param) {
318
+ if (current === index) {
319
+ this.parts.splice(i, 1);
320
+ return;
321
+ }
322
+ current += 1;
323
+ }
324
+ }
325
+ throw new Error(`Missing query parameter at position ${index}.`);
326
+ }
327
+
328
+ /**
329
+ * Toggles the enabled state of the parameter.
330
+ *
331
+ * @param index The index of the parameter as returned by the `getParameters()` method.
332
+ * @param enabled The enabled state of the parameter.
333
+ */
334
+ toggle(index: number, enabled: boolean): void {
335
+ const params = this.list();
336
+ const param = params[index];
337
+ if (!param) {
338
+ throw new Error(`Missing query parameter at position ${index}.`);
339
+ }
340
+ param.enabled = enabled;
341
+ }
342
+ }
343
+
344
+ /**
345
+ * A class that parses a string a treats it as a value that may include
346
+ * URI templates and request parameters.
347
+ *
348
+ * It can be used by the UI libraries to manipulate the URL templates and query parameters.
349
+ */
350
+ export class UrlProcessor {
351
+ /**
352
+ * The source expression.
353
+ * It is immutable for the entire manipulation process of the URI.
354
+ * Any modification is on the parts level and the `expand()` and `toString()`
355
+ * function only rely on the `parts`.
356
+ */
357
+ expression: string;
358
+
359
+ /**
360
+ * The tokenizer object
361
+ */
362
+ tokens: Tokenizer;
363
+
364
+ /**
365
+ * An ordered list of parts of the expression.
366
+ */
367
+ parts: UrlPart[] = [];
368
+
369
+ /**
370
+ * A helper class to manipulate query parameters on the parser.
371
+ */
372
+ search: SearchParams;
373
+
374
+ constructor(expression: string) {
375
+ this.expression = expression;
376
+ this.tokens = new Tokenizer(expression);
377
+ this.search = new SearchParams(this.parts);
378
+ this._parse();
379
+ }
380
+
381
+ /**
382
+ * Creates an URI leaving the template expressions are they are (without expanding them).
383
+ */
384
+ toString(): string {
385
+ let result = '';
386
+ this.parts.forEach((part) => {
387
+ if (part.type === PartType.literal) {
388
+ result += part.expression;
389
+ } else if (part.type === PartType.template) {
390
+ result += `{${part.expression}}`;
391
+ } else {
392
+ const typed = part as IUrlParamPart;
393
+ if (!typed.enabled) {
394
+ return;
395
+ }
396
+ if (result.includes('?')) {
397
+ result += '&';
398
+ } else {
399
+ result += '?';
400
+ }
401
+ result += part.expression;
402
+ }
403
+ });
404
+ return result;
405
+ }
406
+
407
+ /**
408
+ * Creates a URI with expanded template values.
409
+ *
410
+ * @param map The variables to evaluate.
411
+ * @param opts Processing options.
412
+ */
413
+ expand(map: Record<string, any>, opts: IUrlExpandOptions = {}): string {
414
+ let result = '';
415
+ for (const part of this.parts) {
416
+ if (part.type === PartType.literal) {
417
+ result += part.expression;
418
+ } else if (part.type === PartType.param) {
419
+ const typed = part as IUrlParamPart;
420
+ if (!typed.enabled) {
421
+ continue;
422
+ }
423
+ if (result.includes('?')) {
424
+ result += '&';
425
+ } else {
426
+ result += '?';
427
+ }
428
+ result += part.expression;
429
+ } else {
430
+ result += this._expandExpression(part as IUrlExpressionPart, map, opts);
431
+ }
432
+ }
433
+ return result;
434
+ }
435
+
436
+ protected _expandExpression(part: IUrlExpressionPart, map: Record<string, any>, opts: IUrlExpandOptions): string {
437
+ const { operator, variables } = part;
438
+ const buffer: string[] = [];
439
+ for (const variable of variables) {
440
+ const data = this._getData(map, variable.name);
441
+
442
+ if (data.type === DataType.nil && opts.strict) {
443
+ throw new Error(`Missing expansion value for variable "${variable.name}"`);
444
+ }
445
+
446
+ if (data.type === DataType.nil && opts.ignoreMissing) {
447
+ buffer.push(part.expression);
448
+ continue;
449
+ }
450
+
451
+ if (!data.values.length) {
452
+ if (data.type !== DataType.nil) {
453
+ // Empty object / array. We still append the separator.
454
+ buffer.push('');
455
+ }
456
+ continue;
457
+ }
458
+
459
+ if (data.type > DataType.string && variable.maxLength) {
460
+ if (opts.strict) {
461
+ throw new Error(`Invalid expression: Prefix modifier not applicable to variable "${variable.name}"`);
462
+ }
463
+ // we ignore invalid values.
464
+ continue;
465
+ }
466
+
467
+ if (operator.named) {
468
+ buffer.push(this._expandNamedExpression(data, operator, variable));
469
+ } else {
470
+ buffer.push(this._expandUnNamedExpression(data, operator, variable));
471
+ }
472
+ }
473
+ if (buffer.length) {
474
+ return (operator.prefix || '') + buffer.join(operator.separator);
475
+ }
476
+ // prefix is not prepended for empty expressions
477
+ return '';
478
+ }
479
+
480
+ protected _expandNamedExpression(data: IData, operator: IOperator, variable: IVariable): string {
481
+ let result = '';
482
+ const separator = variable.explode && operator.separator || ',';
483
+ const encodeFunction = operator.reserved ? UrlEncoder.encodeReserved : UrlEncoder.strictEncode;
484
+ let name = '';
485
+ if (data.type !== DataType.object && variable.name) {
486
+ name = encodeFunction(variable.name);
487
+ }
488
+ const hasLength = typeof variable.maxLength === 'number';
489
+ data.values.forEach((item, index) => {
490
+ let value: string;
491
+ if (hasLength) {
492
+ value = encodeFunction(item.value.substring(0, variable.maxLength));
493
+ if (data.type === DataType.object) {
494
+ // apply maxLength to keys of objects as well
495
+ name = encodeFunction(item.name!.substring(0, variable.maxLength));
496
+ }
497
+ } else {
498
+ // encode value
499
+ value = encodeFunction(item.value);
500
+ if (data.type === DataType.object) {
501
+ // encode name and cache encoded value
502
+ name = encodeFunction(item.name!);
503
+ }
504
+ }
505
+ if (result) {
506
+ result += separator;
507
+ }
508
+
509
+ if (!variable.explode) {
510
+ if (!index) {
511
+ result += encodeFunction(variable.name) + (operator.emptyNameSeparator || value ? '=' : '');
512
+ }
513
+ if (data.type === DataType.object) {
514
+ result += `${name},`;
515
+ }
516
+ result += value;
517
+ } else {
518
+ result += name + (operator.emptyNameSeparator || value ? '=' : '') + value;
519
+ }
520
+ });
521
+ return result;
522
+ }
523
+
524
+ protected _expandUnNamedExpression(data: IData, operator: IOperator, variable: IVariable): string {
525
+ let result = '';
526
+ const separator = variable.explode && operator.separator || ',';
527
+ const encodeFunction = operator.reserved ? UrlEncoder.encodeReserved : UrlEncoder.strictEncode;
528
+ const hasLength = typeof variable.maxLength === 'number';
529
+ data.values.forEach((item) => {
530
+ let value: string;
531
+ if (hasLength) {
532
+ value = encodeFunction(item.value.substring(0, variable.maxLength));
533
+ } else {
534
+ value = encodeFunction(item.value);
535
+ }
536
+ if (result) {
537
+ result += separator;
538
+ }
539
+ if (data.type === DataType.object) {
540
+ let _name: string;
541
+ if (hasLength) {
542
+ _name = encodeFunction(item.name!.substring(0, variable.maxLength));
543
+ } else {
544
+ _name = encodeFunction(item.name || '');
545
+ }
546
+ result += _name;
547
+ if (variable.explode) {
548
+ result += (operator.emptyNameSeparator || value ? '=' : '');
549
+ } else {
550
+ result += ',';
551
+ }
552
+ }
553
+ result += value;
554
+ });
555
+ return result;
556
+ }
557
+
558
+ protected _getData(map: Record<string, any>, name: string): IData {
559
+ const result: IData = {
560
+ type: DataType.nil,
561
+ values: [],
562
+ };
563
+
564
+ const value = map[name];
565
+ if (value === undefined || value === null) {
566
+ // undefined and null values are to be ignored completely
567
+ return result;
568
+ }
569
+
570
+ if (Array.isArray(value)) {
571
+ for (const v of value) {
572
+ // we only allow primitives in the values
573
+ if (this._validExpansionValue(v)) {
574
+ result.values.push({ value: String(v) });
575
+ }
576
+ }
577
+ } else if (String(Object.prototype.toString.call(value)) === '[object Object]') {
578
+ Object.keys(value).forEach((k) => {
579
+ const v = value[k];
580
+ if (this._validExpansionValue(v)) {
581
+ result.values.push({name: k, value: v});
582
+ }
583
+ });
584
+ if (result.values.length) {
585
+ result.type = DataType.object;
586
+ }
587
+ } else {
588
+ result.type = DataType.string;
589
+ result.values.push({ value: String(value) });
590
+ }
591
+ return result;
592
+ }
593
+
594
+ protected _validExpansionValue(value: any): boolean {
595
+ if (value === undefined || value === null) {
596
+ // these are ignored completely
597
+ return false;
598
+ }
599
+ // we only allow primitives in the values
600
+ return ['string', 'number', 'boolean'].includes(typeof value);
601
+ }
602
+
603
+ /**
604
+ * Creates an ordered list of parts that describe the passed expression.
605
+ *
606
+ * FIXME: Handle error states like unclosed brackets.
607
+ */
608
+ protected _parse(): void {
609
+ const { tokens } = this;
610
+
611
+ let state = State.literal;
612
+ // eslint-disable-next-line no-constant-condition
613
+ while (true) {
614
+ let value: string;
615
+ if (state === State.literal) {
616
+ value = tokens.untilChar('?', '&', '{', '}');
617
+ } else if (state === State.param) {
618
+ value = tokens.untilChar('&', '{');
619
+ } else {
620
+ value = tokens.untilChar('}');
621
+ }
622
+ const next = tokens.next();
623
+
624
+ if (state === State.literal) {
625
+ // when the value is an empty string that means that we entered the expression string
626
+ // and we have a variable or a query parameter as the first character.
627
+ // We don't need to pass a literal in this case.
628
+ if (value !== '') {
629
+ this._addLiteral(value);
630
+ }
631
+ } else if (state === State.param) {
632
+ this._addParam(value);
633
+ } else {
634
+ this._addTemplate(value);
635
+ }
636
+
637
+ if (next === '?' || next === '&') {
638
+ state = State.param;
639
+ } else if (next === '{') {
640
+ state = State.expression;
641
+ } else if (next === undefined) {
642
+ break;
643
+ } else {
644
+ state = State.literal;
645
+ }
646
+ }
647
+ }
648
+
649
+ /**
650
+ * Adds a literal part.
651
+ * Literal parts are not processed in any way. They do not contain query parameters
652
+ * or template expressions.
653
+ *
654
+ * @param expression The literal expression.
655
+ */
656
+ protected _addLiteral(expression: string): void {
657
+ this.parts.push({
658
+ type: PartType.literal,
659
+ expression,
660
+ });
661
+ }
662
+
663
+ /**
664
+ * Adds a part that describes a query parameter
665
+ * @param expression The query param as `name=value` or `name` or `name=`
666
+ */
667
+ protected _addParam(expression: string): void {
668
+ const parts = expression.split('=');
669
+ const part: IUrlParamPart = {
670
+ type: PartType.param,
671
+ expression,
672
+ name: parts[0],
673
+ value: parts[1] || '',
674
+ enabled: true,
675
+ };
676
+ this.parts.push(part);
677
+ }
678
+
679
+ /**
680
+ * Adds a part that is a template expression.
681
+ *
682
+ * @param expression The template expression as defined in RFC 6570
683
+ */
684
+ protected _addTemplate(expression: string): void {
685
+ const ch = expression[0];
686
+ let operator: IOperator;
687
+ if (ch === '+') {
688
+ // Reserved character strings (no encoding)
689
+ operator = {
690
+ operator: '+',
691
+ separator: ',',
692
+ reserved: true,
693
+ }
694
+ } else if (ch === '#') {
695
+ // Fragment identifiers prefixed by '#'
696
+ operator = {
697
+ operator: '#',
698
+ prefix: '#',
699
+ separator: ',',
700
+ reserved: true,
701
+ }
702
+ } else if (ch === '.') {
703
+ // Name labels or extensions prefixed by '.'
704
+ operator = {
705
+ operator: '.',
706
+ prefix: '.',
707
+ separator: '.',
708
+ }
709
+ } else if (ch === '/') {
710
+ // Path segments prefixed by '/'
711
+ operator = {
712
+ operator: '/',
713
+ prefix: '/',
714
+ separator: '/',
715
+ }
716
+ } else if (ch === ';') {
717
+ // Path parameter name or name=value pairs prefixed by ';'
718
+ operator = {
719
+ operator: ';',
720
+ prefix: ';',
721
+ separator: ';',
722
+ named: true,
723
+ }
724
+ } else if (ch === '?') {
725
+ // Query component beginning with '?' and consisting
726
+ // of name=value pairs separated by '&'
727
+ operator = {
728
+ operator: '?',
729
+ prefix: '?',
730
+ separator: '&',
731
+ named: true,
732
+ emptyNameSeparator: true,
733
+ }
734
+ } else if (ch === '&') {
735
+ // Continuation of query-style &name=value pairs
736
+ // within a literal query component.
737
+ operator = {
738
+ operator: '&',
739
+ prefix: '&',
740
+ separator: '&',
741
+ named: true,
742
+ emptyNameSeparator: true,
743
+ }
744
+ } else {
745
+ // The operator characters equals ("="), comma (","), exclamation ("!"),
746
+ // at sign ("@"), and pipe ("|") are reserved for future extensions.
747
+
748
+ // this is level1 simple expression
749
+ operator = {
750
+ separator: ',',
751
+ }
752
+ }
753
+
754
+ const part: IUrlExpressionPart = {
755
+ type: PartType.template,
756
+ expression,
757
+ operator,
758
+ variables: this._readVariables(expression, operator.operator),
759
+ };
760
+
761
+ this.parts.push(part);
762
+ }
763
+
764
+ protected _readVariables(expression: string, operator?: string): IVariable[] {
765
+ let name = expression;
766
+ if (operator) {
767
+ name = name.substring(1);
768
+ }
769
+ return name.split(',').map((item) => {
770
+ let maxLength: number | undefined;
771
+ let varName = item;
772
+ let explode = false;
773
+ if (varName.endsWith('*')) {
774
+ explode = true;
775
+ varName = varName.substring(0, varName.length - 1);
776
+ }
777
+ const lengthIndex = varName.indexOf(':');
778
+ if (lengthIndex >= 0) {
779
+ const len = varName.substring(lengthIndex + 1);
780
+ varName = varName.substring(0, lengthIndex);
781
+ if (len) {
782
+ const parsed = Number(len);
783
+ if (Number.isInteger(parsed)) {
784
+ maxLength = parsed;
785
+ }
786
+ }
787
+ }
788
+
789
+ const result: IVariable = {
790
+ name: varName,
791
+ explode,
792
+ };
793
+ if (typeof maxLength === 'number') {
794
+ result.maxLength = maxLength;
795
+ }
796
+ return result;
797
+ });
798
+ }
799
+ }