Skip to content

Commit

Permalink
feat: 🎸 add Builder Pattern to Shape/Animation/Slide
Browse files Browse the repository at this point in the history
With this commit I'm introducing new features in the kittik-slide
package that allows you to build animations via AnimationBuilder, shapes
via ShapeBuilder and slides via SlideBuilder. These builders provide you
typed API that checks everything you passed as their arguments, so there
is a lesser room for some errors.
  • Loading branch information
ghaiklor committed Mar 28, 2020
1 parent cf3917a commit d30c965
Show file tree
Hide file tree
Showing 8 changed files with 444 additions and 23 deletions.
48 changes: 48 additions & 0 deletions src/kittik-slide/spec/AnimationBuilder.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import { AnimationBuilder } from '../src/animation/AnimationBuilder';

describe('AnimationBuilder', () => {
it('Should properly create animation from the builder', () => {
const animation = AnimationBuilder
.start('Focus')
.withType('Focus')
.withDuration(2000)
.withEasing('inBack')
.end();

expect(animation.toObject()).toEqual({
type: 'Focus',
options: {
direction: 'shakeX',
duration: 2000,
easing: 'inBack',
offset: 5,
repeat: 1
}
});
});

it('Should properly build animation using withOptions()', () => {
const animation = AnimationBuilder
.start('Print')
.withOptions({ duration: 5000 })
.end();

expect(animation.toObject()).toEqual({
type: 'Print',
options: {
duration: 5000,
easing: 'outQuad'
}
});
});

it('Should properly throw an error if animation is absent', () => {
expect(() => {
// This is a specific case where I check if someone tries to build not existing animation
// Though, this case was covered by types, so I need to disable it to write the test
// eslint-disable-next-line @typescript-eslint/ban-ts-ignore
// @ts-ignore
AnimationBuilder.start('Nonsense').end();
}).toThrowError('Animation "Nonsense" you tried to build does not exist');
});
});
62 changes: 62 additions & 0 deletions src/kittik-slide/spec/ShapeBuilder.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
import { ShapeBuilder } from '../src/shape/ShapeBuilder';
import { Canvas } from 'terminal-canvas';

describe('ShapeBuilder', () => {
it('Should properly build the shape via builder', () => {
const shape = ShapeBuilder
.start('Rectangle')
.withCursor(Canvas.create())
.withType('Rectangle')
.withText('Hello, World')
.withX('10%')
.withY('10%')
.withWidth('60%')
.withHeight('40%')
.withBackground('white')
.withForeground('black')
.end();

expect(shape.toObject()).toEqual({
type: 'Rectangle',
options: {
background: 'white',
foreground: 'black',
height: '40%',
text: 'Hello, World',
width: '60%',
x: '10%',
y: '10%'
}
});
});

it('Should properly build the shape via withOptions()', () => {
const shape = ShapeBuilder
.start('Rectangle')
.withOptions({ text: 'Hello, World' })
.end();

expect(shape.toObject()).toEqual({
type: 'Rectangle',
options: {
background: 'none',
foreground: 'none',
height: '25%',
text: 'Hello, World',
width: '50%',
x: 'left',
y: 'top'
}
});
});

it('Should properly throw an error if shape is absent', () => {
expect(() => {
// This is a corner case where I check if builder throws an error when providing wrong type
// Since this case has been covered by type system of TypeScript, I need to disable it
// eslint-disable-next-line @typescript-eslint/ban-ts-ignore
// @ts-ignore
ShapeBuilder.start('Nonsense').end();
}).toThrowError('Shape "Nonsense" you tried to build does not exist');
});
});
51 changes: 50 additions & 1 deletion src/kittik-slide/spec/Slide.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@ import { OrderDeclaration } from '../src/slide/OrderDeclaration';
import { ShapeDeclaration } from '../src/shape/ShapeDeclaration';
import { Slide } from '../src/slide/Slide';
import { SlideDeclaration } from '../src/slide/SlideDeclaration';
import { TextObject } from 'kittik-shape-text';
import { TextObject, Text } from 'kittik-shape-text';
import { Print } from 'kittik-animation-print';

const SLIDE_DECLARATION: SlideDeclaration = {
shapes: [{
Expand Down Expand Up @@ -152,4 +153,52 @@ describe('Core::Slide', () => {

await expect(slide.render()).resolves.toBeUndefined();
});

it('Should properly instantiate an empty slide instance when no declaration nor cursor is passed', () => {
const slide = new Slide();

expect(slide.cursor).toBeInstanceOf(Canvas);
expect(slide.shapes.size).toBe(0);
expect(slide.animations.size).toBe(0);
expect(slide.order.length).toBe(0);
});

it('Should properly instantiate an empty slide instance when nothing is passed but an empty arrays', () => {
const slide = new Slide(undefined, { order: [], shapes: [] });

expect(slide.cursor).toBeInstanceOf(Canvas);
expect(slide.shapes.size).toBe(0);
expect(slide.animations.size).toBe(0);
expect(slide.order.length).toBe(0);
});

it('Should properly throw an error when trying to add shape that is already added', () => {
const slide = new Slide(undefined, { shapes: [{ name: 'Test', type: 'Text' }], order: [] });

expect(() => slide.addShape('Test', Text.create(new Canvas()))).toThrowError('Shape "Test" already exists in slide');
});

it('Should properly throw an error when trying to add animation that is already added', () => {
const slide = new Slide(undefined, { shapes: [], order: [], animations: [{ name: 'Test', type: 'Print' }] });

expect(() => slide.addAnimation('Test', Print.create())).toThrowError('Animation "Test" already exists in slide');
});

it('Should properly throw an error when trying to add ordering for the shape that is already added', () => {
const slide = new Slide(undefined, { shapes: [], order: [{ shape: 'Test' }] });

expect(() => slide.addOrder('Test')).toThrowError(
'You already have an ordering for "Test"\n' +
'Seems like it was defined somewhere before'
);
});

it('Should properly add order to the slide', () => {
const slide = new Slide();

slide.addOrder('Test');

expect(slide.order.length).toBe(1);
expect(slide.order[0]).toEqual({ shape: 'Test' });
});
});
62 changes: 62 additions & 0 deletions src/kittik-slide/spec/SlideBuilder.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
import { SlideBuilder } from '../src/slide/SlideBuilder';
import { ShapeBuilder, AnimationBuilder } from '../src/slide/Slide';

describe('SlideBuilder', () => {
it('Should properly instantiate slide via builder', () => {
const slide = SlideBuilder
.start()
.withShape(
'Hello, World',
ShapeBuilder
.start('Text')
.withText('Hello, World')
.withBackground('white')
.withForeground('black')
.end()
)
.withAnimation(
'Print',
AnimationBuilder
.start('Print')
.withDuration(2000)
.end()
)
.withOrder('Hello, World', ['Print'])
.end();

expect(slide.shapes.size).toBe(1);
expect(slide.shapes.get('Hello, World')?.toObject()).toEqual({
type: 'Text',
options: {
align: 'center',
background: 'white',
blink: false,
bold: false,
dim: false,
foreground: 'black',
height: '25%',
hidden: false,
reverse: false,
text: 'Hello, World',
underlined: false,
width: '50%',
x: 'left',
y: 'top'
}
});

expect(slide.animations.size).toBe(1);
expect(slide.animations.get('Print')?.toObject()).toEqual({
type: 'Print',
options: {
duration: 2000,
easing: 'outQuad'
}
});

expect(slide.order).toEqual([{
shape: 'Hello, World',
animations: ['Print']
}]);
});
});
48 changes: 48 additions & 0 deletions src/kittik-slide/src/animation/AnimationBuilder.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import { AnimationObject, AnimationOptions, Animationable, Easing } from 'kittik-animation-basic';
import { AnimationType, ANIMATIONS } from './Animations';

export class AnimationBuilder implements AnimationObject {
type: AnimationType;
options?: Partial<AnimationOptions>;

constructor (type: AnimationType) {
this.type = type;
}

withType (type: AnimationType): this {
this.type = type;

return this;
}

withOptions (options: Partial<AnimationOptions>): this {
this.options = { ...this.options, ...options };

return this;
}

withDuration (duration: number): this {
this.options = { ...this.options, duration };

return this;
}

withEasing (easing: Easing): this {
this.options = { ...this.options, easing };

return this;
}

end (): Animationable {
const ctr = ANIMATIONS.get(this.type);
if (ctr === undefined) {
throw new Error(`Animation "${this.type}" you tried to build does not exist`);
}

return ctr.fromObject(this);
}

static start (type: AnimationType): AnimationBuilder {
return new this(type);
}
}
86 changes: 86 additions & 0 deletions src/kittik-slide/src/shape/ShapeBuilder.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
import { ShapeObject, ShapeOptions, ShapeRenderable } from 'kittik-shape-basic';
import { ShapeType, SHAPES } from './Shapes';
import { Canvas } from 'terminal-canvas';

export class ShapeBuilder implements ShapeObject {
cursor: Canvas = Canvas.create();
type: ShapeType;
options?: Partial<ShapeOptions>;

constructor (type: ShapeType) {
this.type = type;
}

withCursor (cursor: Canvas): this {
this.cursor = cursor;

return this;
}

withType (type: ShapeType): this {
this.type = type;

return this;
}

withOptions (options: Partial<ShapeOptions>): this {
this.options = { ...this.options, ...options };

return this;
}

withText (text: string): this {
this.options = { ...this.options, text };

return this;
}

withX (x: string): this {
this.options = { ...this.options, x };

return this;
}

withY (y: string): this {
this.options = { ...this.options, y };

return this;
}

withWidth (width: string): this {
this.options = { ...this.options, width };

return this;
}

withHeight (height: string): this {
this.options = { ...this.options, height };

return this;
}

withBackground (background: string): this {
this.options = { ...this.options, background };

return this;
}

withForeground (foreground: string): this {
this.options = { ...this.options, foreground };

return this;
}

end (): ShapeRenderable {
const ctr = SHAPES.get(this.type);
if (ctr === undefined) {
throw new Error(`Shape "${this.type}" you tried to build does not exist`);
}

return ctr.fromObject(this, this.cursor);
}

static start (type: ShapeType): ShapeBuilder {
return new this(type);
}
}
Loading

0 comments on commit d30c965

Please sign in to comment.