@atomist/sdm
Version:
Atomist Software Delivery Machine SDK
186 lines (166 loc) • 7.81 kB
text/typescript
/*
* Copyright © 2020 Atomist, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { AutomationContextAware, HandlerContext } from "@atomist/automation-client/lib/HandlerContext";
import { Arg, CommandIncoming } from "@atomist/automation-client/lib/internal/transport/RequestProcessor";
import { HandlerResponse } from "@atomist/automation-client/lib/internal/transport/websocket/WebSocketMessageClient";
import { Parameter } from "@atomist/automation-client/lib/metadata/automationMetadata";
import * as _ from "lodash";
import { CommandListenerExecutionInterruptError } from "../../api-helper/machine/handlerRegistrations";
import { ParameterStyle } from "../registration/CommandRegistration";
import { ParametersObjectValue } from "../registration/ParametersDefinition";
/**
* Object with properties defining parameters. Useful for combination via spreads.
*/
export type ParametersPromptObject<PARAMS, K extends keyof PARAMS = keyof PARAMS> = Record<K, ParametersObjectValue & { force?: boolean }>;
/**
* Factory to create a ParameterPrompt
*/
export type ParameterPromptFactory<PARAMS> = (ctx: HandlerContext) => ParameterPrompt<PARAMS>;
/**
* Options to configure the parameter prompt
*/
export interface ParameterPromptOptions {
/** Optional thread identifier to send this message to or true to send
* this to the message that triggered this command.
*/
thread?: boolean | string;
/**
* Configure strategy on how to ask for parameters in chat or web
*/
parameterStyle?: ParameterStyle;
/**
* Configure auto submit strategy for when all required parameters are collected
*/
autoSubmit?: boolean;
}
/**
* ParameterPrompts let the caller prompt for the provided parameters
*/
export type ParameterPrompt<PARAMS> = (
parameters: ParametersPromptObject<PARAMS>,
options?: ParameterPromptOptions,
) => Promise<PARAMS>;
/* tslint:disable:cyclomatic-complexity */
/**
* No-op NoParameterPrompt implementation that never prompts for new parameters
* @constructor
*/
export const NoParameterPrompt: ParameterPrompt<any> = async () => ({});
export const AtomistContinuationMimeType = "application/x-atomist-continuation+json";
/* tslint:disable:cyclomatic-complexity */
/**
* Default ParameterPromptFactory that uses the WebSocket connection to send parameter prompts to the backend.
* @param ctx
*/
export function commandRequestParameterPromptFactory<T>(ctx: HandlerContext): ParameterPrompt<T> {
return async (parameters, options = {}) => {
const trigger = ((ctx as any) as AutomationContextAware).trigger as CommandIncoming;
const existingParameters = trigger.parameters;
const newParameters = _.cloneDeep(parameters);
// Find out if - and if - which parameters are actually missing
let requiredMissing = false;
const params: any = {};
for (const parameter in parameters) {
if (parameters.hasOwnProperty(parameter)) {
const existingParameter = existingParameters.find(p => p.name === parameter);
if (!existingParameter) {
// If required isn't defined it means the parameter is required
if (newParameters[parameter].required || newParameters[parameter].required === undefined) {
requiredMissing = true;
}
} else {
// Do some validation against the rules
const parameterDefinition = newParameters[parameter];
const value = existingParameter.value;
// Force question
if (parameterDefinition.force) {
trigger.parameters = trigger.parameters.filter(p => p.name !== parameter);
requiredMissing = true;
continue;
}
// Verify pattern
if (parameterDefinition.pattern && !!value && !value.match(parameterDefinition.pattern)) {
requiredMissing = true;
continue;
}
// Verify minLength
const minLength = parameterDefinition.minLength || (parameterDefinition as any).min_length;
if (minLength !== undefined && !!value && value.length < minLength) {
requiredMissing = true;
continue;
}
// Verify maxLength
const maxLength = parameterDefinition.maxLength || (parameterDefinition as any).max_length;
if (maxLength !== undefined && !!value && value.length > maxLength) {
requiredMissing = true;
continue;
}
params[parameter] = existingParameter.value;
delete newParameters[parameter];
}
}
}
// If no parameters are missing we can return the already collected parameters
if (!requiredMissing) {
return params;
}
// Set up the thread_ts for this response message
let threadTs: string;
if (options.thread === true && !!trigger.source) {
threadTs = _.get(trigger.source, "slack.message.ts");
} else if (typeof options.thread === "string") {
threadTs = options.thread;
}
const destination = _.cloneDeep(trigger.source);
_.set(destination, "slack.thread_ts", threadTs);
// Create a continuation message using the existing HandlerResponse and mixing in parameters
// and parameter_specs
const response: HandlerResponse & {
parameters: Arg[];
parameter_specs: Parameter[];
question: any;
auto_submit: boolean;
} = {
api_version: "1",
correlation_id: trigger.correlation_id,
team: trigger.team,
command: trigger.command,
source: trigger.source,
destinations: [destination],
parameters: [...(trigger.parameters || []), ...(trigger.mapped_parameters || [])],
auto_submit: !!options.autoSubmit ? options.autoSubmit : undefined,
question: !!options.parameterStyle ? options.parameterStyle.toString() : undefined,
parameter_specs: _.map(newParameters, (v, k) => ({
name: k,
description: v.description,
required: v.required !== undefined ? v.required : true,
pattern: v.pattern ? v.pattern.source : undefined,
valid_input: v.validInput,
max_length: v.maxLength,
min_length: v.minLength,
display_name: v.displayName,
default_value: v.defaultValue,
type: v.type,
})),
content_type: AtomistContinuationMimeType,
};
await ctx.messageClient.respond(response);
throw new CommandListenerExecutionInterruptError(
`Prompting for new parameters: ${_.map(newParameters, (v, k) => k).join(", ")}`,
);
};
}
/* tslint:enable:cyclomatic-complexity */