appium 2.0.0-beta.18 → 2.0.0-beta.21

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (54) hide show
  1. package/build/lib/appium-config.schema.json +0 -0
  2. package/build/lib/appium.js +84 -69
  3. package/build/lib/cli/argparse-actions.js +1 -1
  4. package/build/lib/cli/args.js +87 -223
  5. package/build/lib/cli/extension-command.js +2 -2
  6. package/build/lib/cli/extension.js +14 -6
  7. package/build/lib/cli/parser.js +142 -106
  8. package/build/lib/cli/utils.js +1 -1
  9. package/build/lib/config-file.js +141 -0
  10. package/build/lib/config.js +42 -64
  11. package/build/lib/driver-config.js +41 -20
  12. package/build/lib/drivers.js +1 -1
  13. package/build/lib/ext-config-io.js +165 -0
  14. package/build/lib/extension-config.js +110 -60
  15. package/build/lib/grid-register.js +19 -21
  16. package/build/lib/logsink.js +1 -1
  17. package/build/lib/main.js +135 -72
  18. package/build/lib/plugin-config.js +17 -8
  19. package/build/lib/schema/appium-config-schema.js +252 -0
  20. package/build/lib/schema/arg-spec.js +120 -0
  21. package/build/lib/schema/cli-args.js +173 -0
  22. package/build/lib/schema/cli-transformers.js +76 -0
  23. package/build/lib/schema/index.js +36 -0
  24. package/build/lib/schema/keywords.js +62 -0
  25. package/build/lib/schema/schema.js +357 -0
  26. package/build/lib/utils.js +26 -35
  27. package/lib/appium-config.schema.json +277 -0
  28. package/lib/appium.js +99 -75
  29. package/lib/cli/args.js +138 -335
  30. package/lib/cli/extension-command.js +7 -6
  31. package/lib/cli/extension.js +12 -4
  32. package/lib/cli/parser.js +248 -96
  33. package/lib/config-file.js +227 -0
  34. package/lib/config.js +71 -61
  35. package/lib/driver-config.js +66 -11
  36. package/lib/ext-config-io.js +287 -0
  37. package/lib/extension-config.js +209 -66
  38. package/lib/grid-register.js +24 -21
  39. package/lib/main.js +139 -68
  40. package/lib/plugin-config.js +32 -2
  41. package/lib/schema/appium-config-schema.js +286 -0
  42. package/lib/schema/arg-spec.js +218 -0
  43. package/lib/schema/cli-args.js +273 -0
  44. package/lib/schema/cli-transformers.js +123 -0
  45. package/lib/schema/index.js +2 -0
  46. package/lib/schema/keywords.js +119 -0
  47. package/lib/schema/schema.js +577 -0
  48. package/lib/utils.js +29 -52
  49. package/package.json +17 -11
  50. package/types/appium-config.d.ts +197 -0
  51. package/types/types.d.ts +201 -0
  52. package/LICENSE +0 -201
  53. package/build/lib/cli/parser-helpers.js +0 -106
  54. package/lib/cli/parser-helpers.js +0 -106
@@ -0,0 +1,119 @@
1
+ // @ts-check
2
+
3
+ import { transformers } from './cli-transformers';
4
+
5
+ /**
6
+ * Collection of keyword definitions to add to the singleton `Ajv` instance.
7
+ * @type {Record<string,KeywordDefinition>}
8
+ */
9
+ export const keywords = {
10
+ /**
11
+ * Keyword to provide a list of command alias names for the CLI.
12
+ *
13
+ * If defined, there must be at least one item in the array and it must be non-empty.
14
+ * All items in the array must be unique.
15
+ *
16
+ * @todo Avoid alias collisions!
17
+ * @type {KeywordDefinition}
18
+ * @example
19
+ * {appiumCliAliases: ['B', 'bobby', 'robert']}
20
+ */
21
+ appiumCliAliases: {
22
+ keyword: 'appiumCliAliases',
23
+ metaSchema: {
24
+ type: 'array',
25
+ items: {
26
+ type: 'string',
27
+ minLength: 1,
28
+ },
29
+ minItems: 1,
30
+ uniqueItems: true,
31
+ description: 'List of aliases for the argument. Aliases shorter than three (3) characters will be prefixed with a single dash; otherwise two (2).'
32
+ },
33
+ },
34
+ /**
35
+ * Keyword to provide the name of the property in the destination (parsed
36
+ * args) object. By default, this value will be whatever the property name is,
37
+ * but camel-cased. If a flag needs something _other_ than just camel-casing,
38
+ * use this.
39
+ * @type {KeywordDefinition}
40
+ * @example
41
+ * // for prop 'no-color'
42
+ * {appiumCliDest: 'NOCOLOR'} // value will be stored as property `NOCOLOR` instead of `noColor`
43
+ */
44
+ appiumCliDest: {
45
+ keyword: 'appiumCliDest',
46
+ metaSchema: {
47
+ type: 'string',
48
+ minLength: 1,
49
+ description: 'Name of the associated property in the parsed CLI arguments object'
50
+ },
51
+ },
52
+
53
+ /**
54
+ * CLI-specific description of the property. Sometimes the allowed type can
55
+ * be different enough on the CLI that providing a description written for a
56
+ * config file context wouldn't make sense.
57
+ * @type {KeywordDefinition}
58
+ * @example
59
+ * {appiumCliDescription: 'This is a comma-delimited string, but in the config file it is an array'}
60
+ */
61
+ appiumCliDescription: {
62
+ keyword: 'appiumCliDescription',
63
+ schemaType: 'string',
64
+ metaSchema: {
65
+ type: 'string',
66
+ minLength: 1,
67
+ description: 'Description to provide in the --help text of the CLI. Overrides `description`'
68
+ },
69
+ },
70
+
71
+ /**
72
+ * Transformers for CLI args. These usually take strings then do something with them, like
73
+ * read a file or parse further.
74
+ * @type {KeywordDefinition}
75
+ */
76
+ appiumCliTransformer: {
77
+ keyword: 'appiumCliTransformer',
78
+ metaSchema: {
79
+ type: 'string',
80
+ enum: Object.keys(transformers),
81
+ description: 'The name of a custom transformer to run against the value as provided via the CLI.'
82
+ },
83
+ },
84
+
85
+ /**
86
+ * Flag to tell Appium to _not_ provide this property as a CLI argument.
87
+ * @type {KeywordDefinition}
88
+ */
89
+ appiumCliIgnore: {
90
+ keyword: 'appiumCliIgnore',
91
+ metaSchema: {
92
+ type: 'boolean',
93
+ description: 'If `true`, Appium will not provide this property as a CLI argument.'
94
+ }
95
+ }
96
+ };
97
+
98
+ /**
99
+ * These are the valid values for the `appiumCliTransformer` keyword.
100
+ * Unfortunately, TS cannot infer this in a JS context. In TS, we'd use
101
+ * `as const` when defining `argTransformers`, then get `keyof typeof argTransformers`. alas.
102
+ * @typedef {'csv'|'json'} AppiumCliTransformerName
103
+ */
104
+
105
+ /**
106
+ * These are the custom keywords that Appium recognizes.
107
+ *
108
+ * @typedef {Object} AppiumJSONSchemaKeywords
109
+ * @property {string} [appiumCliDest]
110
+ * @property {string} [appiumCliDescription]
111
+ * @property {string[]} [appiumCliAliases]
112
+ * @property {boolean} [appiumCliIgnore]
113
+ * @property {AppiumCliTransformerName} [appiumCliTransformer]
114
+ */
115
+
116
+
117
+ /**
118
+ * @typedef {import('ajv').KeywordDefinition} KeywordDefinition
119
+ */
@@ -0,0 +1,577 @@
1
+ // @ts-check
2
+
3
+ import Ajv from 'ajv';
4
+ import addFormats from 'ajv-formats';
5
+ import _ from 'lodash';
6
+ import path from 'path';
7
+ import {DRIVER_TYPE, PLUGIN_TYPE} from '../extension-config';
8
+ import {ReadonlyMap} from '../utils';
9
+ import appiumConfigSchema from './appium-config-schema';
10
+ import {ArgSpec, SERVER_PROP_NAME, APPIUM_CONFIG_SCHEMA_ID} from './arg-spec';
11
+ import {keywords} from './keywords';
12
+
13
+ /**
14
+ * Extensions that an extension schema file can have.
15
+ */
16
+ export const ALLOWED_SCHEMA_EXTENSIONS = new Set(['.json', '.js', '.cjs']);
17
+
18
+ /**
19
+ * A wrapper around Ajv and schema-related functions.
20
+ *
21
+ * Should have been named Highlander, because _there can only be one_
22
+ */
23
+ class AppiumSchema {
24
+ /**
25
+ * A mapping of unique argument IDs to their corresponding {@link ArgSpec}s.
26
+ *
27
+ * An "argument" is a CLI argument or a config property.
28
+ *
29
+ * Used to provide easy lookups of argument metadata when converting between different representations of those arguments.
30
+ * @private
31
+ * @type {ReadonlyMap<string,ArgSpec>}
32
+ */
33
+ _argSpecs = new ReadonlyMap();
34
+
35
+ /**
36
+ * A map of extension types to extension names to schema objects.
37
+ *
38
+ * This data structure is used to ensure there are no naming conflicts. The schemas
39
+ * are stored here in memory until the instance is _finalized_.
40
+ * @private
41
+ * @type {Record<ExtensionType,Map<string,SchemaObject>>}
42
+ */
43
+ _registeredSchemas = {[DRIVER_TYPE]: new Map(), [PLUGIN_TYPE]: new Map()};
44
+
45
+ /**
46
+ * Ajv instance
47
+ *
48
+ * @private
49
+ * @type {Ajv}
50
+ */
51
+ _ajv;
52
+
53
+ /**
54
+ * Singleton instance.
55
+ * @private
56
+ * @type {AppiumSchema}
57
+ */
58
+ static _instance;
59
+
60
+ /**
61
+ * Lookup of schema IDs to finalized schemas.
62
+ *
63
+ * This does not include references, but rather the root schemas themselves.
64
+ * @private
65
+ * @type {Record<string,StrictSchemaObject>?}
66
+ */
67
+ _finalizedSchemas = null;
68
+
69
+ /**
70
+ * Initializes Ajv, adds standard formats and our custom keywords.
71
+ * @see https://npm.im/ajv-formats
72
+ * @private
73
+ */
74
+ constructor () {
75
+ this._ajv = AppiumSchema._instantiateAjv();
76
+ }
77
+
78
+ /**
79
+ * Factory function for {@link AppiumSchema} instances.
80
+ *
81
+ * Returns a singleton instance if one exists, otherwise creates a new one.
82
+ * Binds public methods to the instance.
83
+ * @returns {AppiumSchema}
84
+ */
85
+ static create () {
86
+ const instance = AppiumSchema._instance ?? new AppiumSchema();
87
+ AppiumSchema._instance = instance;
88
+
89
+ _.bindAll(instance, [
90
+ 'finalize',
91
+ 'flatten',
92
+ 'getArgSpec',
93
+ 'getDefaults',
94
+ 'getSchema',
95
+ 'hasArgSpec',
96
+ 'isFinalized',
97
+ 'registerSchema',
98
+ 'reset',
99
+ 'validate',
100
+ ]);
101
+
102
+ return instance;
103
+ }
104
+
105
+ /**
106
+ * Returns `true` if a schema has been registered using given extension type and name.
107
+ *
108
+ * This does not depend on whether or not the instance has been _finalized_.
109
+ * @param {ExtensionType} extType - Extension type
110
+ * @param {string} extName - Name
111
+ * @returns {boolean} If registered
112
+ */
113
+ hasRegisteredSchema (extType, extName) {
114
+ return this._registeredSchemas[extType].has(extName);
115
+ }
116
+
117
+ /**
118
+ * Return `true` if {@link AppiumSchema.finalize finalize} has been called
119
+ * successfully and {@link AppiumSchema.reset reset} has not been called since.
120
+ * @returns {boolean} If finalized
121
+ */
122
+ isFinalized () {
123
+ return Boolean(this._finalizedSchemas);
124
+ }
125
+
126
+ /**
127
+ * Call this when no more schemas will be registered.
128
+ *
129
+ * This does three things:
130
+ * 1. It combines all schemas from extensions into the Appium config schema,
131
+ * then adds the result to the `Ajv` instance.
132
+ * 2. It adds schemas for _each_ argument/property for validation purposes.
133
+ * The CLI uses these schemas to validate specific arguments.
134
+ * 3. The schemas are validated against JSON schema draft-07 (which is the
135
+ * only one supported at this time)
136
+ *
137
+ * Any method in this instance that needs to interact with the `Ajv` instance
138
+ * will throw if this method has not been called.
139
+ *
140
+ * If the instance has already been finalized, this is a no-op.
141
+ * @public
142
+ * @throws {Error} If the schema is not valid
143
+ * @returns {Readonly<Record<string,StrictSchemaObject>>} Record of schema IDs to full schema objects
144
+ */
145
+ finalize () {
146
+ if (this.isFinalized()) {
147
+ return /** @type {Record<string,StrictSchemaObject>} */ (
148
+ this._finalizedSchemas
149
+ );
150
+ }
151
+
152
+ const ajv = this._ajv;
153
+
154
+ // Ajv will _mutate_ the schema, so we need to clone it.
155
+ const baseSchema = _.cloneDeep(/** @type {StrictSchemaObject} */(appiumConfigSchema));
156
+
157
+ /**
158
+ *
159
+ * @param {SchemaObject} schema
160
+ * @param {ExtensionType} [extType]
161
+ * @param {string} [extName]
162
+ */
163
+ const addArgSpecs = (schema, extType, extName) => {
164
+ for (let [propName, propSchema] of Object.entries(schema)) {
165
+ const argSpec = ArgSpec.create(propName, {
166
+ dest: propSchema.appiumCliDest,
167
+ defaultValue: propSchema.default,
168
+ extType,
169
+ extName,
170
+ });
171
+ const {arg} = argSpec;
172
+ this._argSpecs.set(arg, argSpec);
173
+ }
174
+ };
175
+
176
+ addArgSpecs(
177
+ _.omit(baseSchema.properties.server.properties, [
178
+ DRIVER_TYPE,
179
+ PLUGIN_TYPE,
180
+ ]),
181
+ );
182
+
183
+ /**
184
+ * @type {Record<string,StrictSchemaObject>}
185
+ */
186
+ const finalizedSchemas = {};
187
+
188
+ const finalSchema = _.reduce(
189
+ this._registeredSchemas,
190
+ /**
191
+ * @param {typeof baseSchema} baseSchema
192
+ * @param {Map<string,SchemaObject>} extensionSchemas
193
+ * @param {ExtensionType} extType
194
+ */
195
+ (baseSchema, extensionSchemas, extType) => {
196
+ extensionSchemas.forEach((schema, extName) => {
197
+ const $ref = ArgSpec.toSchemaBaseRef(extType, extName);
198
+ schema.$id = $ref;
199
+ schema.additionalProperties = false; // this makes `schema` become a `StrictSchemaObject`
200
+ baseSchema.properties.server.properties[extType].properties[extName] =
201
+ {$ref, $comment: extName};
202
+ ajv.validateSchema(schema, true);
203
+ addArgSpecs(schema.properties, extType, extName);
204
+ ajv.addSchema(schema, $ref);
205
+ finalizedSchemas[$ref] = /** @type {StrictSchemaObject} */(schema);
206
+ });
207
+ return baseSchema;
208
+ },
209
+ baseSchema,
210
+ );
211
+
212
+ ajv.addSchema(finalSchema, APPIUM_CONFIG_SCHEMA_ID);
213
+ finalizedSchemas[APPIUM_CONFIG_SCHEMA_ID] = finalSchema;
214
+ ajv.validateSchema(finalSchema, true);
215
+
216
+ this._finalizedSchemas = finalizedSchemas;
217
+ return Object.freeze(finalizedSchemas);
218
+ }
219
+
220
+ /**
221
+ * Configures and creates an Ajv instance.
222
+ * @private
223
+ * @returns {Ajv}
224
+ */
225
+ static _instantiateAjv () {
226
+ const ajv = addFormats(
227
+ new Ajv({
228
+ // without this not much validation actually happens
229
+ allErrors: true,
230
+ }),
231
+ );
232
+
233
+ // add custom keywords to ajv. see schema-keywords.js
234
+ _.forEach(keywords, (keyword) => {
235
+ ajv.addKeyword(keyword);
236
+ });
237
+
238
+ return ajv;
239
+ }
240
+
241
+ /**
242
+ * Resets this instance to its original state.
243
+ *
244
+ * - Removes all added schemas from the `Ajv` instance
245
+ * - Resets the map of {@link ArgSpec ArgSpecs}
246
+ * - Resets the map of registered schemas
247
+ * - Sets the {@link AppiumSchema._finalized _finalized} flag to `false`
248
+ *
249
+ * If you need to call {@link AppiumSchema.finalize} again, you'll want to call this first.
250
+ * @returns {void}
251
+ */
252
+ reset () {
253
+ for (const schemaId of Object.keys(this._finalizedSchemas ?? {})) {
254
+ this._ajv.removeSchema(schemaId);
255
+ }
256
+ this._argSpecs = new ReadonlyMap();
257
+ this._registeredSchemas = {
258
+ [DRIVER_TYPE]: new Map(),
259
+ [PLUGIN_TYPE]: new Map(),
260
+ };
261
+ this._finalizedSchemas = null;
262
+
263
+ // Ajv seems to have an over-eager cache, so we have to dump the object entirely.
264
+ this._ajv = AppiumSchema._instantiateAjv();
265
+ }
266
+
267
+ /**
268
+ * Registers a schema from an extension.
269
+ *
270
+ * This is "fail-fast" in that the schema will immediately be validated against JSON schema draft-07 _or_ whatever the value of the schema's `$schema` prop is.
271
+ *
272
+ * Does _not_ add the schema to the `ajv` instance (this is done by {@link AppiumSchema.finalize}).
273
+ * @param {import('../ext-config-io').ExtensionType} extType - Extension type
274
+ * @param {string} extName - Unique extension name for `type`
275
+ * @param {SchemaObject} schema - Schema object
276
+ * @throws {SchemaNameConflictError} If the schema is an invalid
277
+ * @returns {void}
278
+ */
279
+ registerSchema (extType, extName, schema) {
280
+ if (!(extType && extName && !_.isEmpty(schema))) {
281
+ throw new TypeError(
282
+ 'Expected nonempty extension type, extension name and schema parameters',
283
+ );
284
+ }
285
+
286
+ const normalizedExtName = _.kebabCase(extName);
287
+ if (this.hasRegisteredSchema(extType, normalizedExtName)) {
288
+ if (this._registeredSchemas[extType].get(normalizedExtName) === schema) {
289
+ return;
290
+ }
291
+ throw new SchemaNameConflictError(extType, extName);
292
+ }
293
+ this._ajv.validateSchema(schema, true);
294
+
295
+ this._registeredSchemas[extType].set(normalizedExtName, schema);
296
+ }
297
+
298
+ /**
299
+ * Returns a {@link ArgSpec} for the given argument name.
300
+ * @param {string} name - CLI argument name
301
+ * @param {ExtensionType} [extType] - Extension type
302
+ * @param {string} [extName] - Extension name
303
+ * @returns {ArgSpec|undefined} ArgSpec or `undefined` if not found
304
+ */
305
+ getArgSpec (name, extType, extName) {
306
+ return this._argSpecs.get(ArgSpec.toArg(name, extType, extName));
307
+ }
308
+
309
+ /**
310
+ * Returns `true` if the instance knows about an argument by the given `name`.
311
+ * @param {string} name - CLI argument name
312
+ * @param {ExtensionType} [extType] - Extension type
313
+ * @param {string} [extName] - Extension name
314
+ * @returns {boolean} `true` if such an {@link ArgSpec} exists
315
+ */
316
+ hasArgSpec (name, extType, extName) {
317
+ return this._argSpecs.has(ArgSpec.toArg(name, extType, extName));
318
+ }
319
+
320
+ /**
321
+ * Returns a `Record` of argument "dest" strings to default values.
322
+ *
323
+ * The "dest" string is the property name in object returned by `argparse.ArgumentParser['parse_args']`.
324
+ * @returns {Record<string,ArgSpec['defaultValue']>}
325
+ */
326
+ getDefaults () {
327
+ if (!this.isFinalized()) {
328
+ throw new SchemaFinalizationError();
329
+ }
330
+ return [...this._argSpecs.values()].reduce(
331
+ (defaults, {defaultValue, dest}) => {
332
+ if (!_.isUndefined(defaultValue)) {
333
+ defaults[dest] = defaultValue;
334
+ }
335
+ return defaults;
336
+ },
337
+ {},
338
+ );
339
+ }
340
+
341
+ /**
342
+ * Flatten schema into an array of `SchemaObject`s and associated
343
+ * {@link ArgSpec ArgSpecs}.
344
+ *
345
+ * Converts nested extension schemas to keys based on the extension type and
346
+ * name. Used when translating to `argparse` options or getting the list of
347
+ * default values (see {@link AppiumSchema.getDefaults}) for CLI or otherwise.
348
+ *
349
+ * The return value is an intermediate reprsentation used by `cli-args`
350
+ * module's `toParserArgs`, which converts the finalized schema to parameters
351
+ * used by `argparse`.
352
+ * @throws If {@link AppiumSchema.finalize} has not been called yet.
353
+ * @returns {FlattenedSchema}
354
+ */
355
+ flatten () {
356
+ const schema = this.getSchema();
357
+
358
+ /** @type {{properties: SchemaObject, prefix: string[]}[]} */
359
+ const stack = [{properties: schema.properties, prefix: []}];
360
+ /** @type {FlattenedSchema} */
361
+ const flattened = [];
362
+
363
+ // this bit is a recursive algorithm rewritten as a for loop.
364
+ // when we find something we want to traverse, we add it to `stack`
365
+ for (const {properties, prefix} of stack) {
366
+ const pairs = _.toPairs(properties);
367
+ for (const [key, value] of pairs) {
368
+ const {properties, $ref} = value;
369
+ if (properties) {
370
+ stack.push({
371
+ properties,
372
+ prefix: key === SERVER_PROP_NAME ? [] : [...prefix, key],
373
+ });
374
+ } else if ($ref) {
375
+ let refSchema;
376
+ try {
377
+ refSchema = this.getSchema($ref);
378
+ } catch (err) {
379
+ // this can happen if an extension schema supplies a $ref to a non-existent schema
380
+ throw new SchemaUnknownSchemaError($ref);
381
+ }
382
+ const {normalizedExtName} =
383
+ ArgSpec.extensionInfoFromRootSchemaId($ref);
384
+ if (!normalizedExtName) {
385
+ /* istanbul ignore next */
386
+ throw new ReferenceError(
387
+ `Could not determine extension name from schema ID ${$ref}. This is a bug.`,
388
+ );
389
+ }
390
+ stack.push({
391
+ properties: refSchema.properties,
392
+ prefix: [...prefix, key, normalizedExtName],
393
+ });
394
+ } else if (key !== DRIVER_TYPE && key !== PLUGIN_TYPE) {
395
+ const [extType, extName] = prefix;
396
+ const argSpec = this.getArgSpec(
397
+ key,
398
+ /** @type {ExtensionType} */ (extType),
399
+ extName,
400
+ );
401
+ if (!argSpec) {
402
+ /* istanbul ignore next */
403
+ throw new ReferenceError(
404
+ `Unknown argument with key ${key}, extType ${extType} and extName ${extName}. This is a bug.`,
405
+ );
406
+ }
407
+ flattened.push({schema: _.cloneDeep(value), argSpec});
408
+ }
409
+ }
410
+ }
411
+
412
+ return flattened;
413
+ }
414
+
415
+ /**
416
+ * Retrieves the schema itself
417
+ * @public
418
+ * @param {string} [ref] - Schema ID
419
+ * @throws If the schema has not yet been _finalized_
420
+ * @returns {SchemaObject}
421
+ */
422
+ getSchema (ref = APPIUM_CONFIG_SCHEMA_ID) {
423
+ return /** @type {SchemaObject} */ (this._getValidator(ref).schema);
424
+ }
425
+
426
+ /**
427
+ * Retrieves schema validator function from Ajv
428
+ * @param {string} [id] - Schema ID
429
+ * @private
430
+ * @returns {import('ajv').ValidateFunction}
431
+ */
432
+ _getValidator (id = APPIUM_CONFIG_SCHEMA_ID) {
433
+ const validator = this._ajv.getSchema(id);
434
+ if (!validator) {
435
+ if (id === APPIUM_CONFIG_SCHEMA_ID) {
436
+ throw new SchemaFinalizationError();
437
+ } else {
438
+ throw new SchemaUnknownSchemaError(id);
439
+ }
440
+ }
441
+ return validator;
442
+ }
443
+
444
+ /**
445
+ * Given an object, validates it against the Appium config schema.
446
+ * If errors occur, the returned array will be non-empty.
447
+ * @param {any} value - The value (hopefully an object) to validate against the schema
448
+ * @param {string} [ref] - Schema ID or ref.
449
+ * @public
450
+ * @returns {import('ajv').ErrorObject[]} Array of errors, if any.
451
+ */
452
+ validate (value, ref = APPIUM_CONFIG_SCHEMA_ID) {
453
+ const validator = /** @type {import('ajv').ValidateFunction} */ (
454
+ this._getValidator(ref)
455
+ );
456
+ return !validator(value) && _.isArray(validator.errors)
457
+ ? [...validator.errors]
458
+ : [];
459
+ }
460
+
461
+ /**
462
+ * Returns `true` if `filename`'s file extension is allowed (in {@link ALLOWED_SCHEMA_EXTENSIONS}).
463
+ * @param {string} filename
464
+ * @returns {boolean}
465
+ */
466
+ static isAllowedSchemaFileExtension (filename) {
467
+ return ALLOWED_SCHEMA_EXTENSIONS.has(path.extname(filename));
468
+ }
469
+ }
470
+
471
+ /**
472
+ * Thrown when the {@link AppiumSchema} instance has not yet been finalized, but
473
+ * the method called requires it.
474
+ */
475
+ export class SchemaFinalizationError extends Error {
476
+ /**
477
+ * @type {Readonly<string>}
478
+ */
479
+ code = 'APPIUMERR_SCHEMA_FINALIZATION';
480
+
481
+ constructor () {
482
+ super('Schema not yet finalized; `finalize()` must be called first.');
483
+ }
484
+ }
485
+
486
+ /**
487
+ * Thrown when a "unique" schema ID conflicts with an existing schema ID.
488
+ *
489
+ * This is likely going to be caused by attempting to register the same schema twice.
490
+ */
491
+ export class SchemaNameConflictError extends Error {
492
+ /**
493
+ * @type {Readonly<string>}
494
+ */
495
+ code = 'APPIUMERR_SCHEMA_NAME_CONFLICT';
496
+
497
+ /**
498
+ * @type {Readonly<{extType: ExtensionType, extName: string}>}
499
+ */
500
+ data;
501
+
502
+ /**
503
+ * @param {ExtensionType} extType
504
+ * @param {string} extName
505
+ */
506
+ constructor (extType, extName) {
507
+ super(
508
+ `Name for ${extType} schema "${extName}" conflicts with an existing schema`,
509
+ );
510
+ this.data = {extType, extName};
511
+ }
512
+ }
513
+
514
+ /**
515
+ * Thrown when a schema ID was expected, but it doesn't exist on the {@link Ajv} instance.
516
+ */
517
+ export class SchemaUnknownSchemaError extends ReferenceError {
518
+ /**
519
+ * @type {Readonly<string>}
520
+ */
521
+ code = 'APPIUMERR_SCHEMA_UNKNOWN_SCHEMA';
522
+
523
+ /**
524
+ * @type {Readonly<{schemaId: string}>}
525
+ */
526
+ data;
527
+
528
+ /**
529
+ * @param {string} schemaId
530
+ */
531
+ constructor (schemaId) {
532
+ super(`Unknown schema: "${schemaId}"`);
533
+ this.data = {schemaId};
534
+ }
535
+ }
536
+
537
+ const appiumSchema = AppiumSchema.create();
538
+
539
+ export const {
540
+ registerSchema,
541
+ getArgSpec,
542
+ hasArgSpec,
543
+ isFinalized,
544
+ finalize: finalizeSchema,
545
+ reset: resetSchema,
546
+ validate,
547
+ getSchema,
548
+ flatten: flattenSchema,
549
+ getDefaults: getDefaultsFromSchema,
550
+ } = appiumSchema;
551
+ export const {isAllowedSchemaFileExtension} = AppiumSchema;
552
+
553
+ /**
554
+ * @typedef {import('ajv').SchemaObject} SchemaObject
555
+ */
556
+
557
+ /**
558
+ * @typedef {import('../ext-config-io').ExtensionType} ExtensionType
559
+ */
560
+
561
+ /**
562
+ * An object having property `additionalProperties: false`
563
+ * @typedef {Object} StrictProp
564
+ * @property {false} additionalProperties
565
+ */
566
+
567
+ /**
568
+ * A {@link SchemaObject} with `additionalProperties: false`
569
+ * @typedef {SchemaObject & StrictProp} StrictSchemaObject
570
+ */
571
+
572
+ /**
573
+ * A list of schemas associated with properties and their corresponding {@link ArgSpec} objects.
574
+ *
575
+ * Intermediate data structure used when converting the entire schema down to CLI arguments.
576
+ * @typedef {{schema: SchemaObject, argSpec: ArgSpec}[]} FlattenedSchema
577
+ */