Skip to content

Commit

Permalink
Merge pull request #1056 from amzn/utils
Browse files Browse the repository at this point in the history
feat: refactor reference utils and expose as standalone functions
  • Loading branch information
jorenbroekema authored Dec 5, 2023
2 parents 613023e + 90d2cfc commit 00956de
Show file tree
Hide file tree
Showing 21 changed files with 564 additions and 241 deletions.
18 changes: 18 additions & 0 deletions .changeset/happy-numbers-grab.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
---
'style-dictionary': minor
---

Expose a new utility called resolveReferences which takes a value containing references, the dictionary object, and resolves the value's references for you.

```js
import StyleDictionary from 'style-dictionary';
import { resolveReferences } from 'style-dictionary/utils';

const sd = new StyleDictionary({ tokens: {
foo: { value: 'foo' },
bar: { value: '{foo}' },
qux: { value: '{bar}' },
}});

console.log(resolveReferences(sd.tokens.qux.value, sd.tokens)); // 'foo'
```
5 changes: 5 additions & 0 deletions .changeset/sweet-toes-fly.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'style-dictionary': minor
---

BREAKING: expose getReferences and usesReference utilities as standalone utils rather than requiring them to be bound to dictionary object. This makes it easier to use.
15 changes: 8 additions & 7 deletions __tests__/utils/reference/getReferences.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,7 @@
*/

import { expect } from 'chai';
// `.getReferences` is bound to a dictionary object, so to test it we will
// create a dictionary object and then call `.getReferences` on it.
import getReferences from '../../../lib/utils/references/getReferences.js';
import createDictionary from '../../../lib/utils/createDictionary.js';

const tokens = {
Expand Down Expand Up @@ -54,26 +53,28 @@ describe('utils', () => {
describe('reference', () => {
describe('getReferences()', () => {
it(`should return an empty array if the value has no references`, () => {
expect(dictionary.getReferences(tokens.color.red.value)).to.eql([]);
expect(getReferences(dictionary, tokens.color.red.value)).to.eql([]);
});

it(`should work with a single reference`, () => {
expect(dictionary.getReferences(tokens.color.danger.value)).to.eql([{ value: '#f00' }]);
expect(getReferences(dictionary, tokens.color.danger.value)).to.eql([{ value: '#f00' }]);
});

it(`should work with object values`, () => {
expect(dictionary.getReferences(tokens.border.primary.value)).to.eql([
expect(getReferences(dictionary, tokens.border.primary.value)).to.eql([
{ value: '#f00' },
{ value: '2px' },
]);
});

it(`should work with objects that have numbers`, () => {
expect(dictionary.getReferences(tokens.border.secondary.value)).to.eql([{ value: '#f00' }]);
expect(getReferences(dictionary, tokens.border.secondary.value)).to.eql([
{ value: '#f00' },
]);
});

it(`should work with interpolated values`, () => {
expect(dictionary.getReferences(tokens.border.tertiary.value)).to.eql([
expect(getReferences(dictionary, tokens.border.tertiary.value)).to.eql([
{ value: '2px' },
{ value: '#f00' },
]);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
* and limitations under the License.
*/
import { expect } from 'chai';
import resolveReference from '../../../lib/utils/references/resolveReference.js';
import getValueByPath from '../../../lib/utils/references/getValueByPath.js';

const dictionary = {
color: {
Expand All @@ -28,28 +28,28 @@ const dictionary = {
arr: ['one', 'two'],
};

describe('resolveReference()', () => {
describe('getValueByPath()', () => {
it(`returns undefined for non-strings`, () => {
expect(resolveReference(42, dictionary)).to.be.undefined;
expect(getValueByPath(42, dictionary)).to.be.undefined;
});

it(`returns undefined if it does not find the path in the object`, () => {
expect(resolveReference(['color', 'foo'], dictionary)).to.be.undefined;
expect(resolveReference(['color', 'foo', 'bar'], dictionary)).to.be.undefined;
expect(getValueByPath(['color', 'foo'], dictionary)).to.be.undefined;
expect(getValueByPath(['color', 'foo', 'bar'], dictionary)).to.be.undefined;
});

it(`returns the part of the object if referenced path exists`, () => {
expect(resolveReference(['color', 'palette', 'neutral', '0', 'value'], dictionary)).to.equal(
expect(getValueByPath(['color', 'palette', 'neutral', '0', 'value'], dictionary)).to.equal(
dictionary.color.palette.neutral['0'].value,
);
expect(resolveReference(['color'], dictionary)).to.equal(dictionary.color);
expect(getValueByPath(['color'], dictionary)).to.equal(dictionary.color);
});

it(`works with arrays`, () => {
expect(resolveReference(['arr'], dictionary)).to.equal(dictionary.arr);
expect(getValueByPath(['arr'], dictionary)).to.equal(dictionary.arr);
});

it(`works with array indices`, () => {
expect(resolveReference(['arr', '0'], dictionary)).to.equal(dictionary.arr[0]);
expect(getValueByPath(['arr', '0'], dictionary)).to.equal(dictionary.arr[0]);
});
});
223 changes: 223 additions & 0 deletions __tests__/utils/reference/resolveReferences.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,223 @@
/*
* Copyright 2017 Amazon.com, Inc. or its affiliates. All Rights Reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance with
* the License. A copy of the License is located at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* or in the "license" file accompanying this file. This file is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR
* CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions
* and limitations under the License.
*/
import { expect } from 'chai';
import { fileToJSON } from '../../__helpers.js';
import { resolveReferences } from '../../../lib/utils/references/resolveReferences.js';
import GroupMessages from '../../../lib/utils/groupMessages.js';

const PROPERTY_REFERENCE_WARNINGS = GroupMessages.GROUP.PropertyReferenceWarnings;

describe('utils', () => {
describe('references', () => {
describe('resolveReferences', () => {
beforeEach(() => {
GroupMessages.clear(PROPERTY_REFERENCE_WARNINGS);
});

it('should do simple references', () => {
const test = resolveReferences('{foo}', fileToJSON('__tests__/__json_files/simple.json'));
expect(test).to.equal('bar');
});

it('should do simple interpolation for both strings and numbers', () => {
const obj = fileToJSON('__tests__/__json_files/interpolation.json');
expect(resolveReferences(obj.c, obj)).to.equal('test1 value text after');
expect(resolveReferences(obj.d, obj)).to.equal('text before test1 value');
expect(resolveReferences(obj.e, obj)).to.equal('text before test1 value text after');
expect(resolveReferences(obj.f, obj)).to.equal('123 text after');
expect(resolveReferences(obj.g, obj)).to.equal('text before 123');
expect(resolveReferences(obj.h, obj)).to.equal('text before 123 text after');
});

it('should do nested references', () => {
const obj = fileToJSON('__tests__/__json_files/nested_references.json');
expect(resolveReferences(obj.i, obj)).to.equal(2);
expect(resolveReferences(obj.a.b.d, obj)).to.equal(2);
expect(resolveReferences(obj.e.f.h, obj)).to.equal(1);
});

it('should handle nested pointers', () => {
const obj = fileToJSON('__tests__/__json_files/nested_pointers.json');
expect(resolveReferences(obj.b, obj)).to.equal(1);
expect(resolveReferences(obj.c, obj)).to.equal(1);
});

it('should handle deep nested pointers', () => {
const obj = fileToJSON('__tests__/__json_files/nested_pointers_2.json');
expect(resolveReferences(obj.a, obj)).to.equal(1);
expect(resolveReferences(obj.b, obj)).to.equal(1);
expect(resolveReferences(obj.c, obj)).to.equal(1);
expect(resolveReferences(obj.d, obj)).to.equal(1);
expect(resolveReferences(obj.e, obj)).to.equal(1);
expect(resolveReferences(obj.f, obj)).to.equal(1);
});

it('should handle deep nested pointers with string interpolation', () => {
const obj = fileToJSON('__tests__/__json_files/nested_pointers_3.json');
expect(resolveReferences(obj.a, obj)).to.equal('foo bon bee bae boo bla baz bar');
expect(resolveReferences(obj.b, obj)).to.equal('foo bon bee bae boo bla baz');
expect(resolveReferences(obj.c, obj)).to.equal('foo bon bee bae boo bla');
expect(resolveReferences(obj.d, obj)).to.equal('foo bon bee bae boo');
expect(resolveReferences(obj.e, obj)).to.equal('foo bon bee bae');
expect(resolveReferences(obj.f, obj)).to.equal('foo bon bee');
expect(resolveReferences(obj.g, obj)).to.equal('foo bon');
});

it('should handle deep nested pointers and nested references', () => {
const obj = fileToJSON('__tests__/__json_files/nested_pointers_4.json');
expect(resolveReferences(obj.a.a.a, obj)).to.equal(1);
expect(resolveReferences(obj.b.b.b, obj)).to.equal(1);
expect(resolveReferences(obj.c.c.c, obj)).to.equal(1);
expect(resolveReferences(obj.d.d.d, obj)).to.equal(1);
expect(resolveReferences(obj.e.e.e, obj)).to.equal(1);
expect(resolveReferences(obj.f.f.f, obj)).to.equal(1);
});

it('should keep the type of the referenced property', () => {
const obj = fileToJSON('__tests__/__json_files/reference_type.json');
expect(resolveReferences(obj.d, obj)).to.equal(1);
expect(typeof resolveReferences(obj.d, obj)).to.equal('number');
expect(typeof resolveReferences(obj.e, obj)).to.equal('object');
expect(resolveReferences(obj.e, obj)).to.eql({ c: 2 });
expect(resolveReferences(obj.g, obj)).to.eql([1, 2, 3]);
});

it('should handle and evaluate items in an array', () => {
const obj = fileToJSON('__tests__/__json_files/array.json');
expect(resolveReferences(obj.d[0], obj)).to.equal(2);
expect(resolveReferences(obj.d[1], obj)).to.equal(1);
expect(resolveReferences(obj.e[0].a, obj)).to.equal(1);
expect(resolveReferences(obj.e[1].a, obj)).to.equal(2);
});

it("should store warning if pointers don't exist", () => {
const obj = fileToJSON('__tests__/__json_files/non_existent.json');
expect(resolveReferences(obj.foo, obj)).to.be.undefined;
expect(resolveReferences(obj.error, obj)).to.be.undefined;
expect(GroupMessages.fetchMessages(PROPERTY_REFERENCE_WARNINGS)).to.eql([
"Reference doesn't exist: tries to reference bar, which is not defined",
"Reference doesn't exist: tries to reference a.b.d, which is not defined",
]);
});

it('should gracefully handle basic circular references', () => {
const obj = fileToJSON('__tests__/__json_files/circular.json');
expect(resolveReferences(obj.a, obj)).to.equal('{b}');
expect(GroupMessages.count(PROPERTY_REFERENCE_WARNINGS)).to.equal(1);
expect(JSON.stringify(GroupMessages.fetchMessages(PROPERTY_REFERENCE_WARNINGS))).to.equal(
JSON.stringify(['Circular definition cycle: b, c, d, a, b']),
);
});

it('should gracefully handle basic and nested circular references', () => {
const obj = fileToJSON('__tests__/__json_files/circular_2.json');
expect(resolveReferences(obj.j, obj)).to.equal('{a.b.c}');
expect(GroupMessages.count(PROPERTY_REFERENCE_WARNINGS)).to.equal(1);
expect(JSON.stringify(GroupMessages.fetchMessages(PROPERTY_REFERENCE_WARNINGS))).to.equal(
JSON.stringify(['Circular definition cycle: a.b.c, j, a.b.c']),
);
});

it('should gracefully handle nested circular references', () => {
const obj = fileToJSON('__tests__/__json_files/circular_3.json');
expect(resolveReferences(obj.c.d.e, obj)).to.equal('{a.b}');
expect(GroupMessages.count(PROPERTY_REFERENCE_WARNINGS)).to.equal(1);
expect(JSON.stringify(GroupMessages.fetchMessages(PROPERTY_REFERENCE_WARNINGS))).to.equal(
JSON.stringify(['Circular definition cycle: a.b, c.d.e, a.b']),
);
});

it('should gracefully handle multiple nested circular references', () => {
const obj = fileToJSON('__tests__/__json_files/circular_4.json');
expect(resolveReferences(obj.h.i, obj)).to.equal('{a.b.c.d}');
expect(GroupMessages.count(PROPERTY_REFERENCE_WARNINGS)).to.equal(1);
expect(JSON.stringify(GroupMessages.fetchMessages(PROPERTY_REFERENCE_WARNINGS))).to.equal(
JSON.stringify(['Circular definition cycle: a.b.c.d, e.f.g, h.i, a.b.c.d']),
);
});

it('should gracefully handle down-chain circular references', () => {
const obj = fileToJSON('__tests__/__json_files/circular_5.json');
expect(resolveReferences(obj.n, obj)).to.equal('{l}');
expect(GroupMessages.count(PROPERTY_REFERENCE_WARNINGS)).to.equal(1);
expect(JSON.stringify(GroupMessages.fetchMessages(PROPERTY_REFERENCE_WARNINGS))).to.equal(
JSON.stringify(['Circular definition cycle: l, m, l']),
);
});

it('should correctly resolve multiple references without reference errors', function () {
const obj = fileToJSON('__tests__/__json_files/not_circular.json');
expect(resolveReferences(obj.prop8.value, obj)).to.equal(5);
expect(resolveReferences(obj.prop12.value, obj)).to.equal(
'test1 value, test2 value and some extra stuff',
);
expect(resolveReferences(obj.prop124.value, obj)).to.equal(
'test1 value, test2 value and test1 value',
);
expect(resolveReferences(obj.prop15.value, obj)).to.equal(
'test1 value, 5 and some extra stuff',
);
expect(resolveReferences(obj.prop156.value, obj)).to.equal('test1 value, 5 and 6');
expect(resolveReferences(obj.prop1568.value, obj)).to.equal('test1 value, 5, 6 and 5');
expect(GroupMessages.count(PROPERTY_REFERENCE_WARNINGS)).to.equal(0);
});

describe('ignorePaths', () => {
it('should not resolve values containing constiables in ignored paths', () => {
const obj = {
foo: { value: 'bar' },
bar: {
value: '{foo.value}',
},
};
const test = resolveReferences(obj.bar.value, obj, { ignorePaths: ['foo.value'] });
expect(test).to.equal('{foo.value}');
});
});

it('should handle spaces', () => {
const obj = {
foo: { value: 'foo' },
bar: { value: '{ foo.value }' },
};
const test = resolveReferences(obj.bar.value, obj);
expect(test).to.equal('foo');
});

it('should collect multiple reference errors', () => {
const obj = fileToJSON('__tests__/__json_files/multiple_reference_errors.json');
expect(resolveReferences(obj.a.b, obj)).to.be.undefined;
expect(resolveReferences(obj.a.c, obj)).to.be.undefined;
expect(resolveReferences(obj.a.d, obj)).to.be.undefined;
expect(GroupMessages.count(PROPERTY_REFERENCE_WARNINGS)).to.equal(3);
expect(JSON.stringify(GroupMessages.fetchMessages(PROPERTY_REFERENCE_WARNINGS))).to.equal(
JSON.stringify([
"Reference doesn't exist: tries to reference b.a, which is not defined",
"Reference doesn't exist: tries to reference b.c, which is not defined",
"Reference doesn't exist: tries to reference d, which is not defined",
]),
);
});

it('should handle 0', () => {
const obj = {
test: { value: '{zero.value}' },
zero: { value: 0 },
};
const test = resolveReferences(obj.test.value, obj);
expect(GroupMessages.fetchMessages(PROPERTY_REFERENCE_WARNINGS).length).to.equal(0);
expect(test).to.equal(0);
});
});
});
});
Loading

0 comments on commit 00956de

Please sign in to comment.