簡體   English   中英

JavaScript 根據對象列表驗證模式

[英]JavaScript validate schema against list of objects

解決這個問題的最佳方法是什么?

給定具有各種屬性的員工列表。

const employees = [
 { name: 'alice',
   title: "ceo",
   salary: 100,
   reports: [{
      name: "bob",
      title: "cfo",
      salary: 10,
      reports: [{
        name: 'zorp',
        title:"controller",
        salary: 40
      }],
   }],
},
…
]

特別注意“reports”屬性。 員工可以擁有同時也是員工列表的屬性。

和模式 object

const schema = {
  employee: [
    {
    name: "name",
    required: true,
    type: "string"
  },
  {
    name: "title",
    required: true,
    type: "string"
  },
  {
    name: "salary",
    required: false,
    type: "number"
  },
  {
    name: "remote",
    required: false,
    type: "boolean"
  },
  {
    name: "reports",
    required: false,
    type: "array:employee"
  },
]
}

完成驗證 function(我們只需要返回第一個失敗的情況)

function validate(employees,schema) {

/*
- There are multiple test cases, if the test cases all pass you should return
‍‌‌‌‍‍‌‍‌‍‍‌‍‍‌‌‍‌‌‍
     { ok: true, message: "success" }

- if a required property doesn't exist on the validation, you should return 

     { ok: false, message: "${name} does not exist" }

- if a property type is invalid, you should return

     { ok: false, message: "${name} property invalid ${type}" }

- if a property does not belong as part of the schema, you should return

     { ok: false, message: "property ${name} does not belong" }

*/

}

您遇到的問題是您的數據是遞歸的並且您的模式是線性的,即“扁平”。 您試圖將含義編碼為字符串,例如“boolean”和“array:employee”。 這是一種嘗試將遞歸結構表示為模式的糟糕方法。

如果你想構建一個合適的模式驗證器,首先要設計用於制作模式的部分。 使用基礎知識並逐步提高 -

設計

// main.js

import { primitives, validate } from "./schema.js"

const [tnumber, tboolean, tstring] = primitives()

const temployee = ...

const tschema = ...

const mydata = ...

validate(tschema, mydata)

通過定義原語,我們可以創建更高級的類型,例如temployeetschema -

// main.js

import { primitives, validate, required, optional } from "./schema.js"

const [tnumber, tboolean, tstring] = primitives()

const temployee = {
  name: required(tstring),
  title: required(tstring),
  salary: optional(tnumber),
  remote: optional(tboolean),
  get reports() { return optional(tschema) } // recursive types supported!
}

const tschema = [temployee] // array of temployee!

const mydata = ...

validate(tschema, mydata)

實施

現在我們啟動 Schema 模塊 -

  • primitives - 生成符號基元類型
  • required - 防止 null 值的類型
  • optional - 僅當值存在時類型才有效
// schema.js

function *primitives() { while(true) yield Symbol() }

const required = t => v => {
  if (v == null)
    throw Error(`cannot be null`)
  validate(t, v)
}

const optional = t => v => {
  if (v != null)
    validate(t, v)
}

export { primitives, required, optional }

接下來我們將編寫一個內部助手validatePrimitive來驗證基本類型 -

// schema.js (continued)

function validatePrimitive(t, v) {
  switch(t) {
    case tnumber:
      if (v?.constructor != Number)
        throw Error(`${v} is not a number`)
      break
    case tboolean:
      if (v?.constructor != Boolean)
        throw Error(`${v} is not a boolean`)
      break
    case tstring:
      if (v?.constructor != String)
        throw Error(`${v} is not a string`)
      break
    default:
      throw Error(`unsupported primitive type`)
  }
}

最后我們編寫公共validate接口。 它是遞歸的,因為我們正在驗證的模式和數據都是遞歸的。 數據和代碼的這種和諧使我們更容易思考問題並編寫解決問題的程序 -

// schema.js (continued)

function validate(t, v) {
  switch (t?.constructor) {
    case Symbol:
      return validatePrimitive(t, v)
    case Array:
      if (t.length !== 1) throw Error("Array schema must specify exactly one type")
      for (const k of Object.keys(v))
        validate(t[0], v[k])
      break
    case Object:
      for (const k of Object.keys(t))
        validate(t[k], v[k])
      break
    case Function:
      t(v)
      break
    default:
      throw Error(`unsupported schema: ${t}`)
  }
}

export { ..., validate }

運行

import { primitives, required, optional, validate } from "./schema.js"

const [tnumber, tboolean, tstring] = primitives()

const temployee = {
  name: required(tstring),
  title: required(tstring),
  salary: optional(tnumber),
  remote: optional(tboolean),
  get reports() { return optional(tschema) }
}

const tschema = [temployee] // array of temployee

const employees = [
  { name: 'alice',
    title: "ceo",
    salary: 100,
    reports: [{
      name: "bob",
      title: "cfo",
      salary: 10,
      reports: [{
        name: 'zorp',
        title:"controller",
        salary: 40
      }],
    }],
  },
  …
]

validate(tschema, employees) // throws an Error only if invalid

下一步是什么?

您可以設計更多模式工具,例如 -

  • withDefault(t, defaultValue) - 用默認值替換 null 值

    const temployee = { name: tstring, remote: withDefault(tboolean, false) } const tstudent = { name: tstring, major: withDefault(tstring, "undeclared") } const tcourse = { teacher: temployee, enrollments: withDefault([tstudent], []) }
  • inRange(min, max) - 數字范圍守衛

    const temployee = { name: tstring, salary: inRange(0, Infinity) // negative salary invalid! }
  • oneOf(t, choices) - 包容性價值守衛

    const temployee = { name: tstring, title: oneOf(tstring, ["exec", "vp", "staff"]) // must be one of these! }

我們可以通過在遞歸調用周圍添加try..catch來改進錯誤消息。 這允許我們將上下文添加到故障點,以便用戶知道有問題的葉子的完整路徑 -

// schema.js (continued)

function validate(t, v) {
  let k
  switch (t?.constructor) {
    case Symbol:
      return validatePrimitive(t, v)
    case Array:
      if (t.length !== 1) throw Error("Array schema must specify exactly one type")
      try {
        for (k of Object.keys(v))
          validate(t[0], v[k])
      }
      catch (err) {
        throw Error(`${k}th child invalid: ${err.message}`)
      }
      break
    case Object:
      try {
        for (k of Object.keys(t))
          validate(t[k], v[k])
      }
      catch (err) {
        throw Error(`${k} invalid: ${err.message}`)
      }
      break
    case Function:
      t(v)
      break
    default:
      throw Error(`unsupported schema: ${t}`)
  }
}

也許導出常見類型,如 -

  • temail - 有效的 email 地址
  • tphone - 帶有可接受標點符號的數字字符串
  • tpassword - 字符串至少 20 個字符

選擇“必需”或“可選”作為默認行為。 目前這些具有相同的效果 -

const temployee = {
  name: required(tstring),
  ...
}

const temployee = {
  name: tstring,  // null is not a string, so null will fail validation
  ...
}

這意味着required是隱式的,我們可以將其從 Schema 模塊中刪除。 當 nullary 值是可接受的時,用戶應該使用optionalwithDefault

評論

請記住,所有復雜的事物都是由簡單事物組合而成的。 如果你設計的東西不能組合,你就是在寫死胡同的代碼。

這意味着我們可以通過組合其他驗證表達式來編寫復雜的驗證表達式! 考慮添加驗證組合器,例如andor等。

const tuser = {
  newPassword:
    // password must be
    //   at least 20 characters
    //   AND at most 40 characters
    //   AND include 2 symbols
    and(minLength(20), maxLength(40), requireSymbols(2))
  ...
}
const tuser = {
  newPassword:
    // password can be
    //   at least 20 characters
    //   OR 8 characters AND includes 2 symbols
    or(minLength(20), and(requireSymbols(2), minLength(8)))
  ...
}

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM