Skip to content
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
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