summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorPaul Donald2026-02-14 18:05:19 +0000
committerPaul Donald2026-02-16 00:42:58 +0000
commitb6fc02d2810fa515398ec6e53c9d04bf835c1de4 (patch)
tree15096d0f100e5e2eff22dad665ab987808fdd4d1
parent7ee6ba3dc26627be1af0a689a87f813d9d14a98d (diff)
downloadluci-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.js101
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}