refactor: Moved utils to separated files, code cleanup
This commit is contained in:
parent
e814edf9fc
commit
3f5735299d
@ -5,8 +5,8 @@ import messages from '@/validation/messages'
|
||||
import merge from '@/utils/merge'
|
||||
|
||||
import FormularioForm from '@/FormularioForm.vue'
|
||||
import FormularioInput from '@/FormularioInput.vue'
|
||||
import FormularioGrouping from '@/FormularioGrouping.vue'
|
||||
import FormularioInput from '@/FormularioInput.vue'
|
||||
|
||||
import {
|
||||
ValidationContext,
|
||||
|
@ -6,15 +6,8 @@
|
||||
|
||||
<script lang="ts">
|
||||
import Vue from 'vue'
|
||||
import {
|
||||
Component,
|
||||
Model,
|
||||
Prop,
|
||||
Provide,
|
||||
Watch,
|
||||
} from 'vue-property-decorator'
|
||||
import { cloneDeep, getNested, has, setNested, shallowEqualObjects } from '@/libs/utils'
|
||||
import merge from '@/utils/merge'
|
||||
import { Component, Model, Prop, Provide, Watch } from 'vue-property-decorator'
|
||||
import { clone, getNested, has, merge, setNested, shallowEqualObjects } from '@/utils'
|
||||
import Registry from '@/form/registry'
|
||||
import FormularioInput from '@/FormularioInput.vue'
|
||||
|
||||
@ -41,13 +34,22 @@ export default class FormularioForm extends Vue {
|
||||
|
||||
public proxy: Record<string, any> = {}
|
||||
|
||||
registry: Registry = new Registry(this)
|
||||
private registry: Registry = new Registry(this)
|
||||
|
||||
private errorObserverRegistry = new ErrorObserverRegistry()
|
||||
// Local error messages are temporal, they wiped each resetValidation call
|
||||
private localFormErrors: string[] = []
|
||||
private localFieldErrors: Record<string, string[]> = {}
|
||||
|
||||
get initialValues (): Record<string, any> {
|
||||
if (this.hasModel && typeof this.formularioValue === 'object') {
|
||||
// If there is a v-model on the form/group, use those values as first priority
|
||||
return { ...this.formularioValue } // @todo - use a deep clone to detach reference types
|
||||
}
|
||||
|
||||
return {}
|
||||
}
|
||||
|
||||
get mergedFormErrors (): string[] {
|
||||
return [...this.formErrors, ...this.localFormErrors]
|
||||
}
|
||||
@ -64,15 +66,6 @@ export default class FormularioForm extends Vue {
|
||||
return this.formularioValue && typeof this.formularioValue === 'object'
|
||||
}
|
||||
|
||||
get initialValues (): Record<string, any> {
|
||||
if (this.hasModel && typeof this.formularioValue === 'object') {
|
||||
// If there is a v-model on the form/group, use those values as first priority
|
||||
return { ...this.formularioValue } // @todo - use a deep clone to detach reference types
|
||||
}
|
||||
|
||||
return {}
|
||||
}
|
||||
|
||||
@Watch('formularioValue', { deep: true })
|
||||
onFormularioValueChanged (values: Record<string, any>): void {
|
||||
if (this.hasModel && values && typeof values === 'object') {
|
||||
@ -101,7 +94,7 @@ export default class FormularioForm extends Vue {
|
||||
|
||||
onFormSubmit (): Promise<void> {
|
||||
return this.hasValidationErrors()
|
||||
.then(hasErrors => hasErrors ? undefined : cloneDeep(this.proxy))
|
||||
.then(hasErrors => hasErrors ? undefined : clone(this.proxy))
|
||||
.then(data => {
|
||||
if (typeof data !== 'undefined') {
|
||||
this.$emit('submit', data)
|
||||
@ -193,6 +186,11 @@ export default class FormularioForm extends Vue {
|
||||
}
|
||||
}
|
||||
|
||||
setErrors ({ formErrors, inputErrors }: { formErrors?: string[]; inputErrors?: Record<string, string[]> }): void {
|
||||
this.localFormErrors = formErrors || []
|
||||
this.localFieldErrors = inputErrors || {}
|
||||
}
|
||||
|
||||
hasValidationErrors (): Promise<boolean> {
|
||||
return Promise.all(this.registry.reduce((resolvers: Promise<boolean>[], input: FormularioInput) => {
|
||||
resolvers.push(input.runValidation() && input.hasValidationErrors())
|
||||
@ -200,11 +198,6 @@ export default class FormularioForm extends Vue {
|
||||
}, [])).then(results => results.some(hasErrors => hasErrors))
|
||||
}
|
||||
|
||||
setErrors ({ formErrors, inputErrors }: { formErrors?: string[]; inputErrors?: Record<string, string[]> }): void {
|
||||
this.localFormErrors = formErrors || []
|
||||
this.localFieldErrors = inputErrors || {}
|
||||
}
|
||||
|
||||
resetValidation (): void {
|
||||
this.localFormErrors = []
|
||||
this.localFieldErrors = {}
|
||||
|
@ -13,7 +13,7 @@ import {
|
||||
Prop,
|
||||
Watch,
|
||||
} from 'vue-property-decorator'
|
||||
import { arrayify, has, shallowEqualObjects, snakeToCamel } from './libs/utils'
|
||||
import { arrayify, has, shallowEqualObjects, snakeToCamel } from './utils'
|
||||
import {
|
||||
CheckRuleFn,
|
||||
CreateMessageFn,
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { shallowEqualObjects, has, getNested } from '@/libs/utils'
|
||||
import { shallowEqualObjects, has, getNested } from '@/utils'
|
||||
import FormularioForm from '@/FormularioForm.vue'
|
||||
import FormularioInput from '@/FormularioInput.vue'
|
||||
|
||||
@ -80,15 +80,6 @@ export default class Registry {
|
||||
return result
|
||||
}
|
||||
|
||||
/**
|
||||
* Map over the registry (recursively).
|
||||
*/
|
||||
map (mapper: Function): Record<string, any> {
|
||||
const value = {}
|
||||
this.registry.forEach((component, field) => Object.assign(value, { [field]: mapper(component, field) }))
|
||||
return value
|
||||
}
|
||||
|
||||
/**
|
||||
* Map over the registry (recursively).
|
||||
*/
|
||||
@ -137,11 +128,6 @@ export default class Registry {
|
||||
// @ts-ignore
|
||||
this.ctx.setFieldValue(field, component.proxy)
|
||||
}
|
||||
// @ts-ignore
|
||||
if (this.ctx.childrenShouldShowErrors) {
|
||||
// @ts-ignore
|
||||
component.formShouldShowErrors = true
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -1,203 +0,0 @@
|
||||
export function shallowEqualObjects (objA: Record<string, any>, objB: Record<string, any>): boolean {
|
||||
if (objA === objB) {
|
||||
return true
|
||||
}
|
||||
|
||||
if (!objA || !objB) {
|
||||
return false
|
||||
}
|
||||
|
||||
const aKeys = Object.keys(objA)
|
||||
const bKeys = Object.keys(objB)
|
||||
|
||||
if (bKeys.length !== aKeys.length) {
|
||||
return false
|
||||
}
|
||||
|
||||
if (objA instanceof Date && objB instanceof Date) {
|
||||
return objA.getTime() === objB.getTime()
|
||||
}
|
||||
|
||||
if (aKeys.length === 0) {
|
||||
return objA === objB
|
||||
}
|
||||
|
||||
for (let i = 0; i < aKeys.length; i++) {
|
||||
const key = aKeys[i]
|
||||
|
||||
if (objA[key] !== objB[key]) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
/**
|
||||
* Given a string, convert snake_case to camelCase
|
||||
* @param {String} string
|
||||
*/
|
||||
export function snakeToCamel (string: string | any): string | any {
|
||||
if (typeof string === 'string') {
|
||||
return string.replace(/([_][a-z0-9])/ig, ($1) => {
|
||||
if (string.indexOf($1) !== 0 && string[string.indexOf($1) - 1] !== '_') {
|
||||
return $1.toUpperCase().replace('_', '')
|
||||
}
|
||||
return $1
|
||||
})
|
||||
}
|
||||
return string
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts to array.
|
||||
* If given parameter is not string, object ot array, result will be an empty array.
|
||||
* @param {*} item
|
||||
*/
|
||||
export function arrayify (item: any): any[] {
|
||||
if (!item) {
|
||||
return []
|
||||
}
|
||||
if (typeof item === 'string') {
|
||||
return [item]
|
||||
}
|
||||
if (Array.isArray(item)) {
|
||||
return item
|
||||
}
|
||||
if (typeof item === 'object') {
|
||||
return Object.values(item)
|
||||
}
|
||||
return []
|
||||
}
|
||||
|
||||
/**
|
||||
* Escape a string for use in regular expressions.
|
||||
*/
|
||||
export function escapeRegExp (string: string): string {
|
||||
return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&') // $& means the whole matched string
|
||||
}
|
||||
|
||||
/**
|
||||
* Given a string format (date) return a regex to match against.
|
||||
*/
|
||||
export function regexForFormat (format: string): RegExp {
|
||||
const escaped = `^${escapeRegExp(format)}$`
|
||||
const formats: Record<string, string> = {
|
||||
MM: '(0[1-9]|1[012])',
|
||||
M: '([1-9]|1[012])',
|
||||
DD: '([012][1-9]|3[01])',
|
||||
D: '([012]?[1-9]|3[01])',
|
||||
YYYY: '\\d{4}',
|
||||
YY: '\\d{2}'
|
||||
}
|
||||
|
||||
return new RegExp(Object.keys(formats).reduce((regex, format) => {
|
||||
return regex.replace(format, formats[format])
|
||||
}, escaped))
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if
|
||||
* @param {*} data
|
||||
*/
|
||||
export function isScalar (data: any): boolean {
|
||||
switch (typeof data) {
|
||||
case 'symbol':
|
||||
case 'number':
|
||||
case 'string':
|
||||
case 'boolean':
|
||||
case 'undefined':
|
||||
return true
|
||||
default:
|
||||
return data === null
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A simple (somewhat non-comprehensive) cloneDeep function, valid for our use
|
||||
* case of needing to unbind reactive watchers.
|
||||
*/
|
||||
export function cloneDeep (value: any): any {
|
||||
if (typeof value !== 'object') {
|
||||
return value
|
||||
}
|
||||
|
||||
const copy: any | Record<string, any> = Array.isArray(value) ? [] : {}
|
||||
|
||||
for (const key in value) {
|
||||
if (Object.prototype.hasOwnProperty.call(value, key)) {
|
||||
if (isScalar(value[key])) {
|
||||
copy[key] = value[key]
|
||||
} else {
|
||||
copy[key] = cloneDeep(value[key])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return copy
|
||||
}
|
||||
|
||||
/**
|
||||
* Shorthand for Object.prototype.hasOwnProperty.call (space saving)
|
||||
*/
|
||||
export function has (ctx: Record<string, any>, prop: string): boolean {
|
||||
return Object.prototype.hasOwnProperty.call(ctx, prop)
|
||||
}
|
||||
|
||||
export function getNested (obj: Record<string, any>, field: string): any {
|
||||
const fieldParts = field.split('.')
|
||||
|
||||
let result: Record<string, any> = obj
|
||||
|
||||
for (const key in fieldParts) {
|
||||
const matches = fieldParts[key].match(/(.+)\[(\d+)\]$/)
|
||||
if (result === undefined) {
|
||||
return null
|
||||
}
|
||||
if (matches) {
|
||||
result = result[matches[1]]
|
||||
|
||||
if (result === undefined) {
|
||||
return null
|
||||
}
|
||||
result = result[matches[2]]
|
||||
} else {
|
||||
result = result[fieldParts[key]]
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
export function setNested (obj: Record<string, any>, field: string, value: any): void {
|
||||
const fieldParts = field.split('.')
|
||||
|
||||
let subProxy: Record<string, any> = obj
|
||||
for (let i = 0; i < fieldParts.length; i++) {
|
||||
const fieldPart = fieldParts[i]
|
||||
const matches = fieldPart.match(/(.+)\[(\d+)\]$/)
|
||||
|
||||
if (matches) {
|
||||
if (subProxy[matches[1]] === undefined) {
|
||||
subProxy[matches[1]] = []
|
||||
}
|
||||
subProxy = subProxy[matches[1]]
|
||||
|
||||
if (i === fieldParts.length - 1) {
|
||||
subProxy[matches[2]] = value
|
||||
break
|
||||
} else {
|
||||
subProxy = subProxy[matches[2]]
|
||||
}
|
||||
} else {
|
||||
if (i === fieldParts.length - 1) {
|
||||
subProxy[fieldPart] = value
|
||||
break
|
||||
} else {
|
||||
// eslint-disable-next-line max-depth
|
||||
if (subProxy[fieldPart] === undefined) {
|
||||
subProxy[fieldPart] = {}
|
||||
}
|
||||
subProxy = subProxy[fieldPart]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
6
src/shims-ext.d.ts
vendored
6
src/shims-ext.d.ts
vendored
@ -11,10 +11,4 @@ declare module 'vue/types/vue' {
|
||||
interface VueRoute {
|
||||
path: string;
|
||||
}
|
||||
|
||||
interface FormularioForm extends Vue {
|
||||
name: string | boolean;
|
||||
proxy: Record<string, any>;
|
||||
hasValidationErrors(): Promise<boolean>;
|
||||
}
|
||||
}
|
||||
|
20
src/utils/arrayify.ts
Normal file
20
src/utils/arrayify.ts
Normal file
@ -0,0 +1,20 @@
|
||||
/**
|
||||
* Converts to array.
|
||||
* If given parameter is not string, object ot array, result will be an empty array.
|
||||
* @param {*} item
|
||||
*/
|
||||
export default function arrayify (item: any): any[] {
|
||||
if (!item) {
|
||||
return []
|
||||
}
|
||||
if (typeof item === 'string') {
|
||||
return [item]
|
||||
}
|
||||
if (Array.isArray(item)) {
|
||||
return item
|
||||
}
|
||||
if (typeof item === 'object') {
|
||||
return Object.values(item)
|
||||
}
|
||||
return []
|
||||
}
|
22
src/utils/clone.ts
Normal file
22
src/utils/clone.ts
Normal file
@ -0,0 +1,22 @@
|
||||
import isScalar from '@/utils/isScalar'
|
||||
import has from '@/utils/has'
|
||||
|
||||
/**
|
||||
* A simple (somewhat non-comprehensive) clone function, valid for our use
|
||||
* case of needing to unbind reactive watchers.
|
||||
*/
|
||||
export default function clone (value: any): any {
|
||||
if (typeof value !== 'object') {
|
||||
return value
|
||||
}
|
||||
|
||||
const copy: any | Record<string, any> = Array.isArray(value) ? [] : {}
|
||||
|
||||
for (const key in value) {
|
||||
if (has(value, key)) {
|
||||
copy[key] = isScalar(value[key]) ? value[key] : clone(value[key])
|
||||
}
|
||||
}
|
||||
|
||||
return copy
|
||||
}
|
6
src/utils/has.ts
Normal file
6
src/utils/has.ts
Normal file
@ -0,0 +1,6 @@
|
||||
/**
|
||||
* Shorthand for Object.prototype.hasOwnProperty.call (space saving)
|
||||
*/
|
||||
export default function has (ctx: Record<string, any>|any[], prop: string|number): boolean {
|
||||
return Object.prototype.hasOwnProperty.call(ctx, prop)
|
||||
}
|
67
src/utils/index.ts
Normal file
67
src/utils/index.ts
Normal file
@ -0,0 +1,67 @@
|
||||
export { default as arrayify } from './arrayify'
|
||||
export { default as clone } from './clone'
|
||||
export { default as has } from './has'
|
||||
export { default as isScalar } from './isScalar'
|
||||
export { default as merge } from './merge'
|
||||
export { default as regexForFormat } from './regexForFormat'
|
||||
export { default as shallowEqualObjects } from './shallowEqualObjects'
|
||||
export { default as snakeToCamel } from './snakeToCamel'
|
||||
|
||||
export function getNested (obj: Record<string, any>, field: string): any {
|
||||
const fieldParts = field.split('.')
|
||||
|
||||
let result: Record<string, any> = obj
|
||||
|
||||
for (const key in fieldParts) {
|
||||
const matches = fieldParts[key].match(/(.+)\[(\d+)\]$/)
|
||||
if (result === undefined) {
|
||||
return null
|
||||
}
|
||||
if (matches) {
|
||||
result = result[matches[1]]
|
||||
|
||||
if (result === undefined) {
|
||||
return null
|
||||
}
|
||||
result = result[matches[2]]
|
||||
} else {
|
||||
result = result[fieldParts[key]]
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
export function setNested (obj: Record<string, any>, field: string, value: any): void {
|
||||
const fieldParts = field.split('.')
|
||||
|
||||
let subProxy: Record<string, any> = obj
|
||||
for (let i = 0; i < fieldParts.length; i++) {
|
||||
const fieldPart = fieldParts[i]
|
||||
const matches = fieldPart.match(/(.+)\[(\d+)\]$/)
|
||||
|
||||
if (matches) {
|
||||
if (subProxy[matches[1]] === undefined) {
|
||||
subProxy[matches[1]] = []
|
||||
}
|
||||
subProxy = subProxy[matches[1]]
|
||||
|
||||
if (i === fieldParts.length - 1) {
|
||||
subProxy[matches[2]] = value
|
||||
break
|
||||
} else {
|
||||
subProxy = subProxy[matches[2]]
|
||||
}
|
||||
} else {
|
||||
if (i === fieldParts.length - 1) {
|
||||
subProxy[fieldPart] = value
|
||||
break
|
||||
} else {
|
||||
// eslint-disable-next-line max-depth
|
||||
if (subProxy[fieldPart] === undefined) {
|
||||
subProxy[fieldPart] = {}
|
||||
}
|
||||
subProxy = subProxy[fieldPart]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
12
src/utils/isScalar.ts
Normal file
12
src/utils/isScalar.ts
Normal file
@ -0,0 +1,12 @@
|
||||
export default function isScalar (data: any): boolean {
|
||||
switch (typeof data) {
|
||||
case 'symbol':
|
||||
case 'number':
|
||||
case 'string':
|
||||
case 'boolean':
|
||||
case 'undefined':
|
||||
return true
|
||||
default:
|
||||
return data === null
|
||||
}
|
||||
}
|
@ -1,5 +1,5 @@
|
||||
import isPlainObject from 'is-plain-object'
|
||||
import { has } from '@/libs/utils.ts'
|
||||
import has from '@/utils/has.ts'
|
||||
|
||||
/**
|
||||
* Create a new object by copying properties of base and mergeWith.
|
||||
|
25
src/utils/regexForFormat.ts
Normal file
25
src/utils/regexForFormat.ts
Normal file
@ -0,0 +1,25 @@
|
||||
/**
|
||||
* Escape a string for use in regular expressions.
|
||||
*/
|
||||
function escapeRegExp (string: string): string {
|
||||
return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&') // $& means the whole matched string
|
||||
}
|
||||
|
||||
/**
|
||||
* Given a string format (date) return a regex to match against.
|
||||
*/
|
||||
export default function regexForFormat (format: string): RegExp {
|
||||
const escaped = `^${escapeRegExp(format)}$`
|
||||
const formats: Record<string, string> = {
|
||||
MM: '(0[1-9]|1[012])',
|
||||
M: '([1-9]|1[012])',
|
||||
DD: '([012][1-9]|3[01])',
|
||||
D: '([012]?[1-9]|3[01])',
|
||||
YYYY: '\\d{4}',
|
||||
YY: '\\d{2}'
|
||||
}
|
||||
|
||||
return new RegExp(Object.keys(formats).reduce((regex, format) => {
|
||||
return regex.replace(format, formats[format])
|
||||
}, escaped))
|
||||
}
|
34
src/utils/shallowEqualObjects.ts
Normal file
34
src/utils/shallowEqualObjects.ts
Normal file
@ -0,0 +1,34 @@
|
||||
export default function shallowEqualObjects (objA: Record<string, any>, objB: Record<string, any>): boolean {
|
||||
if (objA === objB) {
|
||||
return true
|
||||
}
|
||||
|
||||
if (!objA || !objB) {
|
||||
return false
|
||||
}
|
||||
|
||||
const aKeys = Object.keys(objA)
|
||||
const bKeys = Object.keys(objB)
|
||||
|
||||
if (bKeys.length !== aKeys.length) {
|
||||
return false
|
||||
}
|
||||
|
||||
if (objA instanceof Date && objB instanceof Date) {
|
||||
return objA.getTime() === objB.getTime()
|
||||
}
|
||||
|
||||
if (aKeys.length === 0) {
|
||||
return objA === objB
|
||||
}
|
||||
|
||||
for (let i = 0; i < aKeys.length; i++) {
|
||||
const key = aKeys[i]
|
||||
|
||||
if (objA[key] !== objB[key]) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
11
src/utils/snakeToCamel.ts
Normal file
11
src/utils/snakeToCamel.ts
Normal file
@ -0,0 +1,11 @@
|
||||
/**
|
||||
* Given a string, convert snake_case to camelCase
|
||||
*/
|
||||
export default function snakeToCamel (string: string): string {
|
||||
return string.replace(/([_][a-z0-9])/ig, ($1) => {
|
||||
if (string.indexOf($1) !== 0 && string[string.indexOf($1) - 1] !== '_') {
|
||||
return $1.toUpperCase().replace('_', '')
|
||||
}
|
||||
return $1
|
||||
})
|
||||
}
|
@ -1,4 +1,4 @@
|
||||
import { has } from '@/libs/utils'
|
||||
import { has } from '@/utils'
|
||||
|
||||
export interface ErrorHandler {
|
||||
(errors: Record<string, any> | any[]): void;
|
||||
|
@ -1,5 +1,5 @@
|
||||
import isUrl from 'is-url'
|
||||
import { shallowEqualObjects, regexForFormat, has } from '@/libs/utils'
|
||||
import { shallowEqualObjects, regexForFormat, has } from '@/utils'
|
||||
import { ValidationContext } from '@/validation/validator'
|
||||
|
||||
interface DateValidationContext extends ValidationContext {
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { has, snakeToCamel } from '@/libs/utils'
|
||||
import { has, snakeToCamel } from '@/utils'
|
||||
|
||||
export interface Validator {
|
||||
(context: ValidationContext): Promise<Violation|null>;
|
||||
@ -59,7 +59,7 @@ export function parseModifier (ruleName: string): [string, string|null] {
|
||||
return [snakeToCamel(ruleName), null]
|
||||
}
|
||||
|
||||
export function processArrayConstraint (
|
||||
export function processSingleArrayConstraint (
|
||||
constraint: any[],
|
||||
rules: Record<string, CheckRuleFn>,
|
||||
messages: Record<string, CreateMessageFn>
|
||||
@ -93,7 +93,7 @@ export function processArrayConstraint (
|
||||
throw new Error(`[Formulario] Can't create validator for constraint: ${JSON.stringify(constraint)}`)
|
||||
}
|
||||
|
||||
export function processStringConstraint (
|
||||
export function processSingleStringConstraint (
|
||||
constraint: string,
|
||||
rules: Record<string, CheckRuleFn>,
|
||||
messages: Record<string, CreateMessageFn>
|
||||
@ -117,12 +117,8 @@ export function processStringConstraint (
|
||||
throw new Error(`[Formulario] Can't create validator for constraint: ${constraint}`)
|
||||
}
|
||||
|
||||
/**
|
||||
* Given a string or function, parse it and return an array in the format
|
||||
* [fn, [...arguments]]
|
||||
*/
|
||||
export function processConstraint (
|
||||
constraint: any,
|
||||
export function processSingleConstraint (
|
||||
constraint: string|Validator|[Validator|string, ...any[]],
|
||||
rules: Record<string, CheckRuleFn>,
|
||||
messages: Record<string, CreateMessageFn>
|
||||
): [Validator, string|null, string|null] {
|
||||
@ -131,11 +127,11 @@ export function processConstraint (
|
||||
}
|
||||
|
||||
if (Array.isArray(constraint) && constraint.length) {
|
||||
return processArrayConstraint(constraint, rules, messages)
|
||||
return processSingleArrayConstraint(constraint, rules, messages)
|
||||
}
|
||||
|
||||
if (typeof constraint === 'string') {
|
||||
return processStringConstraint(constraint, rules, messages)
|
||||
return processSingleStringConstraint(constraint, rules, messages)
|
||||
}
|
||||
|
||||
return [(): Promise<Violation|null> => Promise.resolve(null), null, null]
|
||||
@ -152,7 +148,7 @@ export function processConstraints (
|
||||
if (!Array.isArray(constraints)) {
|
||||
return []
|
||||
}
|
||||
return constraints.map(constraint => processConstraint(constraint, rules, messages))
|
||||
return constraints.map(constraint => processSingleConstraint(constraint, rules, messages))
|
||||
}
|
||||
|
||||
export function enlarge (groups: ValidatorGroup[]): ValidatorGroup[] {
|
||||
|
@ -1,116 +0,0 @@
|
||||
import { cloneDeep, isScalar, regexForFormat, snakeToCamel } from '@/libs/utils'
|
||||
|
||||
describe('regexForFormat', () => {
|
||||
it('allows MM format with other characters', () => expect(regexForFormat('abc/MM').test('abc/01')).toBe(true))
|
||||
|
||||
it('fails MM format with single digit', () => expect(regexForFormat('abc/MM').test('abc/1')).toBe(false))
|
||||
|
||||
it('allows M format with single digit', () => expect(regexForFormat('M/abc').test('1/abc')).toBe(true))
|
||||
|
||||
it('fails MM format when out of range', () => expect(regexForFormat('M/abc').test('13/abc')).toBe(false))
|
||||
|
||||
it('fails M format when out of range', () => expect(regexForFormat('M/abc').test('55/abc')).toBe(false))
|
||||
|
||||
it('Replaces double digits before singles', () => expect(regexForFormat('MMM').test('313131')).toBe(false))
|
||||
|
||||
it('allows DD format with zero digit', () => expect(regexForFormat('xyz/DD').test('xyz/01')).toBe(true))
|
||||
|
||||
it('fails DD format with single digit', () => expect(regexForFormat('xyz/DD').test('xyz/9')).toBe(false))
|
||||
|
||||
it('allows D format with single digit', () => expect(regexForFormat('xyz/D').test('xyz/9')).toBe(true))
|
||||
|
||||
it('fails D format with out of range digit', () => expect(regexForFormat('xyz/D').test('xyz/92')).toBe(false))
|
||||
|
||||
it('fails DD format with out of range digit', () => expect(regexForFormat('xyz/D').test('xyz/32')).toBe(false))
|
||||
|
||||
it('allows YY format with double zeros', () => expect(regexForFormat('YY').test('00')).toBe(true))
|
||||
|
||||
it('fails YY format with four zeros', () => expect(regexForFormat('YY').test('0000')).toBe(false))
|
||||
|
||||
it('allows YYYY format with four zeros', () => expect(regexForFormat('YYYY').test('0000')).toBe(true))
|
||||
|
||||
it('allows MD-YY', () => expect(regexForFormat('MD-YY').test('12-00')).toBe(true))
|
||||
|
||||
it('allows DM-YY', () => expect(regexForFormat('DM-YY').test('12-00')).toBe(true))
|
||||
|
||||
it('allows date like MM/DD/YYYY', () => expect(regexForFormat('MM/DD/YYYY').test('12/18/1987')).toBe(true))
|
||||
|
||||
it('allows date like YYYY-MM-DD', () => expect(regexForFormat('YYYY-MM-DD').test('1987-01-31')).toBe(true))
|
||||
|
||||
it('fails date like YYYY-MM-DD with out of bounds day', () => expect(regexForFormat('YYYY-MM-DD').test('1987-01-32')).toBe(false))
|
||||
})
|
||||
|
||||
describe('isScalar', () => {
|
||||
it('passes on strings', () => expect(isScalar('hello')).toBe(true))
|
||||
|
||||
it('passes on numbers', () => expect(isScalar(123)).toBe(true))
|
||||
|
||||
it('passes on booleans', () => expect(isScalar(false)).toBe(true))
|
||||
|
||||
it('passes on symbols', () => expect(isScalar(Symbol(123))).toBe(true))
|
||||
|
||||
it('passes on null', () => expect(isScalar(null)).toBe(true))
|
||||
|
||||
it('passes on undefined', () => expect(isScalar(undefined)).toBe(true))
|
||||
|
||||
it('fails on pojo', () => expect(isScalar({})).toBe(false))
|
||||
})
|
||||
|
||||
describe('cloneDeep', () => {
|
||||
it('basic objects stay the same', () => expect(cloneDeep({ a: 123, b: 'hello' })).toEqual({ a: 123, b: 'hello' }))
|
||||
|
||||
it('basic nested objects stay the same', () => {
|
||||
expect(cloneDeep({ a: 123, b: { c: 'hello-world' } }))
|
||||
.toEqual({ a: 123, b: { c: 'hello-world' } })
|
||||
})
|
||||
|
||||
it('simple pojo reference types are re-created', () => {
|
||||
const c = { c: 'hello-world' }
|
||||
const clone = cloneDeep({ a: 123, b: c })
|
||||
expect(clone.b === c).toBe(false)
|
||||
})
|
||||
|
||||
it('retains array structures inside of a pojo', () => {
|
||||
const obj = { a: 'abcd', d: ['first', 'second'] }
|
||||
const clone = cloneDeep(obj)
|
||||
expect(Array.isArray(clone.d)).toBe(true)
|
||||
})
|
||||
|
||||
it('removes references inside array structures', () => {
|
||||
const deepObj = {foo: 'bar'}
|
||||
const obj = { a: 'abcd', d: ['first', deepObj] }
|
||||
const clone = cloneDeep(obj)
|
||||
expect(clone.d[1] === deepObj).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
describe('snakeToCamel', () => {
|
||||
it('converts underscore separated words to camelCase', () => {
|
||||
expect(snakeToCamel('this_is_snake_case')).toBe('thisIsSnakeCase')
|
||||
})
|
||||
|
||||
it('converts underscore separated words to camelCase even if they start with a number', () => {
|
||||
expect(snakeToCamel('this_is_snake_case_2nd_example')).toBe('thisIsSnakeCase2ndExample')
|
||||
})
|
||||
|
||||
it('has no effect on already camelCase words', () => {
|
||||
expect(snakeToCamel('thisIsCamelCase')).toBe('thisIsCamelCase')
|
||||
})
|
||||
|
||||
it('does not capitalize the first word or strip first underscore if a phrase starts with an underscore', () => {
|
||||
expect(snakeToCamel('_this_starts_with_an_underscore')).toBe('_thisStartsWithAnUnderscore')
|
||||
})
|
||||
|
||||
it('ignores double underscores anywhere in a word', () => {
|
||||
expect(snakeToCamel('__unlikely__thing__')).toBe('__unlikely__thing__')
|
||||
})
|
||||
|
||||
it('has no effect hyphenated words', () => {
|
||||
expect(snakeToCamel('not-a-good-name')).toBe('not-a-good-name')
|
||||
})
|
||||
|
||||
it('returns the same function if passed', () => {
|
||||
const fn = () => {}
|
||||
expect(snakeToCamel(fn)).toBe(fn)
|
||||
})
|
||||
})
|
28
test/unit/utils/clone.test.js
Normal file
28
test/unit/utils/clone.test.js
Normal file
@ -0,0 +1,28 @@
|
||||
import clone from '@/utils/clone'
|
||||
|
||||
describe('clone', () => {
|
||||
it('Basic objects stay the same', () => {
|
||||
const obj = { a: 123, b: 'hello' }
|
||||
expect(clone(obj)).toEqual(obj)
|
||||
})
|
||||
|
||||
it('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', () => {
|
||||
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'] }
|
||||
expect(Array.isArray(clone(obj).d)).toBe(true)
|
||||
})
|
||||
|
||||
it('Removes references inside array structures', () => {
|
||||
const obj = { a: 'abcd', d: ['first', { foo: 'bar' }] }
|
||||
expect(clone(obj).d[1] === obj.d[1]).toBe(false)
|
||||
})
|
||||
})
|
17
test/unit/utils/isScalar.test.js
Normal file
17
test/unit/utils/isScalar.test.js
Normal file
@ -0,0 +1,17 @@
|
||||
import isScalar from '@/utils/isScalar'
|
||||
|
||||
describe('isScalar', () => {
|
||||
it('Passes on strings', () => expect(isScalar('hello')).toBe(true))
|
||||
|
||||
it('Passes on numbers', () => expect(isScalar(123)).toBe(true))
|
||||
|
||||
it('Passes on booleans', () => expect(isScalar(false)).toBe(true))
|
||||
|
||||
it('Passes on symbols', () => expect(isScalar(Symbol(123))).toBe(true))
|
||||
|
||||
it('Passes on null', () => expect(isScalar(null)).toBe(true))
|
||||
|
||||
it('Passes on undefined', () => expect(isScalar(undefined)).toBe(true))
|
||||
|
||||
it('Fails on pojo', () => expect(isScalar({})).toBe(false))
|
||||
})
|
79
test/unit/utils/regexForFormat.test.js
Normal file
79
test/unit/utils/regexForFormat.test.js
Normal file
@ -0,0 +1,79 @@
|
||||
import regexForFormat from '@/utils/regexForFormat'
|
||||
|
||||
describe('regexForFormat', () => {
|
||||
it('Allows MM format with other characters', () => {
|
||||
expect(regexForFormat('abc/MM').test('abc/01')).toBe(true)
|
||||
})
|
||||
|
||||
it('Fails MM format with single digit', () => {
|
||||
expect(regexForFormat('abc/MM').test('abc/1')).toBe(false)
|
||||
})
|
||||
|
||||
it('Allows M format with single digit', () => {
|
||||
expect(regexForFormat('M/abc').test('1/abc')).toBe(true)
|
||||
})
|
||||
|
||||
it('Fails MM format when out of range', () => {
|
||||
expect(regexForFormat('M/abc').test('13/abc')).toBe(false)
|
||||
})
|
||||
|
||||
it('Fails M format when out of range', () => {
|
||||
expect(regexForFormat('M/abc').test('55/abc')).toBe(false)
|
||||
})
|
||||
|
||||
it('Replaces double digits before singles', () => {
|
||||
expect(regexForFormat('MMM').test('313131')).toBe(false)
|
||||
})
|
||||
|
||||
it('Allows DD format with zero digit', () => {
|
||||
expect(regexForFormat('xyz/DD').test('xyz/01')).toBe(true)
|
||||
})
|
||||
|
||||
it('Fails DD format with single digit', () => {
|
||||
expect(regexForFormat('xyz/DD').test('xyz/9')).toBe(false)
|
||||
})
|
||||
|
||||
it('Allows D format with single digit', () => {
|
||||
expect(regexForFormat('xyz/D').test('xyz/9')).toBe(true)
|
||||
})
|
||||
|
||||
it('Fails D format with out of range digit', () => {
|
||||
expect(regexForFormat('xyz/D').test('xyz/92')).toBe(false)
|
||||
})
|
||||
|
||||
it('Fails DD format with out of range digit', () => {
|
||||
expect(regexForFormat('xyz/D').test('xyz/32')).toBe(false)
|
||||
})
|
||||
|
||||
it('Allows YY format with double zeros', () => {
|
||||
expect(regexForFormat('YY').test('00')).toBe(true)
|
||||
})
|
||||
|
||||
it('Fails YY format with four zeros', () => {
|
||||
expect(regexForFormat('YY').test('0000')).toBe(false)
|
||||
})
|
||||
|
||||
it('Allows YYYY format with four zeros', () => {
|
||||
expect(regexForFormat('YYYY').test('0000')).toBe(true)
|
||||
})
|
||||
|
||||
it('Allows MD-YY', () => {
|
||||
expect(regexForFormat('MD-YY').test('12-00')).toBe(true)
|
||||
})
|
||||
|
||||
it('Allows DM-YY', () => {
|
||||
expect(regexForFormat('DM-YY').test('12-00')).toBe(true)
|
||||
})
|
||||
|
||||
it('Allows date like MM/DD/YYYY', () => {
|
||||
expect(regexForFormat('MM/DD/YYYY').test('12/18/1987')).toBe(true)
|
||||
})
|
||||
|
||||
it('Allows date like YYYY-MM-DD', () => {
|
||||
expect(regexForFormat('YYYY-MM-DD').test('1987-01-31')).toBe(true)
|
||||
})
|
||||
|
||||
it('Fails date like YYYY-MM-DD with out of bounds day', () => {
|
||||
expect(regexForFormat('YYYY-MM-DD').test('1987-01-32')).toBe(false)
|
||||
})
|
||||
})
|
27
test/unit/utils/snakeToCamel.test.js
Normal file
27
test/unit/utils/snakeToCamel.test.js
Normal file
@ -0,0 +1,27 @@
|
||||
import snakeToCamel from '@/utils/snakeToCamel'
|
||||
|
||||
describe('snakeToCamel', () => {
|
||||
it('Converts underscore separated words to camelCase', () => {
|
||||
expect(snakeToCamel('this_is_snake_case')).toBe('thisIsSnakeCase')
|
||||
})
|
||||
|
||||
it('Converts underscore separated words to camelCase even if they start with a number', () => {
|
||||
expect(snakeToCamel('this_is_snake_case_2nd_example')).toBe('thisIsSnakeCase2ndExample')
|
||||
})
|
||||
|
||||
it('Has no effect on already camelCase words', () => {
|
||||
expect(snakeToCamel('thisIsCamelCase')).toBe('thisIsCamelCase')
|
||||
})
|
||||
|
||||
it('Does not capitalize the first word or strip first underscore if a phrase starts with an underscore', () => {
|
||||
expect(snakeToCamel('_this_starts_with_an_underscore')).toBe('_thisStartsWithAnUnderscore')
|
||||
})
|
||||
|
||||
it('Ignores double underscores anywhere in a word', () => {
|
||||
expect(snakeToCamel('__unlikely__thing__')).toBe('__unlikely__thing__')
|
||||
})
|
||||
|
||||
it('Has no effect hyphenated words', () => {
|
||||
expect(snakeToCamel('not-a-good-name')).toBe('not-a-good-name')
|
||||
})
|
||||
})
|
@ -1,6 +1,5 @@
|
||||
import rules from '@/validation/rules.ts'
|
||||
|
||||
|
||||
/**
|
||||
* Accepted rule
|
||||
*/
|
||||
|
Loading…
Reference in New Issue
Block a user