Home Reference Source Test

spec/serialisation.spec.ts

import 'mocha';

import { given } from 'mocha-testdata';

import { JSONObject, TinyType, TinyTypeOf } from '../src';
import { expect } from './expect';

describe('Serialisation', () => {

    describe('of single-value TinyTypes', () => {

        class Amount extends TinyTypeOf<number>() {}
        class Name extends TinyTypeOf<string>() {}
        class Vote extends TinyTypeOf<boolean>() {}
        class Maybe extends TinyType {
            constructor(public readonly value: any) {
                super();
            }
        }

        given([
            { obj: new Amount(0),        expectedType: 'number'    },
            { obj: new Amount(10/3),     expectedType: 'number'    },
            { obj: new Name('Jan'),      expectedType: 'string'    },
            { obj: new Vote(true),       expectedType: 'boolean'   },
            { obj: new Vote(false),      expectedType: 'boolean'   },
            { obj: new Maybe(undefined), expectedType: 'undefined' },
        ]).
        it('preserves the type of the wrapped value', ({ obj, expectedType: expectedType }) => {
            expect(obj.toJSON()).to.be.an(expectedType);
        });
    });

    describe('of TinyTypes wrapping several primitive values', () => {

        class Person extends TinyType {
            public role = 'dev';

            static fromJSON(o: JSONObject): Person {
                return new Person(o.firstName as string, o.lastName as string, o.age as number);
            }

            constructor(public readonly firstName: string, public readonly lastName: string, public readonly age: number) {
                super();
            }

            speak() {
                return `Hi, I'm ${this.firstName} ${this.lastName}`;
            }
        }

        it('uses only the significant fields and retains their type', () => {
            const p = new Person('John', 'Smith', 42);
            const serialised = p.toJSON() as JSONObject;

            expect(Object.keys(serialised)).to.include.ordered.members(['age', 'firstName', 'lastName', 'role']);
            expect(Object.keys(serialised)).to.not.include.members(['speak', 'toJSON', 'toString']);

            expect(serialised.age).to.be.a('number');
            expect(serialised.firstName).to.be.a('string');
            expect(serialised.lastName).to.be.a('string');
            expect(serialised.role).to.be.a('string');
        });
    });

    describe('of nested Tiny Types', () => {
        class FirstName extends TinyTypeOf<string>() {
            static fromJSON = (v: string) => new FirstName(v);
        }
        class LastName  extends TinyTypeOf<string>() {
            static fromJSON = (v: string) => new LastName(v);
        }
        class Age extends TinyTypeOf<number>() {
            static fromJSON = (v: number) => new Age(v);
        }

        class AnotherPerson extends TinyType {
            public role = 'dev';

            static fromJSON(o: JSONObject): AnotherPerson {
                return new AnotherPerson(
                    FirstName.fromJSON(o.firstName as string),
                    LastName.fromJSON(o.lastName as string),
                    Age.fromJSON(o.age as number),
                );
            }

            constructor(public readonly firstName: FirstName,
                public readonly lastName: LastName,
                public readonly age: Age,
            ) {
                super();
            }

            speak() {
                return `Hi, I'm ${this.firstName} ${this.lastName}`;
            }
        }

        it('uses only the significant fields and retains the type of their respective values', () => {
            const p = new AnotherPerson(new FirstName('John'), new LastName('Smith'), new Age(42));
            const serialised = p.toJSON() as JSONObject;

            expect(Object.keys(serialised)).to.include.ordered.members(['age', 'firstName', 'lastName', 'role']);
            expect(Object.keys(serialised)).to.not.include.members(['speak', 'toJSON', 'toString']);

            expect(serialised.age).to.be.a('number');
            expect(serialised.firstName).to.be.a('string');
            expect(serialised.lastName).to.be.a('string');
            expect(serialised.role).to.be.a('string');
        });
    });

    it('works both ways', () => {
        class EmployeeId extends TinyTypeOf<number>() {
            static fromJSON = (id: number) => new EmployeeId(id);
        }

        class DepartmentId extends TinyTypeOf<string>() {
            static fromJSON = (id: string) => new DepartmentId(id);
        }

        class Allocation extends TinyType {
            static fromJSON = (o: JSONObject) => new Allocation(
                EmployeeId.fromJSON(o.employeeId as number),
                DepartmentId.fromJSON(o.departmentId as string),
            )

            constructor(public readonly employeeId: EmployeeId, public readonly departmentId: DepartmentId) {
                super();
            }
        }

        const allocation = new Allocation(new EmployeeId(1), new DepartmentId('engineering'));

        const deserialised = Allocation.fromJSON({ departmentId: 'engineering', employeeId: 1 });

        expect(deserialised.equals(allocation)).to.be.true;                 // tslint:disable-line:no-unused-expression
    });
});