UNPKG

@atomist/rug

Version:

TypeScript model for Atomist Rugs, see http://docs.atomist.com/

357 lines (288 loc) 10.7 kB
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"; }