diff --git a/API.md b/API.md index a3a3946f..ee292177 100644 --- a/API.md +++ b/API.md @@ -66,10 +66,33 @@ Returns a string representation of this construct. | **Name** | **Description** | | --- | --- | +| createRoot | Creates a new root construct node. | | isConstruct | Checks if `x` is a construct. | --- +##### `createRoot` + +```typescript +import { Construct } from 'constructs' + +Construct.createRoot(id?: string) +``` + +Creates a new root construct node. + +###### `id`Optional + +- *Type:* string + +The scoped construct ID. + +Must be unique amongst siblings. If +the ID includes a path separator (`/`), then it will be replaced by double +dash `--`. + +--- + ##### `isConstruct` ```typescript @@ -774,9 +797,9 @@ followed by 40 lowercase hexadecimal characters (0-9a-f). Addresses are calculated using a SHA-1 of the components of the construct path. -To enable refactorings of construct trees, constructs with the ID `Default` +To enable refactoring of construct trees, constructs with the ID `Default` will be excluded from the calculation. In those cases constructs in the -same tree may have the same addreess. +same tree may have the same address. --- diff --git a/src/construct.ts b/src/construct.ts index a7b52f64..8456053d 100644 --- a/src/construct.ts +++ b/src/construct.ts @@ -95,9 +95,9 @@ export class Node { * Addresses are calculated using a SHA-1 of the components of the construct * path. * - * To enable refactorings of construct trees, constructs with the ID `Default` + * To enable refactoring of construct trees, constructs with the ID `Default` * will be excluded from the calculation. In those cases constructs in the - * same tree may have the same addreess. + * same tree may have the same address. * * @example c83a2846e506bcc5f10682b564084bca2d275709ee */ @@ -465,6 +465,18 @@ export class Construct implements IConstruct { return x && typeof x === 'object' && x[CONSTRUCT_SYM]; } + /** + * Creates a new root construct node. + * + * @param id The scoped construct ID. Must be unique amongst siblings. If + * the ID includes a path separator (`/`), then it will be replaced by double + * dash `--`. + */ + public static createRoot(id?: string): Construct { + return new Construct(undefined as any, id ?? ''); + } + + /** * The tree node. */ diff --git a/test/construct.test.ts b/test/construct.test.ts index 5da39baa..2c5a0586 100644 --- a/test/construct.test.ts +++ b/test/construct.test.ts @@ -5,7 +5,7 @@ import { Construct, ConstructOrder, DependencyGroup, Dependable, IConstruct } fr // tslint:disable:max-line-length test('the "Root" construct is a special construct which can be used as the root of the tree', () => { - const root = new Root(); + const root = Construct.createRoot(); const node = root.node; expect(node.id).toBe(''); expect(node.scope).toBeUndefined(); @@ -13,7 +13,7 @@ test('the "Root" construct is a special construct which can be used as the root }); test('an empty string is a valid name for the root construct', () => { - const root = new Root(); + const root = Construct.createRoot(); expect(root.node.id).toEqual(''); expect(() => new Construct(root, '')).toThrow(/Only root constructs/); @@ -32,7 +32,7 @@ test('construct.name returns the name of the construct', () => { }); test('construct id can use any character except the path separator', () => { - const root = new Root(); + const root = Construct.createRoot(); expect(() => new Construct(root, 'valid')).not.toThrow(); expect(() => new Construct(root, 'ValiD')).not.toThrow(); expect(() => new Construct(root, 'Va123lid')).not.toThrow(); @@ -48,7 +48,7 @@ test('construct id can use any character except the path separator', () => { }); test('if construct id contains path seperators, they will be replaced by double-dash', () => { - const root = new Root(); + const root = Construct.createRoot(); const c = new Construct(root, 'Boom/Boom/Bam'); expect(c.node.id).toBe('Boom--Boom--Bam'); }); @@ -59,7 +59,7 @@ test('if "undefined" is forcefully used as an "id", it will be treated as an emp }); test('node.addr returns an opaque app-unique address for any construct', () => { - const root = new Root(); + const root = Construct.createRoot(); const child1 = new Construct(root, 'This is the first child'); const child2 = new Construct(child1, 'Second level'); @@ -76,7 +76,7 @@ test('node.addr returns an opaque app-unique address for any construct', () => { test('node.addr excludes "default" from the address calculation', () => { // GIVEN - const root = new Root(); + const root = Construct.createRoot(); const c1 = new Construct(root, 'c1'); // WHEN: @@ -97,7 +97,7 @@ test('node.addr excludes "default" from the address calculation', () => { }); test('construct.getChildren() returns an array of all children', () => { - const root = new Root(); + const root = Construct.createRoot(); const child = new Construct(root, 'Child1'); new Construct(root, 'Child2'); expect(child.node.children.length).toBe(0); @@ -105,15 +105,15 @@ test('construct.getChildren() returns an array of all children', () => { }); test('construct.findChild(name) can be used to retrieve a child from a parent', () => { - const root = new Root(); - const child = new Construct(root, 'Contruct'); + const root = Construct.createRoot(); + const child = new Construct(root, 'Construct'); expect(root.node.tryFindChild(child.node.id)).toBe(child); expect(root.node.tryFindChild('NotFound')).toBeUndefined(); }); test('construct.getChild(name) can be used to retrieve a child from a parent', () => { - const root = new Root(); - const child = new Construct(root, 'Contruct'); + const root = Construct.createRoot(); + const child = new Construct(root, 'Construct'); expect(root.node.findChild(child.node.id)).toBe(child); expect(() => root.node.findChild('NotFound')).toThrow(/No child with id: 'NotFound'/); }); @@ -158,7 +158,7 @@ test('construct.getAllContext can be used to read the full context of a root nod }; // WHEN - const t = new Root(); + const t = Construct.createRoot(); for (const [k, v] of Object.entries(context)) { t.node.setContext(k, v); } @@ -196,7 +196,7 @@ test('construct.tryGetContext(key) can be used to read a value from context defi // tslint:disable-next-line:max-line-length test('construct.setContext(k,v) sets context at some level and construct.tryGetContext(key) will return the lowermost value defined in the stack', () => { - const root = new Root(); + const root = Construct.createRoot(); const highChild = new Construct(root, 'highChild'); highChild.node.setContext('c1', 'root'); highChild.node.setContext('c2', 'root'); @@ -229,7 +229,7 @@ test('construct.setContext(k,v) sets context at some level and construct.tryGetC }); test('construct.setContext(key, value) can only be called before adding any children', () => { - const root = new Root(); + const root = Construct.createRoot(); new Construct(root, 'child1'); expect(() => root.node.setContext('k', 'v')).toThrow(/Cannot set context after children have been added: child1/); }); @@ -265,7 +265,7 @@ test('construct can not be created with the name of a sibling', () => { }); test('addMetadata(type, data) can be used to attach metadata to constructs', () => { - const root = new Root(); + const root = Construct.createRoot(); const con = new Construct(root, 'MyConstruct'); expect(con.node.metadata).toEqual([]); @@ -285,7 +285,7 @@ test('addMetadata(type, data) can be used to attach metadata to constructs', () }); test('addMetadata() respects the "stackTrace" option', () => { - const root = new Root(); + const root = Construct.createRoot(); const con = new Construct(root, 'Foo'); con.node.addMetadata('foo', 'bar1', { stackTrace: true }); @@ -297,7 +297,7 @@ test('addMetadata() respects the "stackTrace" option', () => { }); test('addMetadata(type, undefined/null) is ignored', () => { - const root = new Root(); + const root = Construct.createRoot(); const con = new Construct(root, 'Foo'); const node = con.node; node.addMetadata('Null', null); @@ -316,7 +316,7 @@ test('addMetadata(type, undefined/null) is ignored', () => { }); test('multiple children of the same type, with explicit names are welcome', () => { - const root = new Root(); + const root = Construct.createRoot(); new MyBeautifulConstruct(root, 'mbc1'); new MyBeautifulConstruct(root, 'mbc2'); new MyBeautifulConstruct(root, 'mbc3'); @@ -392,7 +392,7 @@ test('node.addValidation() can be implemented to perform validation, node.valida test('node.validate() returns an empty array if the construct does not implement IValidation', () => { // GIVEN - const root = new Root(); + const root = Construct.createRoot(); // THEN expect(root.node.validate()).toStrictEqual([]); @@ -400,7 +400,7 @@ test('node.validate() returns an empty array if the construct does not implement test('node.addValidation() can be used to add a validation function to a construct', () => { // GIVEN - const construct = new Root(); + const construct = Construct.createRoot(); construct.node.addValidation({ validate: () => ['error1', 'error2'] }); construct.node.addValidation({ validate: () => ['error3'] }); @@ -409,7 +409,7 @@ test('node.addValidation() can be used to add a validation function to a constru test('construct.lock() protects against adding children anywhere under this construct (direct or indirect)', () => { - const root = new Root(); + const root = Construct.createRoot(); const c0a = new Construct(root, 'c0a'); const c0b = new Construct(root, 'c0b'); @@ -461,7 +461,7 @@ test('"root" returns the root construct', () => { describe('defaultChild', () => { test('returns the child with id "Resource"', () => { - const root = new Root(); + const root = Construct.createRoot(); new Construct(root, 'child1'); const defaultChild = new Construct(root, 'Resource'); new Construct(root, 'child2'); @@ -469,7 +469,7 @@ describe('defaultChild', () => { expect(root.node.defaultChild).toBe(defaultChild); }); test('returns the child with id "Default"', () => { - const root = new Root(); + const root = Construct.createRoot(); new Construct(root, 'child1'); const defaultChild = new Construct(root, 'Default'); new Construct(root, 'child2'); @@ -477,7 +477,7 @@ describe('defaultChild', () => { expect(root.node.defaultChild).toBe(defaultChild); }); test('can override defaultChild', () => { - const root = new Root(); + const root = Construct.createRoot(); new Construct(root, 'Resource'); const defaultChild = new Construct(root, 'OtherResource'); root.node.defaultChild = defaultChild; @@ -485,14 +485,14 @@ describe('defaultChild', () => { expect(root.node.defaultChild).toBe(defaultChild); }); test('returns "undefined" if there is no default', () => { - const root = new Root(); + const root = Construct.createRoot(); new Construct(root, 'child1'); new Construct(root, 'child2'); expect(root.node.defaultChild).toBeUndefined(); }); test('fails if there are both "Resource" and "Default"', () => { - const root = new Root(); + const root = Construct.createRoot(); new Construct(root, 'child1'); new Construct(root, 'Default'); new Construct(root, 'child2'); @@ -507,7 +507,7 @@ describe('dependencies', () => { test('addDependency() defines a dependency between two scopes', () => { // GIVEN - const root = new Root(); + const root = Construct.createRoot(); const consumer = new Construct(root, 'consumer'); const producer1 = new Construct(root, 'producer1'); const producer2 = new Construct(root, 'producer2'); @@ -522,7 +522,7 @@ describe('dependencies', () => { test('are deduplicated', () => { // GIVEN - const root = new Root(); + const root = Construct.createRoot(); const consumer = new Construct(root, 'consumer'); const producer = new Construct(root, 'producer'); @@ -539,7 +539,7 @@ describe('dependencies', () => { test('DependencyGroup can represent a group of disjoined producers', () => { // GIVEN - const root = new Root(); + const root = Construct.createRoot(); const group = new DependencyGroup(new Construct(root, 'producer1'), new Construct(root, 'producer2')); const consumer = new Construct(root, 'consumer'); @@ -553,7 +553,7 @@ describe('dependencies', () => { test('Dependable.implement() can be used to implement IDependable on any object', () => { // GIVEN - const root = new Root(); + const root = Construct.createRoot(); const producer = new Construct(root, 'producer'); const consumer = new Construct(root, 'consumer'); @@ -575,7 +575,7 @@ describe('dependencies', () => { test('dependencyRoots are only resolved when node dependencies are evaluated', () => { // GIVEN - const root = new Root(); + const root = Construct.createRoot(); const c1 = new Construct(root, 'c1'); const c2 = new Construct(root, 'c2'); const c3 = new Construct(root, 'c3'); @@ -594,7 +594,7 @@ describe('dependencies', () => { test('DependencyGroup can also include other IDependables', () => { // GIVEN - const root = new Root(); + const root = Construct.createRoot(); const c1 = new Construct(root, 'c1'); // WHEN @@ -614,7 +614,7 @@ describe('dependencies', () => { test('tryRemoveChild()', () => { // GIVEN - const root = new Root(); + const root = Construct.createRoot(); new Construct(root, 'child1'); new Construct(root, 'child2'); @@ -629,7 +629,7 @@ test('tryRemoveChild()', () => { test('toString()', () => { // GIVEN - const root = new Root(); + const root = Construct.createRoot(); const child = new Construct(root, 'child'); const grand = new Construct(child, 'grand'); @@ -641,7 +641,7 @@ test('toString()', () => { test('Construct.isConstruct returns true for constructs', () => { // GIVEN - const root = new Root(); + const root = Construct.createRoot(); class Subclass extends Construct {}; const subclass = new Subclass(root, 'subclass'); const someRandomObject = {}; @@ -659,7 +659,7 @@ test('Construct.isConstruct returns true for constructs', () => { }); function createTree(context?: any) { - const root = new Root(); + const root = Construct.createRoot(); const highChild = new Construct(root, 'HighChild'); if (context) { Object.keys(context).forEach(key => highChild.node.setContext(key, context[key]));