Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

DXCDT-373: Address notation for keyword preservation #741

Merged
merged 16 commits into from
Feb 14, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
);
});
});