1
0
mirror of synced 2024-11-22 05:16:05 +03:00

chore: clone logic refactor preparations & typehinting

This commit is contained in:
Zaytsev Kirill 2021-05-28 21:14:09 +03:00
parent 320df96b96
commit b1c2ee9056
8 changed files with 144 additions and 62 deletions

View File

@ -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<U> = {
model: U;
name: string;
runValidation(): Promise<Violation[]>;
violations: Violation[];
errors: string[];
allErrors: string[];
}
interface ModelGetConverter {
<U, T>(value: U|Empty): U|T|Empty;
}
interface ModelSetConverter {
<T, U>(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

View File

@ -8,3 +8,45 @@ export interface FormularioFieldInterface {
runValidation(): Promise<Violation[]>;
resetValidation(): void;
}
export type FormularioFieldContext<T> = {
model: T;
name: string;
runValidation(): Promise<Violation[]>;
violations: Violation[];
errors: string[];
allErrors: string[];
}
export interface FormularioFieldModelGetConverter {
<U, T>(value: U|Empty): U|T|Empty;
}
export interface FormularioFieldModelSetConverter {
<T, U>(curr: U|T, prev: U|Empty): U|T;
}
export type Empty = undefined | null
export type RecordKey = string | number
export type RecordLike<T> = T[] | Record<RecordKey, T>
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
}
}

View File

@ -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<unknown>
export const cloneInstance = <T>(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.

View File

@ -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'

View File

@ -1,19 +0,0 @@
export type Empty = undefined | null
export type RecordKey = string | number
export type RecordLike<T> = T[] | Record<RecordKey, T>
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
}
}

49
test/unit/types.test.js Normal file
View File

@ -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)
})
})
})

View File

@ -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()
})
})

View File

@ -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))
})