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

feat: new methods replace(), map() and updateEvery() methods to make changes using callback functions #158

Merged
merged 2 commits into from
Oct 5, 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
75 changes: 75 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,81 @@ new ProjenStruct(project, { name: 'MyProjectOptions'})
});
```

### Updating multiple properties

A callback function can be passed to `updateEvery()` to update multiple properties at a time.

Use `updateAll()` to uniformly update all properties.
A convenience `allOptional()` method is provided to make all properties optional.

```js
new ProjenStruct(project, { name: 'MyProjectOptions'})
.mixin(Struct.fromFqn('projen.typescript.TypeScriptProjectOptions'))

// Use a callback to make conditional updates
.updateEvery((property) => {
if (!property.optional) {
return {
docs: {
remarks: 'This property is required.',
},
};
}
return {};
})

// Apply an update to all properties
.updateAll({
immutable: true,
})

// Mark all properties as optional
.allOptional();
```

### Replacing properties

Existing properties can be replaced with a new `@jsii/spec` definition.
If a different `name` is provided, the property is also renamed.

A callback function can be passed to `map()` to map every property to a new `@jsii/spec` definition.

```ts
new ProjenStruct(project, { name: 'MyProjectOptions' })
.mixin(Struct.fromFqn('projen.typescript.TypeScriptProjectOptions'))

// Replace a property with an entirely new definition
.replace('autoApproveOptions', {
name: 'autoApproveOptions',
type: { fqn: 'my_project.AutoApproveOptions' },
docs: {
summary: 'Configure the auto-approval workflow.'
}
})

// Passing a new name, will also rename the property
.replace('autoMergeOptions', {
name: 'mergeFlowOptions',
type: { fqn: 'my_project.MergeFlowOptions' },
})

// Use a callback to map every property to a new definition
.map((property) => {
if (property.protected) {
return {
...property,
protected: false,
docs: {
custom: {
'internal': 'true',
}
}
}
}
return property;
});
```

### Filter properties

Arbitrary predicate functions can be used to filter properties.
Expand Down
154 changes: 104 additions & 50 deletions src/builder/struct.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,48 +34,50 @@ export interface HasStructSpec {

export interface IStructBuilder {
/**
* Keep only the properties that meet the condition specified in the callback function.
* Add properties.
*
* In the same call, the first defined properties take priority.
* However later calls will overwrite existing properties.
*/
filter(predicate: (prop: Property) => boolean): IStructBuilder;
add(...props: Property[]): IStructBuilder;

/**
* Only keep these properties.
* Mix the properties of these sources into the struct.
*
* In the same call, the first defined sources and properties take priority.
* However later calls will overwrite existing properties.
*/
only(...keep: string[]): IStructBuilder;
mixin(...sources: HasProperties[]): IStructBuilder;

/**
* Omit these properties.
* Replaces an existing property with a new spec.
*/
omit(...remove: string[]): IStructBuilder;
replace(name: string, replacement: Property): IStructBuilder;

/**
* Mark all properties as optional.
* Calls a defined callback function on each property, and replaces the property with the returned property.
*
* @param callbackfn — A function that accepts a property spec as an argument. The map method calls the callbackfn function one time for each property.
*/
allOptional(): IStructBuilder;
map(callbackfn: (prop: Property) => Property): IStructBuilder;

/**
* Remove all deprecated properties.
* Update an existing property.
*/
withoutDeprecated(): IStructBuilder;
update(name: string, update: Partial<Property>): IStructBuilder;

/**
* Add properties.
* Calls a defined callback function on each property, and merges the property with the returned property partial.
*
* In the same call, the first defined properties take priority.
* However later calls will overwrite existing properties.
* @param callbackfn — A function that accepts a property spec as an argument. The map method calls the callbackfn function one time for each property.
*/
add(...props: Property[]): IStructBuilder;
updateEvery(callbackfn: (prop: Property) => Partial<Property>): IStructBuilder;

/**
* Update all existing properties.
*/
updateAll(update: Partial<Property>): IStructBuilder;

/**
* Update an existing property.
*/
update(name: string, update: Partial<Property>): IStructBuilder;

/**
* Rename a property.
*
Expand All @@ -84,12 +86,29 @@ export interface IStructBuilder {
rename(from: string, to: string): IStructBuilder;

/**
* Mix the properties of these sources into the struct.
*
* In the same call, the first defined sources and properties take priority.
* However later calls will overwrite existing properties.
* Mark all properties as optional.
*/
mixin(...sources: HasProperties[]): IStructBuilder;
allOptional(): IStructBuilder;

/**
* Keep only the properties that meet the condition specified in the callback function.
*/
filter(predicate: (prop: Property) => boolean): IStructBuilder;

/**
* Only keep these properties.
*/
only(...keep: string[]): IStructBuilder;

/**
* Omit these properties.
*/
omit(...remove: string[]): IStructBuilder;

/**
* Remove all deprecated properties.
*/
withoutDeprecated(): IStructBuilder;
}

/**
Expand Down Expand Up @@ -138,43 +157,41 @@ export class Struct implements IStructBuilder, HasProperties, HasFullyQualifiedN
);
}

public filter(predicate: (prop: Property) => boolean): this {
for (const propertyKey of this._properties.keys()) {
if (!predicate(this._properties.get(propertyKey)!)) {
this._properties.delete(propertyKey);
}
public add(...props: Property[]): this {
for (const prop of props.reverse()) {
this._properties.set(prop.name, prop);
}

return this;
}

public only(...keep: string[]): this {
return this.filter((prop) => keep.includes(prop.name));
}

public omit(...remove: string[]): this {
for (const prop of remove) {
this._properties.delete(prop);
public mixin(...sources: HasProperties[]): this {
for (const source of sources.reverse()) {
this.add(...(source.properties || []));
}

return this;
}

public withoutDeprecated(): this {
return this.filter((prop) => null == prop.docs?.deprecated);
}
public replace(name: string, replacement: Property): this {
const current = this._properties.get(name);

public allOptional(): this {
this._properties.forEach((property) => {
property.optional = true;
});
if (!current) {
throw `Unable to replace property '${name}' in '${this._base.fqn}: Property does not exists, please use \`add\`.'`;
}

return this;
if (replacement.name !== name) {
this.omit(name);
}

return this.add(replacement);
}

public add(...props: Property[]): this {
for (const prop of props.reverse()) {
this._properties.set(prop.name, prop);
public map(callbackfn: (prop: Property) => Property): this {
const keys = this._properties.keys();
for (const propertyKey of keys) {
const current = structuredClone(this._properties.get(propertyKey)!);
this.replace(propertyKey, callbackfn(current));
}

return this;
Expand Down Expand Up @@ -207,6 +224,16 @@ export class Struct implements IStructBuilder, HasProperties, HasFullyQualifiedN
return this.add(updatedProp);
}

public updateEvery(callbackfn: (prop: Property) => Partial<Property>): this {
const keys = this._properties.keys();
for (const propertyKey of keys) {
const current = structuredClone(this._properties.get(propertyKey)!);
this.update(current.name, callbackfn(current) ?? {});
}

return this;
}

public updateAll(update: Partial<Property>): this {
for (const propertyKey of this._properties.keys()) {
this.update(propertyKey, update);
Expand All @@ -218,14 +245,41 @@ export class Struct implements IStructBuilder, HasProperties, HasFullyQualifiedN
return this.update(from, { name: to });
}

public mixin(...sources: HasProperties[]): this {
for (const source of sources.reverse()) {
this.add(...(source.properties || []));
public allOptional(): this {
this.map((property) => {
property.optional = true;
return property;
});

return this;
}

public filter(predicate: (prop: Property) => boolean): this {
for (const propertyKey of this._properties.keys()) {
if (!predicate(this._properties.get(propertyKey)!)) {
this._properties.delete(propertyKey);
}
}

return this;
}

public only(...keep: string[]): this {
return this.filter((prop) => keep.includes(prop.name));
}

public omit(...remove: string[]): this {
for (const prop of remove) {
this._properties.delete(prop);
}

return this;
}

public withoutDeprecated(): this {
return this.filter((prop) => null == prop.docs?.deprecated);
}

/**
* Get the current state of the builder
*/
Expand Down
48 changes: 30 additions & 18 deletions src/projen/projen-struct.ts
Original file line number Diff line number Diff line change
Expand Up @@ -96,34 +96,30 @@ export class ProjenStruct extends Component implements IStructBuilder, HasProper
public get spec() {
return this.builder.spec;
}
filter(predicate: (prop: Property) => boolean): this {
this.builder.filter(predicate);
return this;
}
only(...keep: string[]): this {
this.builder.only(...keep);
return this;
}
omit(...remove: string[]): this {
this.builder.omit(...remove);
add(...props: Property[]): this {
this.builder.add(...props);
return this;
}
withoutDeprecated(): this {
this.builder.withoutDeprecated();
mixin(...sources: HasProperties[]): this {
this.builder.mixin(...sources);
return this;
}
allOptional(): this {
this.builder.allOptional();
replace(name: string, replacement: Property): IStructBuilder {
this.builder.replace(name, replacement);
return this;
}
add(...props: Property[]): this {
this.builder.add(...props);
map(callbackfn: (prop: Property) => Property): this {
this.builder.map(callbackfn);
return this;
}
update(name: string, update: Partial<Property>): this {
this.builder.update(name, update);
return this;
}
updateEvery(callbackfn: (prop: Property) => Partial<Property>): this {
this.builder.updateEvery(callbackfn);
return this;
}
updateAll(update: Partial<Property>): this {
this.builder.updateAll(update);
return this;
Expand All @@ -132,8 +128,24 @@ export class ProjenStruct extends Component implements IStructBuilder, HasProper
this.builder.rename(from, to);
return this;
}
mixin(...sources: HasProperties[]): this {
this.builder.mixin(...sources);
allOptional(): this {
this.builder.allOptional();
return this;
}
filter(predicate: (prop: Property) => boolean): this {
this.builder.filter(predicate);
return this;
}
only(...keep: string[]): this {
this.builder.only(...keep);
return this;
}
omit(...remove: string[]): this {
this.builder.omit(...remove);
return this;
}
withoutDeprecated(): this {
this.builder.withoutDeprecated();
return this;
}
public get properties(): Property[] {
Expand Down
Loading
Loading