From 21a03a8ff9d3b04f69a28f61008a1c83aa983743 Mon Sep 17 00:00:00 2001 From: Raymond Feng Date: Thu, 9 May 2019 13:37:09 -0700 Subject: [PATCH] feat(context): add support for symbols for sorting --- .../src/__tests__/unit/binding-sorter.unit.ts | 65 ++++++++++++++++- packages/context/src/binding-sorter.ts | 69 ++++++++++++++----- 2 files changed, 116 insertions(+), 18 deletions(-) diff --git a/packages/context/src/__tests__/unit/binding-sorter.unit.ts b/packages/context/src/__tests__/unit/binding-sorter.unit.ts index c2cfeab77a73..42cc09978cc6 100644 --- a/packages/context/src/__tests__/unit/binding-sorter.unit.ts +++ b/packages/context/src/__tests__/unit/binding-sorter.unit.ts @@ -4,10 +4,11 @@ // License text available at https://opensource.org/licenses/MIT import {expect} from '@loopback/testlab'; -import {Binding, sortBindingsByGroup} from '../..'; +import {Binding, compareByOrder, sortBindingsByGroup} from '../..'; describe('BindingComparator', () => { - const orderedGroups = ['log', 'auth']; + const FINAL = Symbol('final'); + const orderedGroups = ['log', 'auth', FINAL]; const groupTagName = 'group'; let bindings: Binding[]; let sortedBindingKeys: string[]; @@ -52,6 +53,17 @@ describe('BindingComparator', () => { assertOrder('validator1', 'validator2', 'metrics', 'logger1'); }); + it('sorts by binding order without group tags', () => { + /** + * Groups + * - '': validator1 // not part of ['log', 'auth'] + * - 'metrics': metrics // not part of ['log', 'auth'] + * - 'log': logger1 + * - 'final': Symbol('final') + */ + assertOrder('validator1', 'metrics', 'logger1', 'final'); + }); + /** * The sorted bindings by group: * - '': validator1, validator2 // not part of ['log', 'auth'] @@ -70,6 +82,7 @@ describe('BindingComparator', () => { Binding.bind('rateLimit').tag({[groupTagName]: 'rateLimit'}), Binding.bind('validator1'), Binding.bind('validator2'), + Binding.bind('final').tag({[groupTagName]: FINAL}), ]; } @@ -92,3 +105,51 @@ describe('BindingComparator', () => { } } }); + +describe('compareByOrder', () => { + it('honors order', () => { + expect(compareByOrder('a', 'b', ['b', 'a'])).to.greaterThan(0); + }); + + it('value not included in order comes first', () => { + expect(compareByOrder('a', 'c', ['a', 'b'])).to.greaterThan(0); + }); + + it('values not included are compared alphabetically', () => { + expect(compareByOrder('a', 'c', [])).to.lessThan(0); + }); + + it('null/undefined/"" values are treated as ""', () => { + expect(compareByOrder('', 'c')).to.lessThan(0); + expect(compareByOrder(null, 'c')).to.lessThan(0); + expect(compareByOrder(undefined, 'c')).to.lessThan(0); + }); + + it('returns 0 for equal values', () => { + expect(compareByOrder('c', 'c')).to.equal(0); + expect(compareByOrder(null, '')).to.equal(0); + expect(compareByOrder('', undefined)).to.equal(0); + }); + + it('allows symbols', () => { + const a = Symbol('a'); + const b = Symbol('b'); + expect(compareByOrder(a, b)).to.lessThan(0); + expect(compareByOrder(a, b, [b, a])).to.greaterThan(0); + expect(compareByOrder(a, 'b', [b, a])).to.greaterThan(0); + }); + + it('list symbols before strings', () => { + const a = 'a'; + const b = Symbol('a'); + expect(compareByOrder(a, b)).to.greaterThan(0); + expect(compareByOrder(b, a)).to.lessThan(0); + }); + + it('compare symbols by description', () => { + const a = Symbol('a'); + const b = Symbol('b'); + expect(compareByOrder(a, b)).to.lessThan(0); + expect(compareByOrder(b, a)).to.greaterThan(0); + }); +}); diff --git a/packages/context/src/binding-sorter.ts b/packages/context/src/binding-sorter.ts index 3116ee66c113..1f42cb14f58a 100644 --- a/packages/context/src/binding-sorter.ts +++ b/packages/context/src/binding-sorter.ts @@ -43,32 +43,69 @@ export interface BindingComparator { * 3. If a binding's `group` does not exist in `orderedGroups`, it comes before * the one with `group` exists in `orderedGroups`. * 4. If both bindings have `group` value outside of `orderedGroups`, they are - * ordered by group names alphabetically. + * ordered by group names alphabetically and symbol values come before string + * values. * * @param groupTagName Name of the binding tag for group * @param orderedGroups An array of group names as the predefined order */ export function compareBindingsByGroup( groupTagName: string = 'group', - orderedGroups: string[] = [], + orderedGroups: (string | symbol)[] = [], ): BindingComparator { return (a: Readonly>, b: Readonly>) => { - const g1: string = a.tagMap[groupTagName] || ''; - const g2: string = b.tagMap[groupTagName] || ''; - const i1 = orderedGroups.indexOf(g1); - const i2 = orderedGroups.indexOf(g2); - if (i1 !== -1 || i2 !== -1) { - // Honor the group order - return i1 - i2; - } else { - // Neither group is in the pre-defined order - // Use alphabetical order instead so that `1-group` is invoked before - // `2-group` - return g1 < g2 ? -1 : g1 > g2 ? 1 : 0; - } + return compareByOrder( + a.tagMap[groupTagName], + b.tagMap[groupTagName], + orderedGroups, + ); }; } +/** + * Compare two values by the predefined order + * + * @remarks + * + * The comparison is performed as follows: + * + * 1. If both values are included in `order`, they are sorted by their indexes in + * `order`. + * 2. The value included in `order` comes after the value not included in `order`. + * 3. If neither values are included in `order`, they are sorted: + * - symbol values come before string values + * - alphabetical order for two symbols or two strings + * + * @param a First value + * @param b Second value + * @param order An array of values as the predefined order + */ +export function compareByOrder( + a: string | symbol | undefined | null, + b: string | symbol | undefined | null, + order: (string | symbol)[] = [], +) { + a = a || ''; + b = b || ''; + const i1 = order.indexOf(a); + const i2 = order.indexOf(b); + if (i1 !== -1 || i2 !== -1) { + // Honor the order + return i1 - i2; + } else { + // Neither value is in the pre-defined order + + // symbol comes before string + if (typeof a === 'symbol' && typeof b === 'string') return -1; + if (typeof a === 'string' && typeof b === 'symbol') return 1; + + // both a and b are symbols or both a and b are strings + if (typeof a === 'symbol') a = a.toString(); + if (typeof b === 'symbol') b = b.toString(); + return a < b ? -1 : a > b ? 1 : 0; + } +} + /** * Sort bindings by group names denoted by a tag and the predefined order * @@ -81,7 +118,7 @@ export function compareBindingsByGroup( export function sortBindingsByGroup( bindings: Readonly>[], groupTagName?: string, - orderedGroups?: string[], + orderedGroups?: (string | symbol)[], ) { return bindings.sort(compareBindingsByGroup(groupTagName, orderedGroups)); }