-
-
Notifications
You must be signed in to change notification settings - Fork 6k
/
command-data.ts
295 lines (273 loc) · 8.55 KB
/
command-data.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
import _ from 'lodash';
import pluralize from 'pluralize';
import {
Comment,
DeclarationReflection,
ParameterReflection,
ReflectionFlag,
ReflectionFlags,
ReflectionKind,
} from 'typedoc';
import {
AsyncMethodDeclarationReflection,
ClassDeclarationReflection,
CommentSourceType,
} from '../converter';
import {isCallSignatureReflectionWithArity} from '../guards';
import {AppiumPluginLogger} from '../logger';
import {AllowedHttpMethod, Command, Route} from './types';
/**
* Abstract representation of metadata for some sort of Appium command
*/
export abstract class BaseCommandData {
/**
* The method name of the command handler.
*/
public readonly command: string;
/**
* The comment to display for the command, if any exists
*/
public readonly comment?: Comment;
/**
* Code to describe how and where {@linkcode comment} came from.
*
* For debugging purposes mainly
*/
public readonly commentSource?: CommentSourceType;
/**
* List of optional parameter names derived from a method map
*/
public readonly optionalParams?: string[];
/**
* Actual method reflection.
*
* @todo Determine if this should be required
*/
public readonly methodRefl?: AsyncMethodDeclarationReflection;
/**
* List of required parameter names derived from a method map
*/
public readonly requiredParams?: string[];
/**
* The thing which the method is a member of for documentation purposes
*/
parentRefl?: DeclarationReflection;
protected readonly log: AppiumPluginLogger;
/**
* Cached computed parameters
*/
#parameters: ParameterReflection[] | undefined;
constructor(log: AppiumPluginLogger, command: Command, opts: CommandDataOpts = {}) {
this.command = command;
this.optionalParams = opts.optionalParams;
this.requiredParams = opts.requiredParams;
this.comment = opts.comment;
this.commentSource = opts.commentSource;
this.methodRefl = opts.refl;
this.parentRefl = opts.parentRefl;
this.log = log;
}
/**
* Returns a list of its `ParameterReflection` objects in this instance's method's call signature.
*
* Used to display actual types of parameters by templates. The result is cached.
*/
public get parameters(): ParameterReflection[] {
if (!this.hasCommandParams) {
return [];
}
if (this.#parameters) {
return this.#parameters;
}
const sig = this.methodRefl?.signatures?.find(isCallSignatureReflectionWithArity);
if (!sig) {
return [];
}
const pRefls = [...sig.parameters!];
if (pRefls.length < this.requiredParams!.length + this.optionalParams!.length) {
this.log.warn(
'(%s) Method %s has fewer parameters (%d) than specified in the method map (%d)',
this.parentRefl!.name,
this.methodRefl!.name,
pRefls.length,
this.requiredParams!.length + this.optionalParams!.length
);
}
/**
* This loops over the command parameter names as defined in the method/execute map and attempts
* to associate a `ParameterReflection` object with each.
*
* Because the command param names are essentially properties of a JSON object and the
* `ParameterReflection` instances represent the arguments of a method, we must match them by
* index. In JS, Required arguments always come first, so we can do those first. If there are
* _more_ method arguments than command param names, we toss them out, because they may not be
* part of the public API.
* @param kind Either `required` or `optional`
* @returns List of refls with names matching `commandParams`, throwing out any extra refls
*/
const createNewRefls = (kind: 'required' | 'optional'): ParameterReflection[] => {
const commandParams = this[`${kind}Params`];
if (!commandParams?.length) {
return [];
}
const paramCount = commandParams.length;
const newParamRefls: ParameterReflection[] = [];
for (let i = 0; i < paramCount; i++) {
const pRefl = pRefls.shift();
if (pRefl) {
// if there isn't one, the warning above will have been logged already
const newPRefl = new ParameterReflection(
commandParams[i],
ReflectionKind.CallSignature,
sig
);
_.assign(
newPRefl,
_.pick(pRefl, [
'defaultValue',
'comment',
'type',
'originalName',
'label',
'sources',
'url',
'anchor',
'hasOwnDocument',
'cssClasses',
])
);
// there doesn't seem to be a straightforward way to clone this object.
newPRefl.flags = new ReflectionFlags(...pRefl.flags);
newPRefl.flags.setFlag(ReflectionFlag.Optional, kind === 'optional');
newParamRefls.push(newPRefl);
}
}
return newParamRefls;
};
const newParamRefls = [...createNewRefls('required'), ...createNewRefls('optional')];
if (!newParamRefls.length) {
return [];
}
this.#parameters = newParamRefls;
return newParamRefls;
}
/**
* Returns `true` if the method or execute map defined parameters for this command
*/
get hasCommandParams(): boolean {
return Boolean(this.optionalParams?.length || this.requiredParams?.length);
}
/**
* Should create a shallow clone of the implementing instance
* @param opts New options to pass to the new instance
*/
public abstract clone(opts: CommandDataOpts): BaseCommandData;
}
/**
* Options for {@linkcode CommandData} and {@linkcode ExecMethodData} constructors
*/
export interface CommandDataOpts {
/**
* The comment to display for the command, if any exists
*/
comment?: Comment;
/**
* Name of the reference which the comment is derived from.
*
* For debugging purposes mainly
*/
commentSource?: CommentSourceType;
/**
* List of optional parameter names derived from a method map
*/
optionalParams?: string[];
/**
* Actual method reflection.
*
* @todo Determine if this should be required
*/
refl?: AsyncMethodDeclarationReflection;
/**
* List of required parameter names derived from a method map
*/
requiredParams?: string[];
/**
* The thing which the method is a member of for documentation purposes
*/
parentRefl?: DeclarationReflection;
}
/**
* Represents a generic WD or Appium-specific endpoint
*/
export class CommandData extends BaseCommandData {
/**
* The HTTP method of the route
*/
public readonly httpMethod: AllowedHttpMethod;
/**
* The route of the command
*/
public readonly route: Route;
constructor(
log: AppiumPluginLogger,
command: Command,
httpMethod: AllowedHttpMethod,
route: Route,
opts: CommandDataOpts = {}
) {
super(log, command, opts);
this.httpMethod = httpMethod;
this.route = route;
}
/**
* Creates a **shallow** clone of this instance.
*
* Keeps props {@linkcode BaseCommandData.command command},
* {@linkcode CommandData.httpMethod httpMethod} and
* {@linkcode CommandData.route route}, then applies any other options.
* @param opts Options to apply. _Note:_ you probably want to provide a new `parentRefl`.
* @returns Cloned instance
*/
public override clone(opts: CommandDataOpts = {}): CommandData {
return new CommandData(this.log, this.command, this.httpMethod, this.route, {...this, ...opts});
}
}
/**
* Represents an "execute command" ("execute method")
*
* Each will have a unique `script` property which is provided as the script to run via the
* `execute` WD endpoint.
*
* All of these share the same `execute` route, so it is omitted from this interface.
*/
export class ExecMethodData extends BaseCommandData {
/**
* The name/identifier of the execute script
*
* This is different than the method name.
*/
public readonly script: string;
constructor(
log: AppiumPluginLogger,
command: Command,
script: string,
opts: CommandDataOpts = {}
) {
super(log, command, opts);
this.script = script;
if (!this.methodRefl) {
this.log.verbose(`No reflection for script ${script}`);
}
}
/**
* Creates a **shallow** clone of this instance.
*
* Keeps props {@linkcode BaseCommandData.command command}, {@linkcode ExecMethod.script script},
* then applies any other options.
* @param opts Options to apply
* @returns Cloned instance
*/
public override clone(opts: CommandDataOpts): ExecMethodData {
return new ExecMethodData(this.log, this.command, this.script, {...this, ...opts});
}
}