-
Notifications
You must be signed in to change notification settings - Fork 98
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat!: component set components are now DecodableMaps (#1080)
* feat!: component set components are now DecodableMaps * fix: move DecodeableMap to a new file * chore: add tests for DecodeableMap
- Loading branch information
Showing
3 changed files
with
257 additions
and
12 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,78 @@ | ||
/* | ||
* Copyright (c) 2023, salesforce.com, inc. | ||
* All rights reserved. | ||
* Licensed under the BSD 3-Clause license. | ||
* For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause | ||
*/ | ||
|
||
/** | ||
* This is an extension of the Map class that treats keys as the same by matching first normally, | ||
* then decoded. Decoding the key before comparing can solve some edge cases in component fullNames | ||
* such as Layouts. See: https://github.com/forcedotcom/cli/issues/1683 | ||
* | ||
* Examples: | ||
* | ||
* Given a map with entries: | ||
* ```javascript | ||
* 'layout#Layout__Broker__c-v1%2E1 Broker Layout' : {...} | ||
* 'layout#Layout__Broker__c-v9.2 Broker Layout' : {...} | ||
* ``` | ||
* | ||
* `decodeableMap.has('layout#Layout__Broker__c-v1.1 Broker Layout')` --> returns `true` | ||
* `decodeableMap.has('layout#Layout__Broker__c-v9%2E2 Broker Layout')` --> returns `true` | ||
*/ | ||
export class DecodeableMap<K extends string, V> extends Map<string, V> { | ||
/** | ||
* boolean indicating whether an element with the specified key (matching decoded) exists or not. | ||
*/ | ||
public has(key: K): boolean { | ||
return super.has(key) || this.hasDecoded(key); | ||
} | ||
|
||
/** | ||
* Returns a specified element from the Map object. If the value that is associated to | ||
* the provided key (matching decoded) is an object, then you will get a reference to | ||
* that object and any change made to that object will effectively modify it inside the Map. | ||
*/ | ||
public get(key: K): V | undefined { | ||
return super.get(key) ?? this.getDecoded(key); | ||
} | ||
|
||
/** | ||
* Adds a new element with a specified key and value to the Map. If an element with the | ||
* same key (matching decoded) already exists, the element will be updated. | ||
*/ | ||
public set(key: K, value: V): this { | ||
const sKey = this.getExistingKey(key) ?? key; | ||
return super.set(sKey, value); | ||
} | ||
|
||
/** | ||
* true if an element in the Map existed (matching decoded) and has been removed, or false | ||
* if the element does not exist. | ||
*/ | ||
public delete(key: K): boolean { | ||
const sKey = this.getExistingKey(key) ?? key; | ||
return super.delete(sKey); | ||
} | ||
|
||
// Returns true if the passed `key` matches an existing key entry when both keys are decoded. | ||
private hasDecoded(key: string): boolean { | ||
return !!this.getExistingKey(key); | ||
} | ||
|
||
// Returns the value of an entry matching on decoded keys. | ||
private getDecoded(key: string): V | undefined { | ||
const existingKey = this.getExistingKey(key); | ||
return existingKey ? super.get(existingKey) : undefined; | ||
} | ||
|
||
// Returns the key as it is in the map, matching on decoded keys. | ||
private getExistingKey(key: string): string | undefined { | ||
for (const compKey of this.keys()) { | ||
if (decodeURIComponent(compKey) === decodeURIComponent(key)) { | ||
return compKey; | ||
} | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,168 @@ | ||
/* | ||
* Copyright (c) 2020, salesforce.com, inc. | ||
* All rights reserved. | ||
* Licensed under the BSD 3-Clause license. | ||
* For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause | ||
*/ | ||
import { expect } from 'chai'; | ||
import * as sinon from 'sinon'; | ||
import { DecodeableMap } from '../../src/collections/decodeableMap'; | ||
|
||
describe('DecodeableMap', () => { | ||
let dMap: DecodeableMap<string, string>; | ||
const ENCODED_KEY = 'encodedKey'; | ||
const DECODED_KEY = 'decodedKey'; | ||
|
||
const sandbox = sinon.createSandbox(); | ||
let hasDecodedSpy: sinon.SinonSpy; | ||
let getDecodedSpy: sinon.SinonSpy; | ||
let hasMapSpy: sinon.SinonSpy; | ||
let getMapSpy: sinon.SinonSpy; | ||
let setMapSpy: sinon.SinonSpy; | ||
let deleteMapSpy: sinon.SinonSpy; | ||
|
||
beforeEach(() => { | ||
dMap = new DecodeableMap([ | ||
['Layout-v1%2E1 Layout', ENCODED_KEY], | ||
['Layout-v9.2 Layout', DECODED_KEY], | ||
]); | ||
// eslint-disable-next-line @typescript-eslint/no-explicit-any | ||
hasDecodedSpy = sandbox.spy(dMap, 'hasDecoded' as any); | ||
// eslint-disable-next-line @typescript-eslint/no-explicit-any | ||
getDecodedSpy = sandbox.spy(dMap, 'getDecoded' as any); | ||
hasMapSpy = sandbox.spy(Map.prototype, 'has'); | ||
getMapSpy = sandbox.spy(Map.prototype, 'get'); | ||
setMapSpy = sandbox.spy(Map.prototype, 'set'); | ||
deleteMapSpy = sandbox.spy(Map.prototype, 'delete'); | ||
}); | ||
|
||
afterEach(() => { | ||
sandbox.restore(); | ||
}); | ||
|
||
describe('has()', () => { | ||
it('should match on exact key without decoding', () => { | ||
expect(dMap.has('Layout-v1%2E1 Layout')).to.be.true; | ||
expect(hasMapSpy.called).to.be.true; | ||
expect(hasDecodedSpy.called).to.be.false; | ||
}); | ||
|
||
it('should match encoded key with decoded value', () => { | ||
expect(dMap.has('Layout-v1.1 Layout')).to.be.true; | ||
expect(hasMapSpy.called).to.be.true; | ||
expect(hasDecodedSpy.called).to.be.true; | ||
}); | ||
|
||
it('should match decoded key with encoded value', () => { | ||
expect(dMap.has('Layout-v9%2E2 Layout')).to.be.true; | ||
expect(hasMapSpy.called).to.be.true; | ||
expect(hasDecodedSpy.called).to.be.true; | ||
}); | ||
|
||
it('should not match on no existing key', () => { | ||
expect(dMap.has('Layout-MISSING Layout')).to.be.false; | ||
expect(hasMapSpy.called).to.be.true; | ||
expect(hasDecodedSpy.called).to.be.true; | ||
}); | ||
}); | ||
|
||
describe('get()', () => { | ||
it('should get value with exact key without decoding', () => { | ||
expect(dMap.get('Layout-v1%2E1 Layout')).to.equal(ENCODED_KEY); | ||
expect(getMapSpy.calledOnce).to.be.true; | ||
expect(getDecodedSpy.called).to.be.false; | ||
}); | ||
|
||
it('should get value of encoded key using decoded key', () => { | ||
expect(dMap.get('Layout-v1.1 Layout')).to.equal(ENCODED_KEY); | ||
expect(getMapSpy.calledTwice).to.be.true; | ||
expect(getDecodedSpy.calledOnce).to.be.true; | ||
}); | ||
|
||
it('should get value of decoded key using encoded key', () => { | ||
expect(dMap.get('Layout-v9%2E2 Layout')).to.equal(DECODED_KEY); | ||
expect(getMapSpy.calledTwice).to.be.true; | ||
expect(getDecodedSpy.calledOnce).to.be.true; | ||
}); | ||
|
||
it('should return undefined on no existing key', () => { | ||
expect(dMap.get('Layout-MISSING Layout')).to.be.undefined; | ||
expect(getMapSpy.calledOnce).to.be.true; | ||
expect(getDecodedSpy.called).to.be.true; | ||
}); | ||
}); | ||
|
||
describe('set()', () => { | ||
const NEW_VALUE = 'new value from set'; | ||
|
||
it('should set value with exact key', () => { | ||
expect(dMap.set('Layout-v1%2E1 Layout', NEW_VALUE)).to.equal(dMap); | ||
expect(setMapSpy.called).to.be.true; | ||
expect(setMapSpy.lastCall.args[0]).to.equal('Layout-v1%2E1 Layout'); | ||
expect(setMapSpy.lastCall.args[1]).to.equal(NEW_VALUE); | ||
expect(dMap.size).to.equal(2); | ||
expect(dMap.get('Layout-v1%2E1 Layout')).to.equal(NEW_VALUE); | ||
}); | ||
|
||
it('should set value of encoded key using decoded key', () => { | ||
expect(dMap.set('Layout-v1.1 Layout', NEW_VALUE)).to.equal(dMap); | ||
expect(setMapSpy.called).to.be.true; | ||
expect(setMapSpy.lastCall.args[0]).to.equal('Layout-v1%2E1 Layout'); | ||
expect(setMapSpy.lastCall.args[1]).to.equal(NEW_VALUE); | ||
expect(dMap.size).to.equal(2); | ||
expect(dMap.get('Layout-v1%2E1 Layout')).to.equal(NEW_VALUE); | ||
}); | ||
|
||
it('should set value of decoded key using encoded key', () => { | ||
expect(dMap.set('Layout-v9%2E2 Layout', NEW_VALUE)).to.equal(dMap); | ||
expect(setMapSpy.called).to.be.true; | ||
expect(setMapSpy.lastCall.args[0]).to.equal('Layout-v9.2 Layout'); | ||
expect(setMapSpy.lastCall.args[1]).to.equal(NEW_VALUE); | ||
expect(dMap.size).to.equal(2); | ||
expect(dMap.get('Layout-v9.2 Layout')).to.equal(NEW_VALUE); | ||
}); | ||
|
||
it('should set new entry on no existing key', () => { | ||
expect(dMap.set('Layout-MISSING Layout', NEW_VALUE)).to.equal(dMap); | ||
expect(setMapSpy.called).to.be.true; | ||
expect(setMapSpy.lastCall.args[0]).to.equal('Layout-MISSING Layout'); | ||
expect(setMapSpy.lastCall.args[1]).to.equal(NEW_VALUE); | ||
expect(dMap.size).to.equal(3); | ||
expect(dMap.get('Layout-MISSING Layout')).to.equal(NEW_VALUE); | ||
}); | ||
}); | ||
|
||
describe('delete()', () => { | ||
it('should delete using exact key', () => { | ||
expect(dMap.delete('Layout-v1%2E1 Layout')).to.be.true; | ||
expect(deleteMapSpy.calledOnce).to.be.true; | ||
expect(deleteMapSpy.firstCall.args[0]).to.equal('Layout-v1%2E1 Layout'); | ||
expect(dMap.size).to.equal(1); | ||
expect(dMap.has('Layout-v1%2E1 Layout')).to.be.false; | ||
}); | ||
|
||
it('should delete the encoded key using decoded value', () => { | ||
expect(dMap.delete('Layout-v1.1 Layout')).to.be.true; | ||
expect(deleteMapSpy.calledOnce).to.be.true; | ||
expect(deleteMapSpy.firstCall.args[0]).to.equal('Layout-v1%2E1 Layout'); | ||
expect(dMap.size).to.equal(1); | ||
expect(dMap.has('Layout-v1.1 Layout')).to.be.false; | ||
}); | ||
|
||
it('should delete the decoded key using encoded value', () => { | ||
expect(dMap.delete('Layout-v9%2E2 Layout')).to.be.true; | ||
expect(deleteMapSpy.calledOnce).to.be.true; | ||
expect(deleteMapSpy.firstCall.args[0]).to.equal('Layout-v9.2 Layout'); | ||
expect(dMap.size).to.equal(1); | ||
expect(dMap.has('Layout-v9%2E2 Layout')).to.be.false; | ||
}); | ||
|
||
it('should not delete on no existing key', () => { | ||
expect(dMap.delete('Layout-MISSING Layout')).to.be.false; | ||
expect(deleteMapSpy.calledOnce).to.be.true; | ||
expect(deleteMapSpy.firstCall.args[0]).to.equal('Layout-MISSING Layout'); | ||
expect(dMap.size).to.equal(2); | ||
expect(dMap.has('Layout-MISSING Layout')).to.be.false; | ||
}); | ||
}); | ||
}); |