mirror of
https://github.com/appium/appium.git
synced 2026-04-28 14:30:27 -05:00
feat(base-driver): Remove dependency to validate.js (#21175)
This commit is contained in:
Generated
+5
-5
@@ -17260,6 +17260,7 @@
|
||||
},
|
||||
"node_modules/validate.js": {
|
||||
"version": "0.13.1",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/vary": {
|
||||
@@ -18368,8 +18369,7 @@
|
||||
"path-to-regexp": "8.2.0",
|
||||
"serve-favicon": "2.5.0",
|
||||
"source-map-support": "0.5.21",
|
||||
"type-fest": "4.38.0",
|
||||
"validate.js": "0.13.1"
|
||||
"type-fest": "4.38.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^20.9.0 || >=22.11.0",
|
||||
@@ -20313,8 +20313,7 @@
|
||||
"serve-favicon": "2.5.0",
|
||||
"source-map-support": "0.5.21",
|
||||
"spdy": "4.0.2",
|
||||
"type-fest": "4.38.0",
|
||||
"validate.js": "0.13.1"
|
||||
"type-fest": "4.38.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"accepts": {
|
||||
@@ -32929,7 +32928,8 @@
|
||||
"dev": true
|
||||
},
|
||||
"validate.js": {
|
||||
"version": "0.13.1"
|
||||
"version": "0.13.1",
|
||||
"dev": true
|
||||
},
|
||||
"vary": {
|
||||
"version": "1.1.2"
|
||||
|
||||
@@ -5,7 +5,6 @@
|
||||
const path = require('path');
|
||||
const yaml = require('yaml');
|
||||
const {fs, util, logger} = require('@appium/support');
|
||||
const validate = require('validate.js');
|
||||
const Handlebars = require('handlebars');
|
||||
const _ = require('lodash');
|
||||
const {asyncify} = require('asyncbox');
|
||||
@@ -13,47 +12,48 @@ const url = require('url');
|
||||
|
||||
const log = logger.getLogger('YamlParser');
|
||||
|
||||
validate.validators.array = function array(value, options, key, attributes) {
|
||||
if (attributes[key] && !validate.isArray(attributes[key])) {
|
||||
return `must be an array`;
|
||||
}
|
||||
};
|
||||
const validators = {
|
||||
isArray: (value, options) => {
|
||||
if (_.isUndefined(value) || _.isArray(value) || !options) {
|
||||
return null;
|
||||
}
|
||||
return 'must be an array';
|
||||
},
|
||||
presence: (value, options) => {
|
||||
if (_.isUndefined(value) && options) {
|
||||
return 'must be present';
|
||||
}
|
||||
return null;
|
||||
},
|
||||
hasAttributes: (value, options) => {
|
||||
if (!value || !options) {
|
||||
return null;
|
||||
}
|
||||
|
||||
validate.validators.hasAttributes = function hasAttributes(value, options) {
|
||||
if (!value) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!_.isArray(value)) {
|
||||
value = [value];
|
||||
}
|
||||
|
||||
for (const item of value) {
|
||||
for (const option of options) {
|
||||
if (_.isUndefined(item[option])) {
|
||||
return `must have attributes: ${options}`;
|
||||
for (const item of (_.isArray(value) ? value : [value])) {
|
||||
for (const option of options) {
|
||||
if (!_.has(item, option)) {
|
||||
return `must have attributes: ${options}`;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
return null;
|
||||
},
|
||||
hasPossibleAttributes(value, options) {
|
||||
if (!value || !_.isArray(value)) {
|
||||
// if just a bare value or empty, allow it through
|
||||
return null;
|
||||
}
|
||||
|
||||
validate.validators.hasPossibleAttributes = function hasPossibleAttributes(value, options) {
|
||||
if (!value) {
|
||||
return;
|
||||
}
|
||||
|
||||
// if just a bare value, allow it through
|
||||
if (!_.isArray(value)) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (const item of value) {
|
||||
for (const key of _.keys(item)) {
|
||||
if (!options.includes(key)) {
|
||||
return `must not include '${key}'. Available options: ${options}`;
|
||||
for (const item of value) {
|
||||
for (const key of _.keys(item)) {
|
||||
if (!_.includes(options, key)) {
|
||||
return `must not include '${key}'. Available options: ${options}`;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
},
|
||||
};
|
||||
|
||||
const CLIENT_URL_TYPES = {
|
||||
@@ -62,7 +62,7 @@ const CLIENT_URL_TYPES = {
|
||||
ios: 'iOS',
|
||||
};
|
||||
|
||||
const validator = {
|
||||
const constraints = {
|
||||
name: {presence: true},
|
||||
short_description: {presence: true},
|
||||
example_usage: {},
|
||||
@@ -87,16 +87,16 @@ const validator = {
|
||||
driver_support: {presence: true},
|
||||
'endpoint.url': {presence: true},
|
||||
'endpoint.url_parameters': {
|
||||
array: true,
|
||||
isArray: true,
|
||||
hasAttributes: ['name', 'description'],
|
||||
},
|
||||
'endpoint.json_parameters': {
|
||||
array: true,
|
||||
isArray: true,
|
||||
hasAttributes: ['name', 'description'],
|
||||
},
|
||||
'endpoint.response': {hasAttributes: ['type', 'description']},
|
||||
specifications: {presence: true},
|
||||
links: {array: true, hasAttributes: ['name', 'url']},
|
||||
links: {isArray: true, hasAttributes: ['name', 'url']},
|
||||
};
|
||||
|
||||
// What range of platforms do the driver's support
|
||||
@@ -273,6 +273,30 @@ async function registerSpecUrlHelper() {
|
||||
|
||||
const YAML_DIR = path.join(__dirname, '..', 'commands-yml');
|
||||
|
||||
function validate(values) {
|
||||
const result = {};
|
||||
for (const [key, constraint] of _.toPairs(constraints)) {
|
||||
const value = values[key];
|
||||
for (const [validatorName, options] of _.toPairs(constraint)) {
|
||||
if (!(validatorName in validators)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const validationError = validators[validatorName](value, options, key);
|
||||
if (_.isNil(validationError)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (key in result) {
|
||||
result[key].push(validationError);
|
||||
} else {
|
||||
result[key] = [validationError];
|
||||
}
|
||||
}
|
||||
}
|
||||
return _.isEmpty(result) ? null : result;
|
||||
}
|
||||
|
||||
async function generateCommands() {
|
||||
await registerSpecUrlHelper();
|
||||
|
||||
@@ -295,7 +319,7 @@ async function generateCommands() {
|
||||
const inputYML = await fs.readFile(filename, 'utf8');
|
||||
const inputJSON = yaml.parse(inputYML);
|
||||
inputJSON.ymlFileName = `/${path.relative(rootFolder, filename)}`;
|
||||
const validationErrors = validate(inputJSON, validator);
|
||||
const validationErrors = validate(inputJSON);
|
||||
if (validationErrors) {
|
||||
throw new Error(`Data validation error for ${filename}: ${JSON.stringify(validationErrors)}`);
|
||||
}
|
||||
|
||||
@@ -81,7 +81,7 @@ describe('utils', function () {
|
||||
const err = parseCapsForInnerDriver(W3C_CAPS, {
|
||||
hello: {presence: true},
|
||||
}).error;
|
||||
err.message.should.match(/'hello' can't be blank/);
|
||||
err.message.should.match(/required/);
|
||||
_.isError(err).should.be.true;
|
||||
});
|
||||
it('should only accept W3C caps that have passing constraints', function () {
|
||||
|
||||
@@ -10,7 +10,7 @@ import type {
|
||||
MergeExclusive,
|
||||
} from 'type-fest';
|
||||
import _ from 'lodash';
|
||||
import {validator} from './desired-caps';
|
||||
import {validator} from './validation';
|
||||
import {util} from '@appium/support';
|
||||
import log from './logger';
|
||||
import {errors} from '../protocol/errors';
|
||||
@@ -94,9 +94,7 @@ export function validateCaps<C extends Constraints>(
|
||||
)
|
||||
) as C;
|
||||
|
||||
const validationErrors = validator.validate(_.pickBy(caps, util.hasValue), constraints, {
|
||||
fullMessages: false,
|
||||
});
|
||||
const validationErrors = validator.validate(_.pickBy(caps, util.hasValue), constraints);
|
||||
|
||||
if (validationErrors) {
|
||||
const message: string[] = [];
|
||||
|
||||
@@ -1,103 +0,0 @@
|
||||
import log from './logger';
|
||||
import _validator from 'validate.js';
|
||||
import B from 'bluebird';
|
||||
|
||||
export const validator =
|
||||
/** @type {import('validate.js').ValidateJS & {promise: typeof import('bluebird')}} */ (
|
||||
_validator
|
||||
);
|
||||
|
||||
validator.validators.isString = function isString(value) {
|
||||
if (typeof value === 'string') {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (typeof value === 'undefined') {
|
||||
return null;
|
||||
}
|
||||
|
||||
return 'must be of type string';
|
||||
};
|
||||
validator.validators.isNumber = function isNumber(value) {
|
||||
if (typeof value === 'number') {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (typeof value === 'undefined') {
|
||||
return null;
|
||||
}
|
||||
|
||||
// allow a string value
|
||||
if (typeof value === 'string' && !isNaN(Number(value))) {
|
||||
log.warn('Number capability passed in as string. Functionality may be compromised.');
|
||||
return null;
|
||||
}
|
||||
|
||||
return 'must be of type number';
|
||||
};
|
||||
validator.validators.isBoolean = function isBoolean(value) {
|
||||
if (typeof value === 'boolean') {
|
||||
return null;
|
||||
}
|
||||
|
||||
// allow a string value
|
||||
if (typeof value === 'string' && ['true', 'false', ''].includes(value)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (typeof value === 'undefined') {
|
||||
return null;
|
||||
}
|
||||
|
||||
return 'must be of type boolean';
|
||||
};
|
||||
validator.validators.isObject = function isObject(value) {
|
||||
if (typeof value === 'object') {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (typeof value === 'undefined') {
|
||||
return null;
|
||||
}
|
||||
|
||||
return 'must be of type object';
|
||||
};
|
||||
validator.validators.isArray = function isArray(value) {
|
||||
if (Array.isArray(value)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (typeof value === 'undefined') {
|
||||
return null;
|
||||
}
|
||||
|
||||
return 'must be of type array';
|
||||
};
|
||||
validator.validators.deprecated = function deprecated(value, options, key) {
|
||||
// do not print caps that hasn't been provided.
|
||||
if (typeof value !== 'undefined' && options) {
|
||||
log.warn(
|
||||
`The '${key}' capability has been deprecated and must not be used anymore. ` +
|
||||
`Please check the driver documentation for possible alternatives.`
|
||||
);
|
||||
}
|
||||
return null;
|
||||
};
|
||||
validator.validators.inclusionCaseInsensitive = function inclusionCaseInsensitive(value, options) {
|
||||
if (typeof value === 'undefined') {
|
||||
return null;
|
||||
} else if (typeof value !== 'string') {
|
||||
return 'unrecognised';
|
||||
}
|
||||
for (let option of options) {
|
||||
if (option.toLowerCase() === value.toLowerCase()) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
return `${value} not part of ${options.toString()}`;
|
||||
};
|
||||
|
||||
validator.promise = B;
|
||||
validator.prettify = function prettify(val) {
|
||||
return val;
|
||||
};
|
||||
@@ -410,9 +410,9 @@ export class BaseDriver<
|
||||
} catch (e) {
|
||||
throw this.log.errorWithException(
|
||||
new errors.SessionNotCreatedError(
|
||||
`The desiredCapabilities object was not valid for the ` +
|
||||
`following reason(s): ${e.message}`,
|
||||
),
|
||||
`Session capabilities were not valid for the ` +
|
||||
`following reason(s): ${e.message}`, e
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
+145
@@ -0,0 +1,145 @@
|
||||
import type { Constraint } from '@appium/types';
|
||||
import log from './logger';
|
||||
import _ from 'lodash';
|
||||
|
||||
export class Validator {
|
||||
private readonly _validators: Record<
|
||||
keyof Constraint,
|
||||
(value: any, options?: any, key?: string) => string | null
|
||||
> = {
|
||||
isString: (value: any, options?: any): string | null => {
|
||||
if (_.isUndefined(value) || _.isNil(options)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (_.isString(value)) {
|
||||
return options ? null : 'must not be of type string';
|
||||
}
|
||||
|
||||
return options ? 'must be of type string' : null;
|
||||
},
|
||||
isNumber: (value: any, options?: any): string | null => {
|
||||
if (_.isUndefined(value) || _.isNil(options)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (_.isNumber(value)) {
|
||||
return options ? null : 'must not be of type number';
|
||||
}
|
||||
|
||||
// allow a string value
|
||||
if (options && _.isString(value) && !isNaN(Number(value))) {
|
||||
log.warn('Number capability passed in as string. Functionality may be compromised.');
|
||||
return null;
|
||||
}
|
||||
|
||||
return options ? 'must be of type number' : null;
|
||||
},
|
||||
isBoolean: (value: any, options?: any): string | null => {
|
||||
if (_.isUndefined(value) || _.isNil(options)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (_.isBoolean(value)) {
|
||||
return options ? null : 'must not be of type boolean';
|
||||
}
|
||||
|
||||
// allow a string value
|
||||
if (options && _.isString(value) && ['true', 'false', ''].includes(value)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return options ? 'must be of type boolean' : null;
|
||||
},
|
||||
isObject: (value: any, options?: any): string | null => {
|
||||
if (_.isUndefined(value) || _.isNil(options)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (_.isPlainObject(value)) {
|
||||
return options ? null : 'must not be a plain object';
|
||||
}
|
||||
|
||||
return options ? 'must be a plain object' : null;
|
||||
},
|
||||
isArray: (value: any, options?: any): string | null => {
|
||||
if (_.isUndefined(value) || _.isNil(options)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (_.isArray(value)) {
|
||||
return options ? null : 'must not be of type array';
|
||||
}
|
||||
|
||||
return options ? 'must be of type array' : null;
|
||||
},
|
||||
deprecated: (value: any, options?: any, key?: string): string | null => {
|
||||
if (!_.isUndefined(value) && options) {
|
||||
log.warn(
|
||||
`The '${key}' capability has been deprecated and must not be used anymore. ` +
|
||||
`Please check the driver documentation for possible alternatives.`
|
||||
);
|
||||
}
|
||||
return null;
|
||||
},
|
||||
inclusion: (value: any, options?: any): string | null => {
|
||||
if (_.isUndefined(value) || !options) {
|
||||
return null;
|
||||
}
|
||||
const optionsArr = _.isArray(options) ? options : [options];
|
||||
if (optionsArr.some((opt) => opt === value)) {
|
||||
return null;
|
||||
}
|
||||
return `must be contained by ${JSON.stringify(optionsArr)}`;
|
||||
},
|
||||
inclusionCaseInsensitive: (value: any, options?: any): string | null => {
|
||||
if (_.isUndefined(value) || !options) {
|
||||
return null;
|
||||
}
|
||||
const optionsArr = _.isArray(options) ? options : [options];
|
||||
if (optionsArr.some((opt) => _.toLower(opt) === _.toLower(value))) {
|
||||
return null;
|
||||
}
|
||||
return `must be contained by ${JSON.stringify(optionsArr)}`;
|
||||
},
|
||||
presence: (value: any, options?: any): string | null => {
|
||||
if (_.isUndefined(value) && options) {
|
||||
return 'is required to be present';
|
||||
}
|
||||
if (
|
||||
!options?.allowEmpty &&
|
||||
((!_.isUndefined(value) && _.isEmpty(value)) || (_.isString(value) && !_.trim(value)))
|
||||
) {
|
||||
return 'must not be empty or blank';
|
||||
}
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
validate(values: Record<string, any>, constraints: Record<string, Constraint>): Record<string, string[]> | null {
|
||||
const result: Record<string, string[]> = {};
|
||||
for (const [key, constraint] of _.toPairs(constraints)) {
|
||||
const value = values[key];
|
||||
for (const [validatorName, options] of _.toPairs(constraint)) {
|
||||
if (!(validatorName in this._validators)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const validationError = this._validators[validatorName](value, options, key);
|
||||
if (_.isNil(validationError)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (key in result) {
|
||||
result[key].push(validationError);
|
||||
} else {
|
||||
result[key] = [validationError];
|
||||
}
|
||||
}
|
||||
}
|
||||
return _.isEmpty(result) ? null : result;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export const validator = new Validator();
|
||||
@@ -62,8 +62,7 @@
|
||||
"path-to-regexp": "8.2.0",
|
||||
"serve-favicon": "2.5.0",
|
||||
"source-map-support": "0.5.21",
|
||||
"type-fest": "4.38.0",
|
||||
"validate.js": "0.13.1"
|
||||
"type-fest": "4.38.0"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"spdy": "4.0.2"
|
||||
|
||||
@@ -39,7 +39,7 @@ describe('caps', function () {
|
||||
|
||||
describe('throws errors if constraints are not met', function () {
|
||||
it('returns invalid argument error if "present" constraint not met on property', function () {
|
||||
(() => validateCaps({}, {foo: {presence: true}})).should.throw(/'foo' can't be blank/);
|
||||
(() => validateCaps({}, {foo: {presence: true}})).should.throw(/'foo' is required/);
|
||||
});
|
||||
|
||||
it('returns the capability that was passed in if "skipPresenceConstraint" is false', function () {
|
||||
@@ -69,13 +69,13 @@ describe('caps', function () {
|
||||
it('returns invalid argument error if "inclusion" constraint not met on property', function () {
|
||||
(() =>
|
||||
validateCaps({foo: '3'}, {foo: {inclusionCaseInsensitive: ['1', '2']}})).should.throw(
|
||||
/'foo' 3 not part of 1,2/
|
||||
/'foo' must be contained/
|
||||
);
|
||||
});
|
||||
|
||||
it('returns invalid argument error if "inclusionCaseInsensitive" constraint not met on property', function () {
|
||||
(() => validateCaps({foo: 'a'}, {foo: {inclusion: ['A', 'B', 'C']}})).should.throw(
|
||||
/'foo' a is not included in the list/
|
||||
/'foo' must be contained/
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -361,7 +361,7 @@ describe('caps', function () {
|
||||
presence: true,
|
||||
},
|
||||
}
|
||||
)).should.throw(/'missingCap' can't be blank/);
|
||||
)).should.throw(/'missingCap' is required/);
|
||||
});
|
||||
|
||||
describe('validate Appium constraints', function () {
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
import B from 'bluebird';
|
||||
import {BaseDriver, errors} from '../../../lib/index';
|
||||
import {validator} from '../../../lib/basedriver/desired-caps';
|
||||
import {validator} from '../../../lib/basedriver/validation';
|
||||
// eslint-disable-next-line import/named
|
||||
import {createSandbox} from 'sinon';
|
||||
|
||||
@@ -29,7 +29,7 @@ describe('Desired Capabilities', function () {
|
||||
d = new BaseDriver();
|
||||
sandbox = createSandbox();
|
||||
logWarnSpy = sandbox.spy(d.log, 'warn');
|
||||
deprecatedStub = sandbox.stub(validator.validators, 'deprecated');
|
||||
deprecatedStub = sandbox.stub(validator._validators, 'deprecated');
|
||||
});
|
||||
|
||||
afterEach(function () {
|
||||
@@ -344,66 +344,25 @@ describe('Desired Capabilities', function () {
|
||||
}
|
||||
});
|
||||
|
||||
it('should still validate null/undefined/empty caps whose presence is required', async function () {
|
||||
d.desiredCapConstraints = {
|
||||
foo: {
|
||||
presence: true,
|
||||
},
|
||||
};
|
||||
|
||||
await d
|
||||
.createSession({
|
||||
alwaysMatch: {
|
||||
platformName: 'iOS',
|
||||
'appium:foo': null,
|
||||
for (const capValue of [null, '', {}, [], ' ']) {
|
||||
it(`should still validate ${JSON.stringify(capValue)} whose presence is required`, async function () {
|
||||
d.desiredCapConstraints = {
|
||||
foo: {
|
||||
presence: true,
|
||||
},
|
||||
firstMatch: [{}],
|
||||
})
|
||||
.should.be.rejectedWith(/blank/);
|
||||
};
|
||||
|
||||
await d
|
||||
// @ts-expect-error `null` is not actually allowed here
|
||||
.createSession(null, {
|
||||
alwaysMatch: {
|
||||
platformName: 'iOS',
|
||||
'appium:foo': '',
|
||||
},
|
||||
firstMatch: [{}],
|
||||
})
|
||||
.should.be.rejectedWith(/blank/);
|
||||
|
||||
await d
|
||||
.createSession({
|
||||
firstMatch: [
|
||||
{
|
||||
await d
|
||||
.createSession({
|
||||
alwaysMatch: {
|
||||
platformName: 'iOS',
|
||||
'appium:foo': {},
|
||||
'appium:foo': capValue,
|
||||
},
|
||||
],
|
||||
alwaysMatch: {},
|
||||
})
|
||||
.should.be.rejectedWith(/blank/);
|
||||
|
||||
await d
|
||||
.createSession({
|
||||
alwaysMatch: {
|
||||
platformName: 'iOS',
|
||||
'appium:foo': [],
|
||||
},
|
||||
firstMatch: [{}],
|
||||
})
|
||||
.should.be.rejectedWith(/blank/);
|
||||
|
||||
await d
|
||||
.createSession({
|
||||
alwaysMatch: {
|
||||
platformName: 'iOS',
|
||||
'appium:foo': ' ',
|
||||
},
|
||||
firstMatch: [{}],
|
||||
})
|
||||
.should.be.rejectedWith(/blank/);
|
||||
});
|
||||
firstMatch: [{}],
|
||||
})
|
||||
.should.be.rejectedWith(/(blank|required)/);
|
||||
});
|
||||
}
|
||||
|
||||
describe('w3c', function () {
|
||||
it('should accept w3c capabilities', async function () {
|
||||
|
||||
Reference in New Issue
Block a user