From b1c2ee9056f0e622f91947a41e4a6543ce1ea692 Mon Sep 17 00:00:00 2001 From: Zaytsev Kirill Date: Fri, 28 May 2021 21:14:09 +0300 Subject: [PATCH] chore: clone logic refactor preparations & typehinting --- src/FormularioField.vue | 26 +++++-------------- src/types.ts | 42 ++++++++++++++++++++++++++++++ src/utils/clone.ts | 6 ++++- src/utils/index.ts | 1 - src/utils/types.ts | 19 -------------- test/unit/types.test.js | 49 +++++++++++++++++++++++++++++++++++ test/unit/utils/clone.test.js | 49 +++++++++++++++++++++++++++++------ test/unit/utils/types.test.js | 14 ---------- 8 files changed, 144 insertions(+), 62 deletions(-) delete mode 100644 src/utils/types.ts create mode 100644 test/unit/types.test.js delete mode 100644 test/unit/utils/types.test.js diff --git a/src/FormularioField.vue b/src/FormularioField.vue index 4cff96b..9b7e178 100644 --- a/src/FormularioField.vue +++ b/src/FormularioField.vue @@ -22,31 +22,19 @@ import { Violation, } from '@/validation/validator' +import { + FormularioFieldContext, + FormularioFieldModelGetConverter as ModelGetConverter, + FormularioFieldModelSetConverter as ModelSetConverter, + Empty, +} from '@/types' + const VALIDATION_BEHAVIOR = { DEMAND: 'demand', LIVE: 'live', SUBMIT: 'submit', } -type FormularioFieldContext = { - model: U; - name: string; - runValidation(): Promise; - violations: Violation[]; - errors: string[]; - allErrors: string[]; -} - -interface ModelGetConverter { - (value: U|Empty): U|T|Empty; -} - -interface ModelSetConverter { - (curr: U|T, prev: U|Empty): U|T; -} - -type Empty = null | undefined - @Component({ name: 'FormularioField', inheritAttrs: false }) export default class FormularioField extends Vue { @Inject({ default: '' }) __Formulario_path!: string diff --git a/src/types.ts b/src/types.ts index 9cad847..873fe67 100644 --- a/src/types.ts +++ b/src/types.ts @@ -8,3 +8,45 @@ export interface FormularioFieldInterface { runValidation(): Promise; resetValidation(): void; } + +export type FormularioFieldContext = { + model: T; + name: string; + runValidation(): Promise; + violations: Violation[]; + errors: string[]; + allErrors: string[]; +} + +export interface FormularioFieldModelGetConverter { + (value: U|Empty): U|T|Empty; +} + +export interface FormularioFieldModelSetConverter { + (curr: U|T, prev: U|Empty): U|T; +} + +export type Empty = undefined | null + +export type RecordKey = string | number +export type RecordLike = T[] | Record + +export type Scalar = boolean | number | string | symbol | Empty + +export function isRecordLike (value: unknown): boolean { + return typeof value === 'object' && value !== null && ['Array', 'Object'].includes(value.constructor.name) +} + +export function isScalar (value: unknown): boolean { + switch (typeof value) { + case 'bigint': + case 'boolean': + case 'number': + case 'string': + case 'symbol': + case 'undefined': + return true + default: + return value === null + } +} diff --git a/src/utils/clone.ts b/src/utils/clone.ts index a1ae7a7..a66afca 100644 --- a/src/utils/clone.ts +++ b/src/utils/clone.ts @@ -1,8 +1,12 @@ import has from '@/utils/has' -import { RecordLike, Scalar, isScalar } from '@/utils/types' +import { RecordLike, Scalar, isScalar } from '@/types' type Cloneable = Scalar|Date|RecordLike +export const cloneInstance = (original: T): T => { + return Object.assign(Object.create(Object.getPrototypeOf(original)), original) +} + /** * A simple (somewhat non-comprehensive) clone function, valid for our use * case of needing to unbind reactive watchers. diff --git a/src/utils/index.ts b/src/utils/index.ts index 0c48322..120562d 100644 --- a/src/utils/index.ts +++ b/src/utils/index.ts @@ -1,6 +1,5 @@ export { default as clone } from './clone' export { default as has } from './has' -export { isScalar } from './types' export { default as merge } from './merge' export { default as regexForFormat } from './regexForFormat' export { default as shallowEquals } from './shallowEquals' diff --git a/src/utils/types.ts b/src/utils/types.ts deleted file mode 100644 index 08a70ff..0000000 --- a/src/utils/types.ts +++ /dev/null @@ -1,19 +0,0 @@ -export type Empty = undefined | null - -export type RecordKey = string | number -export type RecordLike = T[] | Record - -export type Scalar = boolean | number | string | symbol | Empty - -export function isScalar (value: unknown): boolean { - switch (typeof value) { - case 'boolean': - case 'number': - case 'string': - case 'symbol': - case 'undefined': - return true - default: - return value === null - } -} diff --git a/test/unit/types.test.js b/test/unit/types.test.js new file mode 100644 index 0000000..d7d5eae --- /dev/null +++ b/test/unit/types.test.js @@ -0,0 +1,49 @@ +import { + isRecordLike, + isScalar, +} from '@/types' + +describe('types', () => { + const scalars = [ + ['booleans', false], + ['numbers', 123], + ['strings', 'hello'], + ['symbols', Symbol(123)], + ['undefined', undefined], + ['null', null], + ] + + const records = [ + [{}], + [{ a: 'a', b: ['b'] }], + [[]], + [['b', 'c']], + ] + + describe('isRecordLike', () => { + test.each(records)('passes on records', record => { + expect(isRecordLike(record)).toBe(true) + }) + + test.each(scalars)('fails on $type', (type, scalar) => { + expect(isRecordLike(scalar)).toBe(false) + }) + + test.each([ + ['class instance', new class {} ()], + ['builtin Date instance', new Date()], + ])('fails on $type', (type, instance) => { + expect(isRecordLike(instance)).toBe(false) + }) + }) + + describe('isScalar', () => { + test.each(scalars)('passes on $type', (type, scalar) => { + expect(isScalar(scalar)).toBe(true) + }) + + test.each(records)('fails on records & arrays', record => { + expect(isScalar(record)).toBe(false) + }) + }) +}) diff --git a/test/unit/utils/clone.test.js b/test/unit/utils/clone.test.js index 541ac25..e37567c 100644 --- a/test/unit/utils/clone.test.js +++ b/test/unit/utils/clone.test.js @@ -1,28 +1,61 @@ -import clone from '@/utils/clone' +import clone, { cloneInstance } from '@/utils/clone' describe('clone', () => { - it('Basic objects stay the same', () => { + test('Basic objects stay the same', () => { const obj = { a: 123, b: 'hello' } expect(clone(obj)).toEqual(obj) }) - it('Basic nested objects stay the same', () => { + test('Basic nested objects stay the same', () => { const obj = { a: 123, b: { c: 'hello-world' } } expect(clone(obj)).toEqual(obj) }) - it('Simple pojo reference types are re-created', () => { + test('Simple pojo reference types are re-created', () => { const c = { c: 'hello-world' } expect(clone({ a: 123, b: c }).b === c).toBe(false) }) - it('Retains array structures inside of a pojo', () => { - const obj = { a: 'abcd', d: ['first', 'second'] } + test('Retains array structures inside of a pojo', () => { + const obj = { a: 'abc', d: ['first', 'second'] } expect(Array.isArray(clone(obj).d)).toBe(true) }) - it('Removes references inside array structures', () => { - const obj = { a: 'abcd', d: ['first', { foo: 'bar' }] } + test('Removes references inside array structures', () => { + const obj = { a: 'abc', d: ['first', { foo: 'bar' }] } expect(clone(obj).d[1] === obj.d[1]).toBe(false) }) }) + +describe('cloneInstance', () => { + test('creates a copy of a class instance', () => { + class Sample { + constructor() { + this.fieldA = 'fieldA' + this.fieldB = 'fieldB' + } + + doSomething () {} + } + + const sample = new Sample() + const copy = cloneInstance(sample) + + expect(sample === copy).toBeFalsy() + + expect(copy).toBeInstanceOf(Sample) + expect(copy.fieldA).toEqual('fieldA') + expect(copy.fieldB).toEqual('fieldB') + expect(copy.doSomething).toBeTruthy() + expect(copy.doSomething).not.toThrow() + }) + + test('creates a broken copy of builtins', () => { + const sample = new Date() + const copy = cloneInstance(sample) + + expect(sample === copy).toBeFalsy() + expect(copy).toBeInstanceOf(Date) + expect(() => copy.toISOString()).toThrow() + }) +}) diff --git a/test/unit/utils/types.test.js b/test/unit/utils/types.test.js deleted file mode 100644 index 8d4d702..0000000 --- a/test/unit/utils/types.test.js +++ /dev/null @@ -1,14 +0,0 @@ -import { isScalar } from '@/utils/types' - -describe('isScalar', () => { - const expectIsScalar = value => expect(isScalar(value)).toBe(true) - - test('passes on booleans', () => expectIsScalar(false)) - test('passes on numbers', () => expectIsScalar(123)) - test('passes on strings', () => expectIsScalar('hello')) - test('passes on symbols', () => expectIsScalar(Symbol(123))) - test('passes on undefined', () => expectIsScalar(undefined)) - test('passes on null', () => expectIsScalar(null)) - - test('fails on pojo', () => expect(isScalar({})).toBe(false)) -})