@atomist/rug
Version:
TypeScript model for Atomist Rugs, see http://docs.atomist.com/
357 lines (288 loc) • 10.7 kB
text/typescript
import {
GraphNode,
Match,
PathExpression,
PathExpressionEngine,
TreeNode,
} from "../tree/PathExpression";
import { GitProjectLoader } from "./GitProjectLoader";
import { Parameter } from "./RugOperation";
export interface RugCoordinate {
readonly name: string;
readonly group: string;
readonly artifact: string;
}
type InstructionKind = "generate" | "edit" | "execute" | "respond" | "command";
export interface Instruction<T extends InstructionKind> {
readonly name: string | RugCoordinate;
readonly parameters?: {};
readonly kind: T;
}
export class EventRespondable<T extends Edit | Generate | Execute> {
public instruction: T;
public onSuccess?: EventPlan | EventMessage | Respond;
public onError?: EventPlan | EventMessage | Respond;
}
export class CommandRespondable<T extends Edit | Generate | Execute | Command> {
public instruction: T;
public onSuccess?: CommandPlan | CommandMessage | Respond;
public onError?: CommandPlan | CommandMessage | Respond;
}
export class Presentable<T extends InstructionKind> {
public instruction: Instruction<T> | PresentableGenerate | PresentableEdit;
public label?: string;
public id?: string;
}
export class Identifiable<T extends InstructionKind> {
public instruction: Instruction<T> | PresentableGenerate | PresentableEdit;
public parameterName?: string;
public id?: string;
}
// Location to a project.
// in the future, we could add things like github urls, orgs etc.
// tslint:disable-next-line:no-empty-interface
interface ProjectReference { }
export interface ProjectInstruction<T extends InstructionKind> extends Instruction<T> {
project: string | ProjectReference;
}
type EditorTargetKind = "direct" | "github-pull-request" | "github-branch";
/**
* How should the `edit` be applied? PR details etc.
*/
export interface EditorTarget<T extends EditorTargetKind> {
kind: T;
baseBranch: string;
}
/**
* Get an editor instruction to run a GitHub Pull Request
*/
export class GitHubPullRequest implements EditorTarget<"github-pull-request"> {
public kind: "github-pull-request" = "github-pull-request";
public title?: string; // title of the PR
public body?: string; // body of the PR (defaults to editor changelog)
public headBranch?: string; // name of PR source branch (default auto-generated)
constructor(public baseBranch: string = "master") { }
}
/**
* Get an editor instruction to run on a new GitHub branch
*/
export class GitHubBranch implements EditorTarget<"github-branch"> {
public kind: "github-branch" = "github-branch";
constructor(public baseBranch: string = "master", public headBranch?: string) { }
}
// tslint:disable-next-line:no-empty-interface
export interface Edit extends ProjectInstruction<"edit"> {
target?: EditorTarget<EditorTargetKind>;
commitMessage?: string;
}
// extends ProjectInstruction because we need to know the project name
// tslint:disable-next-line:no-empty-interface
export interface Generate extends ProjectInstruction<"generate"> { }
// because in a message, we may not know project name yet
export interface PresentableGenerate extends Instruction<"generate"> {
project?: string | ProjectReference;
}
// because in a message, we may not know project name yet
export interface PresentableEdit extends Instruction<"edit"> {
project?: string | ProjectReference;
}
// tslint:disable-next-line:no-empty-interface
export interface Execute extends Instruction<"execute"> { }
// tslint:disable-next-line:no-empty-interface
export interface Respond extends Instruction<"respond"> { }
// tslint:disable-next-line:no-empty-interface
export interface Command extends Instruction<"command"> { }
export interface HandleCommand {
handle(ctx: HandlerContext): CommandPlan;
}
export interface HandleEvent<R extends GraphNode, M extends GraphNode> {
handle(root: Match<R, M>): EventPlan;
}
export interface HandleResponse<T> {
handle(response: Response<T>, ctx?: HandlerContext): EventPlan | CommandPlan;
}
/**
* Context available to all handlers. Unique to a team.
* Exposes a context root from which queries can be run,
* using the given PathExpressionEngine
*/
export interface HandlerContext {
/**
* Id of the team we're working on behalf of
*/
teamId: string;
pathExpressionEngine: PathExpressionEngine;
/**
* The root of this team's context. Allows execution
* of path expressions.
*/
contextRoot: GraphNode;
/**
* RepoResolver to use in loading repositories.
*/
gitProjectLoader: GitProjectLoader;
}
export enum Status {
failure,
success,
}
export interface Response<T> {
msg: string;
code: number;
status: Status;
body: T;
}
type Respondable = EventRespondable<any> | CommandRespondable<any>;
/**
* A bunch of stuff to do asynchronously
* PlanMessages got to the bot.
* ImmediatelyRunnables are run straight away
*/
export abstract class Plan {
public instructions: Respondable[] = [];
public messages: PlanMessage[] = [];
public add?(thing: Respondable | PlanMessage): this {
if (thing instanceof ResponseMessage || thing instanceof DirectedMessage || thing instanceof LifecycleMessage) {
this.messages.push(thing);
} else {
this.instructions.push(thing);
}
return this;
}
}
type EventMessage = LifecycleMessage | DirectedMessage;
type CommandMessage = ResponseMessage | DirectedMessage;
/**
* For returning from Event Handlers
*/
export class EventPlan extends Plan {
public static ofMessage(m: EventMessage): EventPlan {
return new EventPlan().add(m);
}
public add?(msg: EventMessage | EventRespondable<any>): this {
return super.add(msg);
}
}
/**
* Plans returned from Command Handlers
*/
export class CommandPlan extends Plan {
public static ofMessage(m: CommandMessage): CommandPlan {
return new CommandPlan().add(m);
}
public add?(msg: CommandMessage | CommandRespondable<any>): this {
return super.add(msg);
}
}
export type MessageMimeType = "application/x-atomist-slack+json" | "text/plain";
export abstract class MessageMimeTypes {
public static SLACK_JSON: MessageMimeType = "application/x-atomist-slack+json";
public static PLAIN_TEXT: MessageMimeType = "text/plain";
}
type MessageKind = "response" | "lifecycle" | "directed";
interface Message<T extends MessageKind> {
kind: T;
}
export abstract class LocallyRenderedMessage<T extends MessageKind> implements Message<T> {
public kind: T;
public usernames?: string[] = [];
public channelNames?: string[] = [];
public contentType: MessageMimeType = MessageMimeTypes.PLAIN_TEXT;
public body: string;
public instructions?: Array<Identifiable<any>> = [];
public addAddress?(address: MessageAddress): this {
if (address instanceof UserAddress) {
this.usernames.push(address.username);
} else {
this.channelNames.push(address.channelName);
}
return this;
}
public addAction?(instruction: Identifiable<any>): this {
this.instructions.push(instruction);
return this;
}
}
/**
* Represents the response to the bot from a command
*/
export class ResponseMessage extends LocallyRenderedMessage<"response"> {
public kind: "response" = "response";
constructor(body: string, contentType?: MessageMimeType) {
super();
this.body = body;
if (contentType) {
this.contentType = contentType;
}
}
}
export class UserAddress {
constructor(public username: string) { }
}
export class ChannelAddress {
constructor(public channelName: string) { }
}
export type MessageAddress = UserAddress | ChannelAddress;
/**
* Uncorrelated message to the bot
*/
export class DirectedMessage extends LocallyRenderedMessage<"directed"> {
public kind: "directed" = "directed";
constructor(body: string, address: MessageAddress, contentType?: MessageMimeType) {
super();
this.body = body;
if (contentType) {
this.contentType = contentType;
}
this.addAddress(address);
}
}
/**
* Message that can be re-written in the bot
*/
export class UpdatableMessage extends DirectedMessage {
public id: string;
public timestamp: string;
// Time after which the message will get re-posted instead of re-written
public ttl?: string;
// If message with id doesn't exist update_only wouldn't create it
public post?: "always" | "update_only";
constructor(id: string, body: string, address: MessageAddress, contentType?: MessageMimeType) {
super(body, address, contentType);
this.id = id;
}
}
/**
* Correlated, updatable messages to the bot
*/
export class LifecycleMessage implements Message<"lifecycle"> {
public kind: "lifecycle" = "lifecycle";
public node: GraphNode;
public instructions?: Array<Presentable<any>> = [];
public lifecycleId: string;
constructor(node: GraphNode, lifecycleId: string) {
this.node = node;
this.lifecycleId = lifecycleId;
}
public addAction?(instruction: Presentable<any>): this {
this.instructions.push(instruction);
return this;
}
}
export type PlanMessage = ResponseMessage | DirectedMessage | LifecycleMessage;
export abstract class MappedParameters {
public static readonly CORRELATION_ID: string = "atomist://correlation_id";
// GITHUB_REPO_OWNER is deprecated; instead use GITHUB_OWNER
public static readonly GITHUB_REPO_OWNER: string = "atomist://github/repository/owner";
public static readonly GITHUB_OWNER: string = "atomist://github/repository/owner";
public static readonly GITHUB_REPOSITORY: string = "atomist://github/repository";
public static readonly GITHUB_WEBHOOK_URL: string = "atomist://github_webhook_url";
public static readonly GITHUB_URL: string = "atomist://github_url";
public static readonly GITHUB_API_URL: string = "atomist://github_api_url";
public static readonly GITHUB_DEFAULT_REPO_VISIBILITY: string = "atomist://github/default_repo_visibility";
public static readonly SLACK_CHANNEL: string = "atomist://slack/channel";
public static readonly SLACK_CHANNEL_NAME: string = "atomist://slack/channel_name";
public static readonly SLACK_TEAM: string = "atomist://slack/team";
public static readonly SLACK_USER: string = "atomist://slack/user";
public static readonly SLACK_USER_NAME: string = "atomist://slack/user_name";
}