]> git.ipfire.org Git - thirdparty/vuejs/core.git/commitdiff
feat: runtime prop validation
authorEvan You <yyx990803@gmail.com>
Tue, 2 Oct 2018 22:29:14 +0000 (18:29 -0400)
committerEvan You <yyx990803@gmail.com>
Tue, 2 Oct 2018 22:29:14 +0000 (18:29 -0400)
packages/core/src/componentOptions.ts
packages/core/src/componentProps.ts
packages/core/src/utils.ts

index fbe25083935032028ded0efd4ad39cc09049476f..4cdfcb3e33ed0731580e30bbf4ecb2b842322608 100644 (file)
@@ -29,7 +29,7 @@ export type PropType<T> = Prop<T> | Prop<T>[]
 export type PropValidator<T> = PropOptions<T> | PropType<T>
 
 export interface PropOptions<T = any> {
-  type?: PropType<T>
+  type?: PropType<T> | true | null
   required?: boolean
   default?: T | null | undefined | (() => T | null | undefined)
   validator?(value: T): boolean
index 0b11c11aca1df360d257b8419468afcda1563bab..f48ae1a93729801d99786e4298db56e295e44052 100644 (file)
@@ -8,12 +8,11 @@ import { immutable, unwrap, lock, unlock } from '@vue/observer'
 import {
   Data,
   ComponentPropsOptions,
-  PropValidator,
   PropOptions,
   Prop,
   PropType
 } from './componentOptions'
-import { camelize, hyphenate } from './utils'
+import { camelize, hyphenate, capitalize } from './utils'
 
 export function initializeProps(instance: MountedComponent, data: Data | null) {
   const { props, attrs } = resolveProps(
@@ -136,7 +135,7 @@ export function resolveProps(
       }
       // runtime validation
       if (__DEV__) {
-        validateProp(key, rawData[key], opt, Component)
+        validateProp(key, rawData[key], opt, Component, isAbsent)
       }
     }
   }
@@ -211,7 +210,7 @@ function isSameType(a: Prop<any>, b: Prop<any>): boolean {
 
 function getTypeIndex(
   type: Prop<any>,
-  expectedTypes: PropType<any> | void
+  expectedTypes: PropType<any> | void | null | true
 ): number {
   if (Array.isArray(expectedTypes)) {
     for (let i = 0, len = expectedTypes.length; i < len; i++) {
@@ -219,17 +218,130 @@ function getTypeIndex(
         return i
       }
     }
-  } else if (expectedTypes != null) {
+  } else if (expectedTypes != null && typeof expectedTypes === 'object') {
     return isSameType(expectedTypes, type) ? 0 : -1
   }
   return -1
 }
 
+type AssertionResult = {
+  valid: boolean
+  expectedType: string
+}
+
 function validateProp(
-  key: string,
+  name: string,
   value: any,
-  validator: PropValidator<any>,
-  Component: ComponentClass | FunctionalComponent
+  prop: PropOptions<any>,
+  Component: ComponentClass | FunctionalComponent,
+  isAbsent: boolean
 ) {
-  // TODO
+  const { type, required, validator } = prop
+  // required!
+  if (required && isAbsent) {
+    console.warn('Missing required prop: "' + name + '"')
+    return
+  }
+  // missing but optional
+  if (value == null && !prop.required) {
+    return
+  }
+  // type check
+  if (type != null && type !== true) {
+    let isValid = false
+    const types = Array.isArray(type) ? type : [type]
+    const expectedTypes = []
+    // value is valid as long as one of the specified types match
+    for (let i = 0; i < types.length && !isValid; i++) {
+      const { valid, expectedType } = assertType(value, types[i])
+      expectedTypes.push(expectedType || '')
+      isValid = valid
+    }
+    if (!isValid) {
+      console.warn(getInvalidTypeMessage(name, value, expectedTypes))
+      return
+    }
+  }
+  // custom validator
+  if (validator && !validator(value)) {
+    console.warn(
+      'Invalid prop: custom validator check failed for prop "' + name + '".'
+    )
+  }
+}
+
+const simpleCheckRE = /^(String|Number|Boolean|Function|Symbol)$/
+
+function assertType(value: any, type: Prop<any>): AssertionResult {
+  let valid
+  const expectedType = getType(type)
+  if (simpleCheckRE.test(expectedType)) {
+    const t = typeof value
+    valid = t === expectedType.toLowerCase()
+    // for primitive wrapper objects
+    if (!valid && t === 'object') {
+      valid = value instanceof type
+    }
+  } else if (expectedType === 'Object') {
+    valid = toRawType(value) === 'Object'
+  } else if (expectedType === 'Array') {
+    valid = Array.isArray(value)
+  } else {
+    valid = value instanceof type
+  }
+  return {
+    valid,
+    expectedType
+  }
+}
+
+function getInvalidTypeMessage(
+  name: string,
+  value: any,
+  expectedTypes: string[]
+): string {
+  let message =
+    `Invalid prop: type check failed for prop "${name}".` +
+    ` Expected ${expectedTypes.map(capitalize).join(', ')}`
+  const expectedType = expectedTypes[0]
+  const receivedType = toRawType(value)
+  const expectedValue = styleValue(value, expectedType)
+  const receivedValue = styleValue(value, receivedType)
+  // check if we need to specify expected value
+  if (
+    expectedTypes.length === 1 &&
+    isExplicable(expectedType) &&
+    !isBoolean(expectedType, receivedType)
+  ) {
+    message += ` with value ${expectedValue}`
+  }
+  message += `, got ${receivedType} `
+  // check if we need to specify received value
+  if (isExplicable(receivedType)) {
+    message += `with value ${receivedValue}.`
+  }
+  return message
+}
+
+function styleValue(value: any, type: string): string {
+  if (type === 'String') {
+    return `"${value}"`
+  } else if (type === 'Number') {
+    return `${Number(value)}`
+  } else {
+    return `${value}`
+  }
+}
+
+function toRawType(value: any): string {
+  return Object.prototype.toString.call(value).slice(8, -1)
+}
+
+function isExplicable(type: string): boolean {
+  const explicitTypes = ['string', 'number', 'boolean']
+  return explicitTypes.some(elem => type.toLowerCase() === elem)
+}
+
+function isBoolean(...args: string[]): boolean {
+  return args.some(elem => elem.toLowerCase() === 'boolean')
 }
index 13d81d2d4fbdf2c5daa562bb5b348547542b79e1..834a7cce3df66dd79448ba4c21db8083f091b538 100644 (file)
@@ -55,6 +55,10 @@ export const hyphenate = (str: string): string => {
   return str.replace(hyphenateRE, '-$1').toLowerCase()
 }
 
+export const capitalize = (str: string): string => {
+  return str.charAt(0).toUpperCase() + str.slice(1)
+}
+
 // https://en.wikipedia.org/wiki/Longest_increasing_subsequence
 export function lis(arr: number[]): number[] {
   const p = arr.slice()