diff options
| author | Paul Donald | 2026-02-14 18:05:19 +0000 |
|---|---|---|
| committer | Paul Donald | 2026-02-16 00:42:58 +0000 |
| commit | b6fc02d2810fa515398ec6e53c9d04bf835c1de4 (patch) | |
| tree | 15096d0f100e5e2eff22dad665ab987808fdd4d1 | |
| parent | 7ee6ba3dc26627be1af0a689a87f813d9d14a98d (diff) | |
| download | luci-b6fc02d2810fa515398ec6e53c9d04bf835c1de4.tar.gz | |
luci-base: add tuple validator
There are a number of validation types which are useful
but inaccessible when a value field combines simple
data-types. Example <ipaddr><space><ipaddr>. At which point
one must write a custom validate function, and applying the
built-in factory methods is not trivial.
Introduce a tuple function which combines known types
to validate a string, with a single line definition.
E.g. an IP and a port space-separated:
opt.datatype = 'tuple(ipaddr,port)';
All validation methods must return true for valid data.
The tuple function splits on space by default, or any string
provided by sep(). Here, a comma:
opt.datatype = 'tuple(ipaddr,port,sep(","))';
After the string is separated, any error message displayed
corresponds to the first invalid part of the input string
encountered.
Signed-off-by: Paul Donald <newtwen+github@gmail.com>
| -rw-r--r-- | modules/luci-base/htdocs/luci-static/resources/validation.js | 101 |
1 files changed, 101 insertions, 0 deletions
diff --git a/modules/luci-base/htdocs/luci-static/resources/validation.js b/modules/luci-base/htdocs/luci-static/resources/validation.js index b6986bbec8..5e0e7e0d15 100644 --- a/modules/luci-base/htdocs/luci-static/resources/validation.js +++ b/modules/luci-base/htdocs/luci-static/resources/validation.js @@ -309,6 +309,18 @@ const ValidatorFactory = baseclass.extend(/** @lends LuCI.validation.ValidatorFa esc = true; break; + // Skip over quoted strings so commas inside quotes don't split tokens + case 34: // " + case 39: { // '\'' + const quote = code.charCodeAt(i); + let j = i + 1; + for (; j < code.length; j++) { + if (code.charCodeAt(j) === 92) { j++; continue; } + if (code.charCodeAt(j) === quote) { i = j; break; } + } + break; + } + case 40: case 44: if (depth <= 0) { @@ -842,6 +854,95 @@ const ValidatorFactory = baseclass.extend(/** @lends LuCI.validation.ValidatorFa }, /** + * Define a string separator `sep` for use in [tuple]{@link + * LuCI.validation.ValidatorFactory.types#tuple}. + * @function LuCI.validation.ValidatorFactory.types#sep + * @param {string} str define the separator string + * @returns {@link LuCI.validation.Validator#assert assert()} {boolean} + */ + sep(str) { + return this.apply('string', str); + }, + + /** + * Tuple validator: accepts 1-N tokens separated by a given separator + * {@link LuCI.validation.ValidatorFactory.types#sep sep} + * (whitespace by default if {@link LuCI.validation.ValidatorFactory.types#sep sep} + * is omitted) which will be validated against the 1-N types. + * + * This differs from {@link LuCI.validation.ValidatorFactory.types#and and} + * by first splitting the input and applying each validator function + * sequentially on the resulting array of the split string, whereby the + * first type applies to the first value element, the second to the + * second, and so on, to define a concrete order. + * + * {@link LuCI.validation.ValidatorFactory.types#sep sep} + * can appear at any position in the list. + * + * @example + * + * tuple(ipaddr,port) // "192.0.2.1 88" + * + * tuple(host,port,sep(',')) // "taurus,8000" + * + * tuple(port,port,port,sep('-')) // "33-45-78" + * + * @function LuCI.validation.ValidatorFactory.types#tuple + * @param {...function} types {@link LuCI.validation.ValidatorFactory.types + * types validation functions} + * @param {string} [sep()] function to define split separator string. + * @returns {@link LuCI.validation.Validator#assert assert()} {boolean} + */ + tuple() { + const argsraw = Array.prototype.slice.call(arguments); + let sep = null; + + // Build list of (validator, validatorArgs) pairs + const types = []; + for (let i = 0; i < argsraw.length; i += 2) + types.push([ argsraw[i], argsraw[i+1] ]); + + // Determine the separator, if provided + if (types.length) { + for (let t of types) { + if (t[0] === this.factory.types['sep']) { + const e = types.pop(); + if (Array.isArray(e[1]) && e[1].length > 0) + sep = e[1][0]; + } + } + } + + const raw = (this.value || ''); + let tokens = (sep == null) ? raw.split(/\s+/) : raw.split(sep).map(s => s.trim()); + + if (tokens.length != types.length) { + const getName = (t) => { + if (typeof t === 'function') { + for (const k in this.factory.types) + if (this.factory.types[k] === t) + return k; + return _('value'); + } + return _('value'); + }; + + const expectedTypes = types.map(t => getName(t[0])).join(sep == null ? ' ' : sep); + const sepDesc = sep == null ? _('whitespace') : `"${sep}"`; + const msg_multi = _('%s; %d tokens separated by %s').format(expectedTypes, types.length, sepDesc); + const msg_single = _('%s').format(expectedTypes, types.length, sepDesc); + return this.assert(false, (types.length > 1) ? msg_multi : msg_single); + } + + for (let i = 0; i < tokens.length; i++) { + if (!this.apply(types[i][0], tokens[i], types[i][1])) + return this.assert(false, this.error); + } + + return this.assert(true); + }, + + /** * Assert a valid (hexadecimal) WPA key of `8 <= length <= 63`, or hex if `length == 64`. * @function LuCI.validation.ValidatorFactory.types#wpakey * @returns {@link LuCI.validation.Validator#assert assert()} {boolean} |