Skip to content

Commit

Permalink
DXCDT-373: Address notation for keyword preservation (#741)
Browse files Browse the repository at this point in the history
* Adding shouldFieldBePreserved function

* Adding getPreservableFieldsFromAssets function with test

* Adding array keyword replace case

* Adding undefined and null check

* Adding address notation to traversal

* Adding address notation lookup

* Adding stronger tests, cleaning up code

* Adding more context

* Adding address notation to traversal

* Adding address notation lookup

* Adding stronger tests, cleaning up code

* Adding more context

* Removing unused package

* Removing unused package

---------

Co-authored-by: Will Vedder <[email protected]>
  • Loading branch information
willvedd and willvedd authored Feb 14, 2023
1 parent 808aa3e commit ec3d248
Show file tree
Hide file tree
Showing 2 changed files with 137 additions and 11 deletions.
66 changes: 63 additions & 3 deletions src/keywordPreservation.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { get as getByDotNotation } from 'dot-prop';
import { KeywordMappings } from './types';
import { keywordReplaceArrayRegExp, keywordReplaceStringRegExp } from './tools/utils';

Expand All @@ -15,18 +16,32 @@ export const shouldFieldBePreserved = (

export const getPreservableFieldsFromAssets = (
asset: any,
address: string,
keywordMappings: KeywordMappings
): string[] => {
if (typeof asset === 'string') {
if (shouldFieldBePreserved(asset, keywordMappings)) {
return [asset];
return [address];
}
return [];
}

const shouldRenderDot = address !== '';

if (Array.isArray(asset)) {
return asset
.map((arrayItem) => {
return getPreservableFieldsFromAssets(arrayItem, keywordMappings);
// Using the `name` field as the primary unique identifier for array items
// TODO: expand the available identifier fields to encompass objects that lack name
const hasIdentifier = arrayItem.name !== undefined;

if (!hasIdentifier) return [];

return getPreservableFieldsFromAssets(
arrayItem,
`${address}${shouldRenderDot ? '.' : ''}[name=${arrayItem.name}]`,
keywordMappings
);
})
.flat();
}
Expand All @@ -36,9 +51,54 @@ export const getPreservableFieldsFromAssets = (
const value = asset[key];

if (value === undefined || value === null) return [];
return getPreservableFieldsFromAssets(value, keywordMappings);

return getPreservableFieldsFromAssets(
value,
`${address}${shouldRenderDot ? '.' : ''}${key}`,
keywordMappings
);
})
.flat();
}
return [];
};

// getAssetsValueByAddress returns a value for an arbitrary data structure when
// provided an "address" of that value. This address is similar to JS object notation
// with the exception of identifying array items by a unique property instead of order.
// Example:
// Object: `{ actions: [ { name: "action-1", code: "..."}] }`
// Address: `.actions[name=action-1].code`
export const getAssetsValueByAddress = (address: string, assets: any): any => {
//Look ahead and see if the address path only contains dots (ex: `tenant.friendly_name`)
//if so the address is trivial and can use the dot-prop package to return the value

const isTrivialAddress = address.indexOf('[') === -1;
if (isTrivialAddress) {
return getByDotNotation(assets, address);
}

// It is easier to handle an address piece-by-piece by
// splitting on the period into separate "directions"
const directions = address.split('.');

// If the the next directions are the proprietary array syntax (ex: `[name=foo]`)
// then perform lookup against unique array-item property
if (directions[0].charAt(0) === '[') {
const identifier = directions[0].substring(1, directions[0].length - 1).split('=')[0];
const identifierValue = directions[0].substring(1, directions[0].length - 1).split('=')[1];

if (assets === undefined) return undefined;

const target = assets.find((item: any) => {
return item[identifier] === identifierValue;
});

return getAssetsValueByAddress(directions.slice(1).join('.'), target);
}

return getAssetsValueByAddress(
directions.slice(1).join('.'),
getByDotNotation(assets, directions[0])
);
};
82 changes: 74 additions & 8 deletions test/keywordPreservation.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
import { expect } from 'chai';
import { shouldFieldBePreserved, getPreservableFieldsFromAssets } from '../src/keywordPreservation';
import {
shouldFieldBePreserved,
getPreservableFieldsFromAssets,
getAssetsValueByAddress,
} from '../src/keywordPreservation';

describe('#Keyword Preservation', () => {
describe('shouldFieldBePreserved', () => {
Expand Down Expand Up @@ -54,7 +58,17 @@ describe('#Keyword Preservation', () => {
},
array: [
{
nestedArray: ['Nested array value 1 ##KEYWORD##', 'Nested array value 2 ##KEYWORD##'],
name: 'array-item-1',
nestedArray: [
{
name: 'nested-array-item-1',
value: 'Nested array value 1 ##KEYWORD##',
},
{
name: 'nested-array-item-2',
value: 'Nested array value 2 ##KEYWORD##',
},
],
notInKeywordMapping: '##NOT_IN_KEYWORD_MAPPING##',
nested: {
nestedProperty: 'Another nested array property ##KEYWORD##',
Expand All @@ -65,20 +79,72 @@ describe('#Keyword Preservation', () => {
nullField: null,
undefinedField: undefined,
},
'',
{
KEYWORD: 'Travel0',
ARRAY_REPLACE_KEYWORD: ['this value', 'that value'],
}
);

expect(fieldsToPreserve).to.have.members([
'Friendly name ##KEYWORD##',
'Nested property ##KEYWORD##',
'Nested array value 1 ##KEYWORD##',
'Nested array value 2 ##KEYWORD##',
'Another nested array property ##KEYWORD##',
'@@ARRAY_REPLACE_KEYWORD@@',
'object.friendly_name',
'object.nested.nestedProperty',
'array.[name=array-item-1].nestedArray.[name=nested-array-item-1].value',
'array.[name=array-item-1].nestedArray.[name=nested-array-item-2].value',
'array.[name=array-item-1].nested.nestedProperty',
'arrayReplace',
]);
});
});
});

describe('getAssetsValueByAddress', () => {
it('should find the value of the addressed property', () => {
const mockAssetTree = {
tenant: {
display_name: 'This is my tenant display name',
},
clients: [
{
name: 'client-1',
display_name: 'Some Display Name',
},
{
name: 'client-2',
display_name: 'This is the target value',
},
{
name: 'client-3',
connections: [
{
connection_name: 'connection-1',
display_name: 'My connection display name',
},
],
},
],
};

expect(getAssetsValueByAddress('tenant.display_name', mockAssetTree)).to.equal(
'This is my tenant display name'
);
expect(getAssetsValueByAddress('clients.[name=client-2].display_name', mockAssetTree)).to.equal(
'This is the target value'
);
expect(
getAssetsValueByAddress(
'clients.[name=client-3].connections.[connection_name=connection-1].display_name',
mockAssetTree
)
).to.equal('My connection display name');
expect(getAssetsValueByAddress('this.address.should.not.exist', mockAssetTree)).to.equal(
undefined
);
expect(getAssetsValueByAddress('this.address.[should=not].exist', mockAssetTree)).to.equal(
undefined
);
expect(getAssetsValueByAddress('this.address.should.[not=exist]', mockAssetTree)).to.equal(
undefined
);
});
});

0 comments on commit ec3d248

Please sign in to comment.