Skip to content

Commit

Permalink
feature(#285): Basic Implementation of Vue Props Validators
Browse files Browse the repository at this point in the history
  • Loading branch information
FlorianWendelborn committed Sep 13, 2020
1 parent b123322 commit 1d991ca
Show file tree
Hide file tree
Showing 18 changed files with 1,672 additions and 35 deletions.
6 changes: 4 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -50,17 +50,18 @@
"typescript": "^3.9.6"
},
"devDependencies": {
"@babel/core": "7.x",
"@babel/core": "^7.11.6",
"@babel/plugin-proposal-nullish-coalescing-operator": "^7.10.4",
"@babel/plugin-proposal-optional-chaining": "^7.11.0",
"@babel/plugin-syntax-dynamic-import": "^7.8.3",
"@babel/preset-env": "^7.11.0",
"@babel/preset-env": "^7.11.5",
"@babel/preset-typescript": "^7.10.4",
"@typescript-eslint/eslint-plugin": "^2.26.0",
"@typescript-eslint/parser": "^2.26.0",
"@vue/eslint-config-typescript": "^5.0.2",
"autoprefixer": "^9.8.5",
"babel-helper-vue-jsx-merge-props": "^2.0.3",
"babel-jest": "^26.3.0",
"babel-plugin-syntax-jsx": "^6.18.0",
"babel-plugin-transform-vue-jsx": "^3.7.0",
"concurrently": "^5.3.0",
Expand All @@ -73,6 +74,7 @@
"eslint-plugin-sonarjs": "^0.5.0",
"eslint-plugin-vue": "^6.1.2",
"husky": "^4.2.5",
"jest": "^26.4.2",
"lerna": "^3.22.1",
"lint-staged": "^10.2.11",
"nodemon": "^2.0.4",
Expand Down
19 changes: 19 additions & 0 deletions packages/vue-props-validators/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
# `@3yourmind/vue-props-validators`

> Generate Properly Validated Vue Props
## Usage

```typescript
import { vuePropsValidators } from '@3yourmind/vue-props-validators'

export default defineComponent {
props: vuePropsValidators({
example: {
nullable: true,
required: true,
type: 'enum',
}
}
}
```
6 changes: 6 additions & 0 deletions packages/vue-props-validators/babel.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
module.exports = {
presets: [
['@babel/preset-env', { targets: { node: 'current' } }],
'@babel/preset-typescript',
],
}
34 changes: 34 additions & 0 deletions packages/vue-props-validators/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
{
"bugs": {
"url": "https://github.com/3YOURMIND/kotti/issues"
},
"dependencies": {},
"description": "Vue Props Validators",
"directories": {
"dist": "dist"
},
"homepage": "https://github.com/3YOURMIND/kotti/tree/master/packages/vue-props-validators",
"keywords": [
"vue",
"props",
"validators",
"validator",
"vuejs"
],
"license": "MIT",
"main": "dist/index.js",
"name": "@3yourmind/vue-props-validators",
"publishConfig": {
"access": "public"
},
"repository": {
"type": "git",
"url": "git+https://github.com/3YOURMIND/kotti.git"
},
"scripts": {
"build": "rm -rf dist && tsc",
"test": "jest",
"test:watch": "jest --watch"
},
"version": "1.0.0"
}
7 changes: 7 additions & 0 deletions packages/vue-props-validators/source/common/base-validator.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { Option } from '../modules'

export const baseValidator = (
option: Option,
validator: (value: unknown) => boolean,
) => (value: unknown) =>
option.nullable && value === null ? true : validator(value)
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { REQUIRED } from '../constants'
import { Option } from '../modules'

export const resolveDefault = (
option: Option,
): Option['default'] extends typeof REQUIRED
? { required: true }
: { default: Option['default'] } =>
option.default === REQUIRED ? { required: true } : { default: option.default }
1 change: 1 addition & 0 deletions packages/vue-props-validators/source/constants.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export const REQUIRED = Symbol('REQUIRED')
75 changes: 75 additions & 0 deletions packages/vue-props-validators/source/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
import { resolveDefault } from './common/resolve-default'
import { REQUIRED } from './constants'
import { createEnum } from './modules/enum'
import { Options, ExtendsOne, Result } from './types'
import { isNumber } from './utilities'

export const vuePropsValidators = <PROPS extends Options>(
props: PROPS,
): {
[KEY in keyof PROPS]: ExtendsOne<
PROPS[KEY],
'enum',
Result<PROPS[KEY], StringConstructor>,
ExtendsOne<
PROPS[KEY],
'float',
Result<PROPS[KEY], NumberConstructor>,
ExtendsOne<
PROPS[KEY],
'integer',
Result<PROPS[KEY], NumberConstructor>,
never
>
>
>
} =>
Object.fromEntries(
Object.entries(props).map(([prop, option]) => {
switch (option.type) {
case 'enum':
return [prop, createEnum(option)]

case 'float':
return [
prop,
{
...resolveDefault(option),
type: Number,
validator: (value: unknown) => isNumber(value),
},
]

case 'integer':
return [
prop,
{
...resolveDefault(option),
type: Number,
validator: (value: unknown) => Number.isSafeInteger(value),
},
]
}

throw new Error('invalid')
}),
)

const X = vuePropsValidators({
example: {
default: REQUIRED,
nullable: true,
type: 'enum',
},
example2: {
default: REQUIRED,
nullable: true,
type: 'integer',
},
example3: {
// eslint-disable-next-line no-magic-numbers
default: () => 123,
nullable: true,
type: 'integer',
},
})
58 changes: 58 additions & 0 deletions packages/vue-props-validators/source/modules/enum.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import { vuePropsValidators } from '..'
import { REQUIRED } from '../constants'

const BASE_ENUM = {
default: REQUIRED as typeof REQUIRED,
nullable: false,
type: 'enum' as const,
options: [],
}

test('enum has correct type', () =>
expect(
vuePropsValidators({
example: BASE_ENUM,
}),
).toMatchObject({ example: { type: String } }))

test('enum validator works', () => {
const { validator } = vuePropsValidators({
example: { ...BASE_ENUM, options: ['test', 'example'] },
}).example

expect(validator('test')).toBeTruthy()
expect(validator('example')).toBeTruthy()
expect(validator('anything')).toBeFalsy()
})

test('enum (nullable: false)', () =>
expect(
vuePropsValidators({
example: { ...BASE_ENUM, nullable: false },
}).example.validator(null),
).toBeFalsy())

test('enum (nullable: true)', () =>
expect(
vuePropsValidators({
example: { ...BASE_ENUM, nullable: true },
}).example.validator(null),
).toBeTruthy())

test('enum (default)', () =>
expect(
vuePropsValidators({
example: { ...BASE_ENUM, default: () => null },
}).example.default(),
).toBe(null))

test('enum (required)', () =>
expect(
vuePropsValidators({
example: { ...BASE_ENUM, default: REQUIRED },
}),
).toMatchObject({
example: {
required: true,
},
}))
22 changes: 22 additions & 0 deletions packages/vue-props-validators/source/modules/enum.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { baseValidator } from '../common/base-validator'
import { resolveDefault } from '../common/resolve-default'
import { Result } from '../types'

import { TypeBase } from '.'

export type TypeEnum = TypeBase & {
options: string[]
type: 'enum'
}

export const createEnum = <OPTION extends TypeEnum>(
option: OPTION,
): Result<OPTION, StringConstructor> => ({
...resolveDefault(option),
type: String,
validator: baseValidator(
option,
(value: unknown) =>
typeof value === 'string' && option.options.includes(value),
),
})
5 changes: 5 additions & 0 deletions packages/vue-props-validators/source/modules/float.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { TypeBase } from '.'

export type TypeFloat = TypeBase & {
type: 'float'
}
13 changes: 13 additions & 0 deletions packages/vue-props-validators/source/modules/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { REQUIRED } from '../constants'

import { TypeEnum } from './enum'
import { TypeFloat } from './float'
import { TypeInteger } from './integer'
import { TypeString } from './string'

export type TypeBase = {
default: typeof REQUIRED | (() => unknown)
nullable: boolean
}

export type Option = TypeEnum | TypeFloat | TypeInteger | TypeString
5 changes: 5 additions & 0 deletions packages/vue-props-validators/source/modules/integer.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { TypeBase } from '.'

export type TypeInteger = TypeBase & {
type: 'integer'
}
5 changes: 5 additions & 0 deletions packages/vue-props-validators/source/modules/string.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { TypeBase } from '.'

export type TypeString = TypeBase & {
type: 'string'
}
25 changes: 25 additions & 0 deletions packages/vue-props-validators/source/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { REQUIRED } from './constants'
import { Option } from './modules'

type Id<T extends object> = { [KEY in keyof T]: T[KEY] }

export type Options = Record<string, Option>

export type ExtendsOne<
INPUT extends Option,
IF extends Option['type'],
RESULT,
ELSE
> = INPUT['type'] extends IF ? RESULT : ELSE

export type Result<
INPUT extends Option,
TYPE extends typeof String | typeof Number
> = Id<
{
type: TYPE
validator: (value: unknown) => boolean
} & (INPUT['default'] extends typeof REQUIRED
? { required: true }
: { default: INPUT['default'] })
>
7 changes: 7 additions & 0 deletions packages/vue-props-validators/source/utilities.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
/**
* We don't need the full package, as we don’t consider strings to be valid numbers
* @see {@link https://github.com/jonschlinkert/is-number/blob/98e8ff1da1a89f93d1397a24d7413ed15421c139/index.js#L11-L13}
*/
export const isNumber = (value: unknown): boolean =>
// eslint-disable-next-line sonarjs/no-identical-expressions
typeof value === 'number' ? value - value === 0 : false
23 changes: 23 additions & 0 deletions packages/vue-props-validators/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
{
"compilerOptions": {
"allowJs": true,
"allowSyntheticDefaultImports": true,
"declaration": true,
"declarationDir": "dist",
"esModuleInterop": true,
"experimentalDecorators": true,
"lib": ["ESNext", "ES2019"],
"module": "CommonJS",
"moduleResolution": "node",
"noImplicitAny": true,
"outDir": "dist",
"resolveJsonModule": true,
"rootDir": "source",
"skipLibCheck": true,
"sourceMap": true,
"strict": true,
"target": "es5"
},
"exclude": ["node_modules", "dist"],
"include": ["source/*.ts", "source/**/*.ts"]
}

0 comments on commit 1d991ca

Please sign in to comment.