[英]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)
通過定義原語,我們可以創建更高級的類型,例如temployee
和tschema
-
// 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 值是可接受的時,用戶應該使用optional
或withDefault
。
評論
請記住,所有復雜的事物都是由簡單事物組合而成的。 如果你設計的東西不能組合,你就是在寫死胡同的代碼。
這意味着我們可以通過組合其他驗證表達式來編寫復雜的驗證表達式! 考慮添加驗證組合器,例如and
和or
等。
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.