Skip to content

Commit

Permalink
feat(context): add support for symbols for sorting
Browse files Browse the repository at this point in the history
  • Loading branch information
raymondfeng committed May 9, 2019
1 parent add0552 commit 21a03a8
Show file tree
Hide file tree
Showing 2 changed files with 116 additions and 18 deletions.
65 changes: 63 additions & 2 deletions packages/context/src/__tests__/unit/binding-sorter.unit.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<unknown>[];
let sortedBindingKeys: string[];
Expand Down Expand Up @@ -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']
Expand All @@ -70,6 +82,7 @@ describe('BindingComparator', () => {
Binding.bind('rateLimit').tag({[groupTagName]: 'rateLimit'}),
Binding.bind('validator1'),
Binding.bind('validator2'),
Binding.bind('final').tag({[groupTagName]: FINAL}),
];
}

Expand All @@ -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);
});
});
69 changes: 53 additions & 16 deletions packages/context/src/binding-sorter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<Binding<unknown>>, b: Readonly<Binding<unknown>>) => {
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
*
Expand All @@ -81,7 +118,7 @@ export function compareBindingsByGroup(
export function sortBindingsByGroup(
bindings: Readonly<Binding<unknown>>[],
groupTagName?: string,
orderedGroups?: string[],
orderedGroups?: (string | symbol)[],
) {
return bindings.sort(compareBindingsByGroup(groupTagName, orderedGroups));
}

0 comments on commit 21a03a8

Please sign in to comment.