Skip to content

Commit

Permalink
Update resolver fallback logic (#8456)
Browse files Browse the repository at this point in the history
  • Loading branch information
kurkle authored Feb 19, 2021
1 parent e8f9542 commit 850e6e4
Show file tree
Hide file tree
Showing 2 changed files with 246 additions and 39 deletions.
34 changes: 23 additions & 11 deletions src/helpers/helpers.config.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import {defined, isArray, isFunction, isObject, resolveObjectKey, _capitalize} from './helpers.core';
import {defined, isArray, isFunction, isObject, resolveObjectKey, valueOrDefault, _capitalize} from './helpers.core';

/**
* Creates a Proxy for resolving raw values for options.
Expand All @@ -12,7 +12,7 @@ export function _createResolver(scopes, prefixes = ['']) {
[Symbol.toStringTag]: 'Object',
_cacheable: true,
_scopes: scopes,
override: (scope) => _createResolver([scope].concat(scopes), prefixes),
override: (scope) => _createResolver([scope, ...scopes], prefixes),
};
return new Proxy(cache, {
/**
Expand Down Expand Up @@ -186,7 +186,7 @@ function _resolveScriptable(prop, value, target, receiver) {
_stack.delete(prop);
if (isObject(value)) {
// When scriptable option returns an object, create a resolver on that.
value = createSubResolver([value].concat(_proxy._scopes), prop, value);
value = createSubResolver(_proxy._scopes, prop, value);
}
return value;
}
Expand All @@ -202,7 +202,7 @@ function _resolveArray(prop, value, target, isIndexable) {
const scopes = _proxy._scopes.filter(s => s !== arr);
value = [];
for (const item of arr) {
const resolver = createSubResolver([item].concat(scopes), prop, item);
const resolver = createSubResolver(scopes, prop, item);
value.push(_attachContext(resolver, _context, _subProxy && _subProxy[prop]));
}
}
Expand All @@ -211,12 +211,20 @@ function _resolveArray(prop, value, target, isIndexable) {

function createSubResolver(parentScopes, prop, value) {
const set = new Set([value]);
const {keys, includeParents} = _resolveSubKeys(parentScopes, prop, value);
for (const key of keys) {
for (const item of parentScopes) {
const lookupScopes = [value, ...parentScopes];
const {keys, includeParents} = _resolveSubKeys(lookupScopes, prop, value);
while (keys.length) {
const key = keys.shift();
for (const item of lookupScopes) {
const scope = resolveObjectKey(item, key);
if (scope) {
set.add(scope);
// fallback detour?
const fallback = scope._fallback;
if (defined(fallback)) {
keys.push(...resolveFallback(fallback, key, scope).filter(k => k !== key));
}

} else if (key !== prop && scope === false) {
// If any of the fallback scopes is explicitly false, return false
// For example, options.hover falls back to options.interaction, when
Expand All @@ -231,14 +239,18 @@ function createSubResolver(parentScopes, prop, value) {
return _createResolver([...set]);
}

function resolveFallback(fallback, prop, value) {
const resolved = isFunction(fallback) ? fallback(prop, value) : fallback;
return isArray(resolved) ? resolved : typeof resolved === 'string' ? [resolved] : [];
}

function _resolveSubKeys(parentScopes, prop, value) {
const fallback = _resolve('_fallback', parentScopes.map(scope => scope[prop] || scope));
const fallback = valueOrDefault(_resolve('_fallback', parentScopes.map(scope => scope[prop] || scope)), true);
const keys = [prop];
if (defined(fallback)) {
const resolved = isFunction(fallback) ? fallback(prop, value) : fallback;
keys.push(...(isArray(resolved) ? resolved : [resolved]));
keys.push(...resolveFallback(fallback, prop, value));
}
return {keys: keys.filter(v => v), includeParents: fallback !== prop};
return {keys: keys.filter(v => v), includeParents: fallback !== false && fallback !== prop};
}

function _resolveWithPrefixes(prop, prefixes, scopes) {
Expand Down
251 changes: 223 additions & 28 deletions test/specs/helpers.config.tests.js
Original file line number Diff line number Diff line change
Expand Up @@ -35,34 +35,6 @@ describe('Chart.helpers.config', function() {
expect(sub.opt).toEqual('opt');
});

it('should follow _fallback', function() {
const defaults = {
interaction: {
mode: 'test',
priority: 'fall'
},
hover: {
_fallback: 'interaction',
priority: 'main'
}
};
const options = {
interaction: {
a: 1
},
hover: {
b: 2
}
};
const resolver = _createResolver([options, defaults]);
expect(resolver.hover).toEqualOptions({
mode: 'test',
priority: 'main',
a: 1,
b: 2
});
});

it('should support overriding options', function() {
const defaults = {
option1: 'defaults1',
Expand Down Expand Up @@ -123,6 +95,229 @@ describe('Chart.helpers.config', function() {

expect(resolver.getter).toEqual('options getter');
});

describe('_fallback', function() {
it('should follow simple _fallback', function() {
const defaults = {
interaction: {
mode: 'test',
priority: 'fall'
},
hover: {
_fallback: 'interaction',
priority: 'main'
}
};
const options = {
interaction: {
a: 1
},
hover: {
b: 2
}
};
const resolver = _createResolver([options, defaults]);
expect(resolver.hover).toEqualOptions({
mode: 'test',
priority: 'main',
a: 1,
b: 2
});
});

it('should not fallback when _fallback is false', function() {
const defaults = {
hover: {
_fallback: false,
a: 'defaults.hover'
},
controllers: {
y: 'defaults.controllers',
bar: {
z: 'defaults.controllers.bar',
hover: {
b: 'defaults.controllers.bar.hover'
}
}
},
x: 'defaults root'
};
const options = {
x: 'options',
hover: {
c: 'options.hover',
sub: {
f: 'options.hover.sub'
}
},
controllers: {
y: 'options.controllers',
bar: {
z: 'options.controllers.bar',
hover: {
d: 'options.controllers.bar.hover',
sub: {
e: 'options.controllers.bar.hover.sub'
}
}
}
}
};
const resolver = _createResolver([options, options.controllers.bar, options.controllers, defaults.controllers.bar, defaults.controllers, defaults]);
expect(resolver.hover).toEqualOptions({
a: 'defaults.hover',
b: 'defaults.controllers.bar.hover',
c: 'options.hover',
d: 'options.controllers.bar.hover',
e: undefined,
f: undefined,
x: undefined,
y: undefined,
z: undefined
});
expect(resolver.hover.sub).toEqualOptions({
a: undefined,
b: undefined,
c: undefined,
d: undefined,
e: 'options.controllers.bar.hover.sub',
f: 'options.hover.sub',
x: undefined,
y: undefined,
z: undefined
});
});

it('should fallback to specific scope', function() {
const defaults = {
hover: {
_fallback: 'hover',
a: 'defaults.hover'
},
controllers: {
y: 'defaults.controllers',
bar: {
z: 'defaults.controllers.bar',
hover: {
b: 'defaults.controllers.bar.hover'
}
}
},
x: 'defaults root'
};
const options = {
x: 'options',
hover: {
c: 'options.hover',
sub: {
f: 'options.hover.sub'
}
},
controllers: {
y: 'options.controllers',
bar: {
z: 'options.controllers.bar',
hover: {
d: 'options.controllers.bar.hover',
sub: {
e: 'options.controllers.bar.hover.sub'
}
}
}
}
};
const resolver = _createResolver([options, options.controllers.bar, options.controllers, defaults.controllers.bar, defaults.controllers, defaults]);
expect(resolver.hover).toEqualOptions({
a: 'defaults.hover',
b: 'defaults.controllers.bar.hover',
c: 'options.hover',
d: 'options.controllers.bar.hover',
e: undefined,
f: undefined,
x: undefined,
y: undefined,
z: undefined
});
expect(resolver.hover.sub).toEqualOptions({
a: 'defaults.hover',
b: 'defaults.controllers.bar.hover',
c: 'options.hover',
d: 'options.controllers.bar.hover',
e: 'options.controllers.bar.hover.sub',
f: 'options.hover.sub',
x: undefined,
y: undefined,
z: undefined
});
});

it('should fallback throuhg multiple routes', function() {
const defaults = {
root: {
a: 'root'
},
level1: {
_fallback: 'root',
b: 'level1',
},
level2: {
_fallback: 'level1',
level1: {
g: 'level2.level1'
},
c: 'level2',
sublevel1: {
d: 'sublevel1'
},
sublevel2: {
e: 'sublevel2',
level1: {
f: 'sublevel2.level1'
}
}
}
};
const resolver = _createResolver([defaults]);
expect(resolver.level1).toEqualOptions({
a: 'root',
b: 'level1',
c: undefined
});
expect(resolver.level2).toEqualOptions({
a: 'root',
b: 'level1',
c: 'level2',
d: undefined
});
expect(resolver.level2.sublevel1).toEqualOptions({
a: 'root',
b: 'level1',
c: 'level2', // TODO: this should be undefined
d: 'sublevel1',
e: undefined,
f: undefined,
g: 'level2.level1'
});
expect(resolver.level2.sublevel2).toEqualOptions({
a: 'root',
b: 'level1',
c: 'level2', // TODO: this should be undefined
d: undefined,
e: 'sublevel2',
f: undefined,
g: 'level2.level1'
});
expect(resolver.level2.sublevel2.level1).toEqualOptions({
a: 'root',
b: 'level1',
c: 'level2', // TODO: this should be undefined
d: undefined,
e: 'sublevel2', // TODO: this should be undefined
f: 'sublevel2.level1',
g: 'level2.level1'
});
});
});
});

describe('_attachContext', function() {
Expand Down

0 comments on commit 850e6e4

Please sign in to comment.