feat(typedoc-plugin-appium): display response type

This now displays the type of the response in the command output.  Since all return types from commands are `Promise<T>`, it unwraps `T` and displays only that (since `Promise` is very specific to JS).
This commit is contained in:
Christopher Hiller
2023-01-12 12:26:10 -08:00
parent 734127144d
commit 2a12b88b2f
3 changed files with 216 additions and 73 deletions

View File

@@ -6,16 +6,60 @@ import {
ReflectionFlag,
ReflectionFlags,
ReflectionKind,
SignatureReflection,
} from 'typedoc';
import {CommandMethodDeclarationReflection, CommentSourceType} from '../converter';
import {isCallSignatureReflection} from '../guards';
import {isCallSignatureReflection, isReferenceType} from '../guards';
import {AppiumPluginLogger} from '../logger';
import {AllowedHttpMethod, Command, Route} from './types';
/**
* List of fields to shallow copy from a `SignatureReflection` to a clone
* @internal
*/
const SIGNATURE_REFLECTION_CLONE_FIELDS = [
'anchor',
'comment',
'flags',
'hasOwnDocument',
'implementationOf',
'inheritedFrom',
'kindString',
'label',
'originalName',
'overwrites',
'parameters',
'sources',
'typeParameters',
'url',
];
/**
* List of fields to shallow copy from a `ParameterReflection` to a clone
* @internal
*/
const PARAMETER_REFLECTION_CLONE_FIELDS = [
'anchor',
'comment',
'cssClasses',
'defaultValue',
'hasOwnDocument',
'label',
'originalName',
'sources',
'type',
'url',
];
/**
* Abstract representation of metadata for some sort of Appium command
*/
export abstract class BaseCommandData {
/**
* Loggher
*/
protected readonly log: AppiumPluginLogger;
/**
* The method name of the command handler.
*/
@@ -31,73 +75,55 @@ export abstract class BaseCommandData {
*/
public readonly commentSource?: CommentSourceType;
/**
* List of optional parameter names derived from a method map
* If `true`, this command is from a Plugin (not a Driver)
*/
public readonly optionalParams?: string[];
public readonly isPluginCommand: boolean;
/**
* Actual method reflection.
*
* @todo Determine if this should be required
*/
public readonly methodRefl?: CommandMethodDeclarationReflection;
/**
* List of optional parameter names derived from a method map
*/
public readonly optionalParams?: string[];
/**
* 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
* Loops through signatures of the command's method declaration and returns the first that is a
* `CallSignatureReflection` (if any). This is what we think of when we think "function signature"
*/
#parameters: ParameterReflection[] | undefined;
public readonly isPluginCommand: boolean;
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;
this.isPluginCommand = Boolean(opts.isPluginCommand);
}
public static findCallSignature = _.memoize((cmd: BaseCommandData) =>
cmd.methodRefl?.getAllSignatures()?.find(isCallSignatureReflection)
);
/**
* 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) {
* Returns a list of `ParameterReflection` objects in the command's method declaration;
* rewrites them to prefer the method map parameter list (and the param names)
**/
public static rewriteParameters = _.memoize((cmd: BaseCommandData) => {
if (!cmd.hasCommandParams) {
return [];
}
if (this.#parameters) {
return this.#parameters;
}
const sig = this.methodRefl?.signatures?.find(isCallSignatureReflection);
const sig = BaseCommandData.findCallSignature(cmd);
if (!sig) {
return [];
}
const pRefls =
(this.isPluginCommand ? sig.parameters?.slice(2) : sig.parameters?.slice()) ?? [];
const pRefls = (cmd.isPluginCommand ? sig.parameters?.slice(2) : sig.parameters?.slice()) ?? [];
if (pRefls.length < this.requiredParams!.length + this.optionalParams!.length) {
this.log.warn(
if (pRefls.length < cmd.requiredParams!.length + cmd.optionalParams!.length) {
cmd.log.warn(
'(%s) Method %s has fewer parameters (%d) than specified in the method map (%d)',
this.parentRefl!.name,
this.methodRefl!.name,
cmd.parentRefl!.name,
cmd.methodRefl!.name,
pRefls.length,
this.requiredParams!.length + this.optionalParams!.length
cmd.requiredParams!.length + cmd.optionalParams!.length
);
}
@@ -114,7 +140,7 @@ export abstract class BaseCommandData {
* @returns List of refls with names matching `commandParams`, throwing out any extra refls
*/
const createNewRefls = (kind: 'required' | 'optional'): ParameterReflection[] => {
const commandParams = this[`${kind}Params`];
const commandParams = cmd[`${kind}Params`];
if (!commandParams?.length) {
return [];
}
@@ -130,23 +156,9 @@ export abstract class BaseCommandData {
ReflectionKind.CallSignature,
sig
);
_.assign(
newPRefl,
_.pick(pRefl, [
'defaultValue',
'comment',
'type',
'originalName',
'label',
'sources',
'url',
'anchor',
'hasOwnDocument',
'cssClasses',
])
);
_.assign(newPRefl, _.pick(pRefl, PARAMETER_REFLECTION_CLONE_FIELDS));
// there doesn't seem to be a straightforward way to clone this object.
// there doesn't seem to be a straightforward way to clone flags.
newPRefl.flags = new ReflectionFlags(...pRefl.flags);
newPRefl.flags.setFlag(ReflectionFlag.Optional, kind === 'optional');
newParamRefls.push(newPRefl);
@@ -160,17 +172,86 @@ export abstract class BaseCommandData {
if (!newParamRefls.length) {
return [];
}
this.#parameters = newParamRefls;
return newParamRefls;
});
/**
* Rewrites a method's return value for documentation.
*
* Given a command having a method declaration, creates a clone of its call signature wherein the
* return type is unwrapped from `Promise`. In other words, if a method returns `Promise<T>`,
* this changes the return type in the signature to `T`.
*
* Note that the return type of a command's method declaration should always be a `ReferenceType` having
* name `Promise`.
*/
public static unwrapSignatureType = _.memoize((cmd: BaseCommandData) => {
const callSig = BaseCommandData.findCallSignature(cmd);
if (!callSig) {
return;
}
if (isReferenceType(callSig.type) && callSig.type.name === 'Promise') {
const newCallSig = new SignatureReflection(
callSig.name,
ReflectionKind.CallSignature,
cmd.methodRefl!
);
_.assign(newCallSig, _.pick(callSig, SIGNATURE_REFLECTION_CLONE_FIELDS));
// this is the actual unwrapping. `Promise` only has a single type argument `T`,
// so we can safely use the first one.
newCallSig.type = callSig.type.typeArguments?.[0];
if (!newCallSig.type) {
cmd.log.warn(
'(%s) No type arg T found for return type Promise<T> in %s; this is a bug',
cmd.parentRefl!.name,
cmd.methodRefl!.name
);
return;
}
return newCallSig;
}
});
/**
* The thing which the method is a member of for documentation purposes
*/
parentRefl?: DeclarationReflection;
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;
this.isPluginCommand = Boolean(opts.isPluginCommand);
}
/**
* Returns `true` if the method or execute map defined parameters for this command
*/
get hasCommandParams(): boolean {
public get hasCommandParams(): boolean {
return Boolean(this.optionalParams?.length || this.requiredParams?.length);
}
/**
* Gets a list of function parameters (for use in rendering)
*/
public get parameters() {
return BaseCommandData.rewriteParameters(this);
}
/**
* Gets the call signature (for use in rendering)
*/
public get signature() {
return BaseCommandData.unwrapSignatureType(this);
}
/**
* Should create a shallow clone of the implementing instance
* @param opts New options to pass to the new instance
@@ -192,29 +273,34 @@ export interface CommandDataOpts {
* For debugging purposes mainly
*/
commentSource?: CommentSourceType;
/**
* If `true`, `refl` represents a `PluginCommand`, wherein we will ignore
* the first two parameters altogether.
*/
isPluginCommand?: boolean;
/**
* List of optional parameter names derived from a method map
*/
optionalParams?: string[];
/**
* The thing which the method is a member of for documentation purposes
*/
parentRefl?: DeclarationReflection;
/**
* Actual method reflection.
*
* @todo Determine if this should be required
*/
refl?: CommandMethodDeclarationReflection;
/**
* 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;
/**
* If `true`, `refl` represents a `PluginCommand`, wherein we will ignore
* the first two parameters altogether.
*/
isPluginCommand?: boolean;
}
/**
@@ -225,6 +311,7 @@ export class CommandData extends BaseCommandData {
* The HTTP method of the route
*/
public readonly httpMethod: AllowedHttpMethod;
/**
* The route of the command
*/

View File

@@ -1,4 +1,10 @@
import {Comment, DeclarationReflection, ParameterReflection} from 'typedoc';
import {
Comment,
DeclarationReflection,
ParameterReflection,
SignatureReflection,
SomeType,
} from 'typedoc';
import {CommandMethodDeclarationReflection, CommentSourceType} from '../../converter';
import {isExecMethodData} from '../../guards';
import {CommandData, ExecMethodData} from '../command-data';
@@ -52,11 +58,25 @@ export class CommandReflection extends DeclarationReflection {
*/
public readonly comment?: Comment;
/**
* Metadata about where `comment` came from
*/
public readonly commentSource?: CommentSourceType;
/**
* Original method declaration
*/
public readonly refl?: CommandMethodDeclarationReflection;
/**
* Parameters for template display
*/
public readonly parameters?: ParameterReflection[];
/**
* Call signature for template display
*/
public readonly signature?: SignatureReflection;
/**
* Sets props depending on type of `data`
* @param data Command or execute method data
@@ -81,6 +101,7 @@ export class CommandReflection extends DeclarationReflection {
methodRefl: refl,
commentSource,
parameters,
signature,
} = data;
// kind-specific data
@@ -109,6 +130,7 @@ export class CommandReflection extends DeclarationReflection {
this.refl = refl;
this.commentSource = commentSource;
this.parameters = parameters;
this.signature = signature;
}
/**

View File

@@ -20,7 +20,7 @@
{{else}}
{{#if hasRequiredParams}}
#### Required Parameters
##### Required Parameters
{{#each requiredParams}}
- `{{this}}`
@@ -30,7 +30,7 @@
{{#if hasOptionalParams}}
#### Optional Parameters
##### Optional Parameters
{{#each optionalParams}}
- `{{this}}`
@@ -39,4 +39,38 @@
{{/if}}
{{/if}}
{{#if signature}}
#### Response
{{#with signature}}
{{#with type}}
{{{type 'all'}}}
{{/with}}
{{#with comment}}
{{{returns this}}}
{{/with}}
{{#with type}}
{{#if declaration.signatures}}
{{#each declaration.signatures}}
{{> member.signature showSources=false }}
{{/each}}
{{/if}}
{{/with}}
{{/with}}
{{/if}}
{{/unless}}