Skip to content

Commit

Permalink
Remove mutate and put it on the builder to improve usability an… (#17)
Browse files Browse the repository at this point in the history
* Works first time every time?????

* Version

* Update readme

* Rename

* Update

* Update

* Update readme

* Update readme
  • Loading branch information
develohpanda authored Oct 26, 2019
1 parent 6697ae5 commit d95dc0b
Show file tree
Hide file tree
Showing 5 changed files with 73 additions and 89 deletions.
5 changes: 1 addition & 4 deletions .codacy.yml
Original file line number Diff line number Diff line change
@@ -1,6 +1,3 @@
exclude_paths:
- '.pipelines/**/*'
- '.vscode/**/*'
- 'test/**/*'
- '*.min.js'
- '**/tests/**'
- '**/test/**'
23 changes: 8 additions & 15 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,10 @@

### Generate a fluent, typed object builder for any interface or type.

`fluent-builder` consumes a seeding schema, and generates a `mutator` with a signature identical to the type being built, but with `mutate` functions, to make iterative modifications to your object.
`fluent-builder` consumes a seeding schema, and generates a builder with a signature identical to the type being built, but with `mutate` functions, to make iterative modifications to your object. The builder contains two additional properties, `reset()` and `build()`.

```ts
createBuilder<Person>(schema).mutate(set => set.name('Bob').age(42)).instance();
createBuilder<Product>(schema).name('Shirt').price(42).build();
```

## Why?
Expand Down Expand Up @@ -45,13 +45,11 @@ interface Product {
```ts
import {Schema} from '@develohpanda/fluent-builder';

const buyMock = jest.fn();

const schema: Schema<Product> = {
name: () => 'Shirt',
price: () => 2),
price: () => 2,
color: () => undefined,
buy: () => buyMock,
buy: () => jest.fn(),
}
```

Expand All @@ -68,23 +66,18 @@ describe('suite', () => {
beforeEach(() => builder.reset());

it('test', () => {
builder.mutate(set =>
set
.price(4)
.buy(jest.fn(() => console.log('here lol 1234')))
);

const instance = builder.instance();
const mock = jest.fn();
const instance = builder.price(4).buy(mock).build();

// use instance
// use instance and mock
});
});
```

The overhead of constructing a new builder can be avoided by using the `builder.reset()` method. This resets the mutated schema back to its original, and can be chained.

```ts
builder.reset().mutate(...).instance();
builder.reset().price(5).build();
```

## Contributing
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@develohpanda/fluent-builder",
"version": "1.0.1",
"version": "2.0.0",
"description": "A typed, fluent builder for creating objects in Typescript",
"repository": "https://github.com/develohpanda/fluent-builder",
"author": "Opender Singh <opender94@gmail.com>",
Expand Down
64 changes: 30 additions & 34 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,47 +23,43 @@ type Mutator<T> = {
};

type Mutate<T, K extends keyof T> = IsOptional<T[K]> extends true
? (value?: T[K]) => Mutator<T>
: (value: T[K]) => Mutator<T>;
? (value?: T[K]) => FluentBuilder<T>
: (value: T[K]) => FluentBuilder<T>;

export class FluentBuilder<T extends object> {
private readonly mutator: Mutator<T>;
private readonly schema: Schema<T>;
private internalSchema: InternalSchema<T>;
interface Builder<T> {
reset: () => FluentBuilder<T>;
build: () => T;
}

public constructor(schema: Schema<T>) {
this.schema = schema;
this.internalSchema = {...schema};
const mutator: Partial<Mutator<T>> = {};
export type FluentBuilder<T> = Builder<T> & Mutator<T>;

for (const key in this.internalSchema) {
if (this.internalSchema.hasOwnProperty(key)) {
mutator[key] = ((v: T[typeof key]) => {
this.internalSchema[key] = () => v;
export const createBuilder = <T extends object>(
schema: Schema<T>
): FluentBuilder<T> => {
const internalSchema: InternalSchema<T> = {...schema};
const mutator: Partial<Mutator<T>> = {};

return this.mutator;
}) as Mutate<T, typeof key>;
}
}
for (const key in internalSchema) {
if (internalSchema.hasOwnProperty(key)) {
mutator[key] = ((v: T[typeof key]) => {
internalSchema[key] = () => v;

this.mutator = mutator as Mutator<T>;
return mutator as FluentBuilder<T>;
}) as Mutate<T, typeof key>;
}
}

public mutate = (func: (mutate: Mutator<T>) => void): FluentBuilder<T> => {
func(this.mutator);

return this;
};

public reset = (): FluentBuilder<T> => {
this.internalSchema = {...this.schema};
const builder = mutator as FluentBuilder<T>;
builder.build = () => fromSchema<T>(internalSchema);
builder.reset = () => {
for (const key in schema) {
if (schema.hasOwnProperty(key)) {
internalSchema[key] = schema[key];
}
}

return this;
return builder;
};

public instance = (): T => fromSchema<T>(this.internalSchema);
}

export const createBuilder = <T extends object>(
schema: Schema<T>
): FluentBuilder<T> => new FluentBuilder<T>(schema);
return builder;
};
68 changes: 33 additions & 35 deletions test/FluentBuilder.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,29 +46,29 @@ describe('FluentBuilder', () => {
beforeEach(() => jest.clearAllMocks());

it('should create initial instance from schema', () => {
const instance = createBuilder(schema).instance();
const instance = createBuilder(schema).build();

expect(instance).toEqual(expectedInitial);
});

it('should track complex properties by reference from schema initializer to instance', () => {
const builder = createBuilder(schema);
const before = builder.instance();
const before = builder.build();

expect(before.arr).toBe(arr);
expect(before.obj).toBe(obj);

arr.push(3);
obj.valOpt = 2;

const after = builder.instance();
const after = builder.build();

expect(after.arr).toBe(arr);
expect(after.obj).toBe(obj);
});

it('can track jest function calls on the instance', () => {
const instance = createBuilder(schema).instance();
const instance = createBuilder(schema).build();

expect(instance.func).not.toHaveBeenCalled();

Expand All @@ -79,17 +79,17 @@ describe('FluentBuilder', () => {

it('can track jest function calls between instances', () => {
const builder = createBuilder(schema);
expect(builder.instance().func).not.toHaveBeenCalled();
builder.instance().func();
expect(builder.instance().func).toHaveBeenCalled();
expect(builder.build().func).not.toHaveBeenCalled();
builder.build().func();
expect(builder.build().func).toHaveBeenCalled();
});

it('can track mutated function calls', () => {
const mutatedFunc = jest.fn();

const instance = createBuilder(schema)
.mutate(s => s.func(mutatedFunc))
.instance();
.func(mutatedFunc)
.build();

expect(instance.func).not.toHaveBeenCalled();
mutatedFunc();
Expand All @@ -101,12 +101,13 @@ describe('FluentBuilder', () => {
const builder = createBuilder(schema);

const instance = builder
.mutate(set => set.numOpt(5).str('test'))
.instance();
.numOpt(5)
.str('test')
.build();

expect(instance).not.toEqual(expectedInitial);

const resetInstance = builder.reset().instance();
const resetInstance = builder.reset().build();

expect(resetInstance).toEqual(expectedInitial);
});
Expand All @@ -119,26 +120,25 @@ describe('FluentBuilder', () => {
const func = jest.fn();

const instance = builder
.mutate(set =>
set
.numOpt(numOpt)
.str(str)
.func(func)
)
.instance();
.numOpt(numOpt)
.str(str)
.func(func)
.build();

expect(instance.numOpt).toEqual(numOpt);
expect(instance.str).toEqual(str);
expect(instance.func).toBe(func);

const resetInstance = builder.reset().instance();
const resetInstance = builder.reset().build();
expect(resetInstance).toEqual(expectedInitial);

numOpt = 3;
str = 'test';
const rebuiltInstance = builder
.mutate(set => set.numOpt(numOpt).str(str))
.instance();
.numOpt(numOpt)
.str(str)
.build();

expect(rebuiltInstance.numOpt).toEqual(numOpt);
expect(rebuiltInstance.str).toEqual(str);
expect(rebuiltInstance.func).toBe(expectedInitial.func);
Expand All @@ -148,23 +148,21 @@ describe('FluentBuilder', () => {
it('should define all mutator properties', () => {
const builder = createBuilder(schema);

builder.mutate(set => {
for (const key in set) {
expect((set as any)[key]).toBeDefined();
}
});
for (const key in builder) {
expect((builder as any)[key]).toBeDefined();
}
});

it('should not update a previous instance if the builder is mutated afterards', () => {
const builder = createBuilder(schema);
const before = builder.instance();
const before = builder.build();

expect(before.num).toEqual(num);

const updatedNum = num + 1;
builder.mutate(set => set.num(updatedNum));
builder.num(updatedNum);

const after = builder.instance();
const after = builder.build();

expect(before.num).toEqual(num);
expect(after.num).toEqual(updatedNum);
Expand All @@ -173,19 +171,19 @@ describe('FluentBuilder', () => {
it('can mutate an optional property that was initialized as undefined', () => {
const builder = createBuilder(schema);

expect(builder.instance().numOpt).toBeUndefined();
expect(builder.build().numOpt).toBeUndefined();

const update = 1;
builder.mutate(set => set.numOpt(update));
builder.numOpt(update);

expect(builder.instance().numOpt).toEqual(update);
expect(builder.build().numOpt).toEqual(update);
});

it('should show mutation on instance after mutator function', () => {
const builder = createBuilder(schema);

const str = 'test';
const instance = builder.mutate(set => set.str(str)).instance();
const instance = builder.str(str).build();

expect(instance.str).toEqual(str);
});
Expand All @@ -195,7 +193,7 @@ describe('FluentBuilder', () => {
(input: any) => {
const builder = createBuilder(schema);

const instance = builder.mutate(set => set.numOpt(input)).instance();
const instance = builder.numOpt(input).build();

expect(instance.numOpt).toEqual(input);
}
Expand Down

0 comments on commit d95dc0b

Please sign in to comment.