Skip to content

Instantly share code, notes, and snippets.

@nexpr
Created April 7, 2019 09:43
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save nexpr/eb437a1e8c4cf41e7954a2341e3db7e7 to your computer and use it in GitHub Desktop.
Save nexpr/eb437a1e8c4cf41e7954a2341e3db7e7 to your computer and use it in GitHub Desktop.
query validator for URLSearchParams
import * as qv from "./query-validator.mjs"
const validate = qv.validator(
{
A: "string",
B: { name: "string" },
C: { name: "or", subs: ["string", "null"] },
D: { name: "ref", target: "A", sub: { name: "=", value: "1" } },
E: {
name: "cond",
when: { name: "ref", target: "A", sub: { name: "=", value: "txt" } },
then: { name: "in", values: ["a", "b"] },
else: "number",
},
F: {
name: "cond",
when: { name: "ref", target: "A", sub: { name: "=", value: "1" } },
then: { name: "=", value: "a" },
else: {
name: "cond",
when: { name: "ref", target: "A", sub: { name: "=", value: "2" } },
then: { name: "=", value: "b" },
else: {
name: "cond",
when: { name: "ref", target: "A", sub: { name: "=", value: "3" } },
then: { name: "=", value: "c" },
else: { name: "=", value: "" },
},
},
},
G: { name: "ex1" },
},
new class extends qv.Definition {
ex1(values, option) {
const value = qv.requires(values)
if (value.valid === false) return value
const valid = value.startsWith("p") && value.endsWith("q")
return valid ? qv.ok(value.slice(1, -1)) : qv.error("pで始まってqで終わる必要があります")
}
}()
)
console.log(validate(new URLSearchParams("A=1&B=abc&E=3&F=a&G=p123q")))
// { valid: true, fixed_value: { A: '1', B: 'abc', C: null, D: '1', E: 3, F: 'a', G: '123' }, messages: {} }
qv.register()
console.log(new URLSearchParams("A=xxx&B=10").validate({A: "string", B: "number"}))
// { valid: true, fixed_value: { A: 'xxx', B: 10 }, messages: {} }
const validator = (spec, def) => {
def = def ? (typeof def === "function" ? new def() : def) : new Definition()
return function(param) {
if (typeof param === "string") param = new URLSearchParams(param)
const messages = {}
const result = {}
let all_valid = true
for (const [param_key, param_spec] of Object.entries(spec)) {
const values = param.getAll(param_key)
const {
result: { valid, message, fixed_value },
def_name,
} = run(values, param_spec, { key: param_key, param, spec, def }) || {}
if (valid) {
result[param_key] = fixed_value
} else {
result[param_key] = values
all_valid = false
}
if (message) messages[param_key] = `[${def_name}] ${message}`
}
return { valid: all_valid, fixed_value: result, messages }
}
}
const normalizeParamSpec = sp => {
const normalized = typeof sp === "string" ? { name: sp } : sp
if (typeof normalized.name !== "string") {
throw new Error("definition name is required and must be specified as a string")
}
return normalized
}
const run = (values, sub, info) => {
const sp = normalizeParamSpec(sub)
const def_name = sp.name
if (typeof info.def[def_name] !== "function") {
throw new Error(`definition "${def_name}" is not found`)
}
const result = info.def[def_name](values, sp, info)
return { result, def_name }
}
const ok = fixed_value => ({ valid: true, message: null, fixed_value })
const error = message => ({ valid: false, message })
const requires = values => (values.length ? values[0] : error("値が存在しません"))
const register = () => {
URLSearchParams.prototype.validate = function(spec, def) {
return validator(spec, def)(this)
}
}
class Definition {
null(values, option) {
const valid = values.length === 0
return valid ? ok(null) : error("キーが存在します")
}
multiple(values, option, info) {
switch (option.mode) {
case "all": {
if (option.allow_null && values.length === 0) {
return ok(values)
}
const fixed_values = []
for (const value of values) {
const { result, def_name } = run([value], option.sub, info)
if (!result.valid) {
return error(`エラーになる値が含まれています - [${def_name}] ${result.message}`)
}
fixed_values.push(result.fixed_value)
}
return ok(fixed_values)
}
case "any": {
if (option.allow_null && values.length === 0) {
return ok(values)
}
const messages = []
for (const value of values) {
const { result, def_name } = run([value], option.sub, info)
if (result.valid) {
return ok(result.fixed_value)
}
messages.push(`[${def_name}] ${result.message}`)
}
return error(`すべての値がエラーになりました - ${messages.join(", ")}`)
}
default: {
return error("不正なモードです")
}
}
}
"="(values, option) {
const value = requires(values)
if (value.valid === false) return value
const valid = value === option.value
return valid ? ok(value) : error("値が一致しません")
}
in(values, option) {
const value = requires(values)
if (value.valid === false) return value
const valid = option.values.includes(value)
return valid ? ok(value) : error("値がリストに存在しません")
}
boolean(values, option) {
const value = requires(values)
if (value.valid === false) return value
const valid = ["true", "false"].includes(value)
return valid ? ok(value === "true") : error("boolean型として不正な値です")
}
num_boolean(values, option) {
const value = requires(values)
if (value.valid === false) return value
const on_error_return = error("boolean型として不正な値です")
if (typeof value !== "string") {
return on_error_return
} else if (option.mode === "01") {
const valid = ["0", "1"].includes(value)
return valid ? ok(value === "1") : on_error_return
} else if (option.mode === "zero-other") {
return ok(value !== "0")
} else {
// mode=int
const valid = value && !!value.match(/^[0-9]+$/)
return valid ? ok(!!+value) : on_error_return
}
}
number(values, option) {
const value = requires(values)
if (value.valid === false) return value
const fixed_value = +value
const is_number = value === String(fixed_value)
if (!is_number) return error("数値として不正です")
if (!option.allow_nan && isNaN(fixed_value)) return error("NaN許可されていません")
if (option.integer && ~~fixed_value !== fixed_value) return error("整数値として不正です")
if (option.min && option.min > fixed_value) return error("最小値を下回ります")
if (option.max && option.max < fixed_value) return error("最大値を上回ります")
return ok(fixed_value)
}
string(values, option) {
const value = requires(values)
if (value.valid === false) return value
if (option.min && option.min > value.length) return error("最小文字数を下回ります")
if (option.max && option.max < value.length) return error("最大文字数を上回ります")
if (option.regexp instanceof RegExp && !option.regexp.test(value)) return error("正規表現にマッチしません")
return ok(value)
}
or(values, option, info) {
if (!option.subs || !option.subs.length) {
throw new Error("'or' definition requires 'subs' property")
}
const messages = []
for (const sub of option.subs) {
const { result, def_name } = run(values, sub, info)
if (result.valid) {
return ok(result.fixed_value)
}
messages.push(`[${def_name}] ${result.message}`)
}
return error(`すべての条件がエラーになりました - ${messages.join(", ")}`)
}
and(values, option, info) {
if (!option.subs || !option.subs.length) {
throw new Error("'or' definition requires 'subs' property")
}
const fixed_values = []
for (const sub of option.subs) {
const { result, def_name } = run(values, sub, info)
if (!result.valid) {
return error(`エラーになる条件が含まれています - [${def_name}] ${result.message}`)
}
fixed_values.push(result.fixed_value)
}
return ok(fixed_values)
}
ref(values, option, info) {
if (!option.target || !option.sub) {
throw new Error("'ref' definition requires 'target' and 'sub' properties.")
}
const { result } = run(info.param.getAll(option.target), option.sub, info)
return result
}
cond(values, option, info) {
if (!option.when || !option.then || !option.else) {
throw new Error("'cond' definition requires 'when', 'then', and 'else' properties.")
}
const { result } = run(values, option.when, info)
const sub = result.valid ? option.then : option.else
{
const { result } = run(values, sub, info)
return result
}
}
}
export { validator, register, run, ok, error, requires, Definition }
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment