Created
April 7, 2019 09:43
-
-
Save nexpr/eb437a1e8c4cf41e7954a2341e3db7e7 to your computer and use it in GitHub Desktop.
query validator for URLSearchParams
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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: {} } |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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