1 | // Copyright IBM Corp. and LoopBack contributors 2019. All Rights Reserved.
|
2 | // Node module: @loopback/context
|
3 | // This file is licensed under the MIT License.
|
4 | // License text available at https://opensource.org/licenses/MIT
|
5 |
|
6 | import {Binding} from './binding';
|
7 |
|
8 | /**
|
9 | * Compare function to sort an array of bindings.
|
10 | * It is used by `Array.prototype.sort()`.
|
11 | *
|
12 | * @example
|
13 | * ```ts
|
14 | * const compareByKey: BindingComparator = (a, b) => a.key.localeCompare(b.key);
|
15 | * ```
|
16 | */
|
17 | export interface BindingComparator {
|
18 | /**
|
19 | * Compare two bindings
|
20 | * @param bindingA - First binding
|
21 | * @param bindingB - Second binding
|
22 | * @returns A number to determine order of bindingA and bindingB
|
23 | * - 0 leaves bindingA and bindingB unchanged
|
24 | * - <0 bindingA comes before bindingB
|
25 | * - >0 bindingA comes after bindingB
|
26 | */
|
27 | (
|
28 | bindingA: Readonly<Binding<unknown>>,
|
29 | bindingB: Readonly<Binding<unknown>>,
|
30 | ): number;
|
31 | }
|
32 |
|
33 | /**
|
34 | * Creates a binding compare function to sort bindings by tagged phase name.
|
35 | *
|
36 | * @remarks
|
37 | * Two bindings are compared as follows:
|
38 | *
|
39 | * 1. Get values for the given tag as `phase` for bindings, if the tag is not
|
40 | * present, default `phase` to `''`.
|
41 | * 2. If both bindings have `phase` value in `orderOfPhases`, honor the order
|
42 | * specified by `orderOfPhases`.
|
43 | * 3. If a binding's `phase` does not exist in `orderOfPhases`, it comes before
|
44 | * the one with `phase` exists in `orderOfPhases`.
|
45 | * 4. If both bindings have `phase` value outside of `orderOfPhases`, they are
|
46 | * ordered by phase names alphabetically and symbol values come before string
|
47 | * values.
|
48 | *
|
49 | * @param phaseTagName - Name of the binding tag for phase
|
50 | * @param orderOfPhases - An array of phase names as the predefined order
|
51 | */
|
52 | export function compareBindingsByTag(
|
53 | phaseTagName = 'phase',
|
54 | orderOfPhases: (string | symbol)[] = [],
|
55 | ): BindingComparator {
|
56 | return (a: Readonly<Binding<unknown>>, b: Readonly<Binding<unknown>>) => {
|
57 | return compareByOrder(
|
58 | a.tagMap[phaseTagName],
|
59 | b.tagMap[phaseTagName],
|
60 | orderOfPhases,
|
61 | );
|
62 | };
|
63 | }
|
64 |
|
65 | /**
|
66 | * Compare two values by the predefined order
|
67 | *
|
68 | * @remarks
|
69 | *
|
70 | * The comparison is performed as follows:
|
71 | *
|
72 | * 1. If both values are included in `order`, they are sorted by their indexes in
|
73 | * `order`.
|
74 | * 2. The value included in `order` comes after the value not included in `order`.
|
75 | * 3. If neither values are included in `order`, they are sorted:
|
76 | * - symbol values come before string values
|
77 | * - alphabetical order for two symbols or two strings
|
78 | *
|
79 | * @param a - First value
|
80 | * @param b - Second value
|
81 | * @param order - An array of values as the predefined order
|
82 | */
|
83 | export function compareByOrder(
|
84 | a: string | symbol | undefined | null,
|
85 | b: string | symbol | undefined | null,
|
86 | order: (string | symbol)[] = [],
|
87 | ) {
|
88 | a = a ?? '';
|
89 | b = b ?? '';
|
90 | const i1 = order.indexOf(a);
|
91 | const i2 = order.indexOf(b);
|
92 | if (i1 !== -1 || i2 !== -1) {
|
93 | // Honor the order
|
94 | return i1 - i2;
|
95 | } else {
|
96 | // Neither value is in the pre-defined order
|
97 |
|
98 | // symbol comes before string
|
99 | if (typeof a === 'symbol' && typeof b === 'string') return -1;
|
100 | if (typeof a === 'string' && typeof b === 'symbol') return 1;
|
101 |
|
102 | // both a and b are symbols or both a and b are strings
|
103 | if (typeof a === 'symbol') a = a.toString();
|
104 | if (typeof b === 'symbol') b = b.toString();
|
105 | return a < b ? -1 : a > b ? 1 : 0;
|
106 | }
|
107 | }
|
108 |
|
109 | /**
|
110 | * Sort bindings by phase names denoted by a tag and the predefined order
|
111 | *
|
112 | * @param bindings - An array of bindings
|
113 | * @param phaseTagName - Tag name for phase, for example, we can use the value
|
114 | * `'a'` of tag `order` as the phase name for `binding.tag({order: 'a'})`.
|
115 | *
|
116 | * @param orderOfPhases - An array of phase names as the predefined order
|
117 | */
|
118 | export function sortBindingsByPhase<T = unknown>(
|
119 | bindings: Readonly<Binding<T>>[],
|
120 | phaseTagName?: string,
|
121 | orderOfPhases?: (string | symbol)[],
|
122 | ) {
|
123 | return bindings.sort(compareBindingsByTag(phaseTagName, orderOfPhases));
|
124 | }
|