1
0
Fork 0

Refactor base middlewares

pull/1562/head
Nikolai Laevskii 2023-10-12 06:58:22 +02:00
parent 36629a3962
commit b45c9d3801
5 changed files with 146 additions and 154 deletions

View File

@ -1,9 +1,6 @@
import * as exec from '@actions/exec'
import {CommandRunnerBase} from './core'
import {
ErrorMatcher,
ExitCodeMatcher,
OutputMatcher,
failAction,
matchEvent,
matchExitCode,
@ -16,7 +13,10 @@ import {
CommandRunnerActionType,
CommandRunnerEventTypeExtended,
CommandRunnerMiddleware,
CommandRunnerOptions
CommandRunnerOptions,
ErrorMatcher,
ExitCodeMatcher,
OutputMatcher
} from './types'
const commandRunnerActions = {

View File

@ -3,39 +3,12 @@ import {StringDecoder} from 'string_decoder'
import {
CommandRunnerContext,
CommandRunnerMiddleware,
CommandRunnerMiddlewarePromisified,
CommandRunnerOptions
} from './types'
export const promisifyCommandRunnerMiddleware =
(middleware: CommandRunnerMiddleware): CommandRunnerMiddlewarePromisified =>
async (ctx, next) => {
return Promise.resolve(middleware(ctx, next))
}
export const composeCommandRunnerMiddleware =
(middleware: CommandRunnerMiddlewarePromisified[]) =>
async (context: CommandRunnerContext, nextGlobal: () => Promise<void>) => {
let index = 0
const nextLocal = async (): Promise<void> => {
if (index < middleware.length) {
const currentMiddleware = middleware[index++]
if (middleware === undefined) {
return
}
await currentMiddleware(context, nextLocal)
}
await nextGlobal()
}
await nextLocal()
}
import {PromisifiedFn, promisifyFn} from './utils'
export class CommandRunnerBase {
private middleware: CommandRunnerMiddlewarePromisified[] = []
private middleware: PromisifiedFn<CommandRunnerMiddleware>[] = []
constructor(
private commandLine: string,
@ -45,7 +18,7 @@ export class CommandRunnerBase {
) {}
use(middleware: CommandRunnerMiddleware): this {
this.middleware.push(promisifyCommandRunnerMiddleware(middleware))
this.middleware.push(promisifyFn(middleware))
return this
}
@ -104,8 +77,36 @@ export class CommandRunnerBase {
}
const next = async (): Promise<void> => Promise.resolve()
await composeCommandRunnerMiddleware(this.middleware)(context, next)
await composeMiddleware(this.middleware)(context, next)
return context
}
}
export function composeMiddleware(
middleware: PromisifiedFn<CommandRunnerMiddleware>[]
): PromisifiedFn<CommandRunnerMiddleware> {
middleware = middleware.map(mw => promisifyFn(mw))
return async (
context: CommandRunnerContext,
nextGlobal: () => Promise<void>
) => {
let index = 0
const nextLocal = async (): Promise<void> => {
if (index < middleware.length) {
const currentMiddleware = middleware[index++]
if (middleware === undefined) {
return
}
await currentMiddleware(context, nextLocal)
}
await nextGlobal()
}
await nextLocal()
}
}

View File

@ -1,15 +1,16 @@
import * as core from '@actions/core'
import {
CommandRunnerAction,
CommandRunnerContext,
CommandRunnerEventType,
CommandRunnerEventTypeExtended,
CommandRunnerMiddleware,
CommandRunnerMiddlewarePromisified
ErrorMatcher,
ExitCodeMatcher,
OutputMatcher
} from './types'
import {
composeCommandRunnerMiddleware,
promisifyCommandRunnerMiddleware
} from './core'
import {composeMiddleware} from './core'
import {gte, gt, lte, lt, eq, PromisifiedFn} from './utils'
const getEventTypesFromContext = (
ctx: CommandRunnerContext
@ -39,16 +40,15 @@ const getEventTypesFromContext = (
return [...eventTypes]
}
type CommandRunnerAction = (
message?:
| string
| ((ctx: CommandRunnerContext, events: CommandRunnerEventType[]) => string)
) => CommandRunnerMiddlewarePromisified
/**
* Basic middleware
*/
/** Calls next middleware */
export const passThrough: () => PromisifiedFn<CommandRunnerMiddleware> =
() => async (_, next) =>
next()
/**
* Fails Github Action with the given message or with a default one depending on execution conditions.
*/
@ -202,31 +202,6 @@ export const produceLog: CommandRunnerAction = message => async (ctx, next) => {
next()
}
/**
* Filtering middleware
*/
/** Calls next middleware */
export const passThrough: () => CommandRunnerMiddlewarePromisified =
() => async (_, next) =>
next()
/**
* Either calls next middleware or not depending on the result of the given condition.
*/
export const filter: (
shouldPass:
| boolean
| ((ctx: CommandRunnerContext) => boolean | Promise<boolean>)
) => CommandRunnerMiddlewarePromisified = shouldPass => async (ctx, next) => {
if (typeof shouldPass === 'function') {
if (await shouldPass(ctx)) {
next()
return
}
}
}
/**
* Will call passed middleware if matching event has occured.
* Will call the next middleware otherwise.
@ -234,13 +209,9 @@ export const filter: (
export const matchEvent = (
eventType: CommandRunnerEventTypeExtended | CommandRunnerEventTypeExtended[],
middleware?: CommandRunnerMiddleware[]
): CommandRunnerMiddlewarePromisified => {
if (!middleware?.length) {
middleware = [passThrough()]
}
const composedMiddleware = composeCommandRunnerMiddleware(
middleware.map(mw => promisifyCommandRunnerMiddleware(mw))
): PromisifiedFn<CommandRunnerMiddleware> => {
const composedMiddleware = composeMiddleware(
!middleware?.length ? [passThrough()] : middleware
)
const expectedEventsPositiveArray = (
@ -283,8 +254,6 @@ export const matchEvent = (
}
}
export type OutputMatcher = RegExp | string | ((output: string) => boolean)
/**
* Will call passed middleware if matching event has occured.
* Will call the next middleware otherwise.
@ -292,13 +261,9 @@ export type OutputMatcher = RegExp | string | ((output: string) => boolean)
export const matchOutput = (
matcher: OutputMatcher,
middleware?: CommandRunnerMiddleware[]
): CommandRunnerMiddlewarePromisified => {
if (!middleware?.length) {
middleware = [passThrough()]
}
const composedMiddleware = composeCommandRunnerMiddleware(
middleware.map(mw => promisifyCommandRunnerMiddleware(mw))
): PromisifiedFn<CommandRunnerMiddleware> => {
const composedMiddleware = composeMiddleware(
!middleware?.length ? [passThrough()] : middleware
)
return async (ctx, next) => {
@ -328,30 +293,9 @@ export const matchOutput = (
}
}
export type ExitCodeMatcher = string | number
const removeWhitespaces = (str: string): string => str.replace(/\s/g, '')
const lte =
(a: number) =>
(b: number): boolean =>
b <= a
const gte =
(a: number) =>
(b: number): boolean =>
b >= a
const lt =
(a: number) =>
(b: number): boolean =>
b < a
const gt =
(a: number) =>
(b: number): boolean =>
b > a
const eq =
(a: number) =>
(b: number): boolean =>
b === a
const matchers = {
const MATCHERS = {
'>=': gte,
'>': gt,
'<=': lte,
@ -359,11 +303,9 @@ const matchers = {
'=': eq
} as const
const removeWhitespaces = (str: string): string => str.replace(/\s/g, '')
const parseExitCodeMatcher = (
code: ExitCodeMatcher
): [keyof typeof matchers, number] => {
): [keyof typeof MATCHERS, number] => {
if (typeof code === 'number') {
return ['=', code]
}
@ -382,14 +324,7 @@ const parseExitCodeMatcher = (
}
const [, operator, number] = match
return [operator as keyof typeof matchers, parseInt(number)]
}
const matcherToMatcherFn = (
matcher: ExitCodeMatcher
): ((exitCode: number) => boolean) => {
const [operator, number] = parseExitCodeMatcher(matcher)
return matchers[operator](number)
return [operator as keyof typeof MATCHERS, parseInt(number)]
}
/**
@ -399,20 +334,17 @@ const matcherToMatcherFn = (
export const matchExitCode = (
code: ExitCodeMatcher,
middleware?: CommandRunnerMiddleware[]
): CommandRunnerMiddlewarePromisified => {
const matcher = matcherToMatcherFn(code)
): PromisifiedFn<CommandRunnerMiddleware> => {
const [operator, number] = parseExitCodeMatcher(code)
const matcherFn = MATCHERS[operator](number)
if (!middleware?.length) {
middleware = [passThrough()]
}
const composedMiddleware = composeCommandRunnerMiddleware(
middleware.map(mw => promisifyCommandRunnerMiddleware(mw))
const composedMiddleware = composeMiddleware(
!middleware?.length ? [passThrough()] : middleware
)
return async (ctx, next) => {
// if exit code is undefined, NaN will not match anything
if (matcher(ctx.exitCode ?? NaN)) {
if (matcherFn(ctx.exitCode ?? NaN)) {
composedMiddleware(ctx, next)
return
}
@ -421,25 +353,14 @@ export const matchExitCode = (
}
}
export type ErrorMatcher =
| RegExp
| string
| ((error: {
type: 'stderr' | 'execerr'
error: Error | null
message: string
}) => boolean)
export const matchSpecificError = (
matcher: ErrorMatcher,
middleware?: CommandRunnerMiddleware[]
): CommandRunnerMiddlewarePromisified => {
if (!middleware?.length) {
middleware = [passThrough()]
}
const composedMiddleware = composeCommandRunnerMiddleware(
middleware.map(mw => promisifyCommandRunnerMiddleware(mw))
middleware?:
| CommandRunnerMiddleware[]
| PromisifiedFn<CommandRunnerMiddleware>[]
): PromisifiedFn<CommandRunnerMiddleware> => {
const composedMiddleware = composeMiddleware(
!middleware?.length ? [passThrough()] : middleware
)
return async (ctx, next) => {

View File

@ -1,4 +1,5 @@
import * as exec from '@actions/exec'
import {PromisifiedFn} from './utils'
/* CommandRunner core */
@ -15,20 +16,22 @@ export interface CommandRunnerContext {
exitCode: number | null
}
/* Middlewares as used internally in CommandRunner */
export type CommandRunnerMiddlewarePromisified = (
ctx: CommandRunnerContext,
next: () => Promise<void>
) => Promise<void>
/* Middlewares as used by the user */
export type CommandRunnerMiddleware = (
type _CommandRunnerMiddleware = (
ctx: CommandRunnerContext,
next: () => Promise<void>
) => void | Promise<void>
export type CommandRunnerMiddleware = PromisifiedFn<_CommandRunnerMiddleware>
/* Command runner events handling and command runner actions */
export type CommandRunnerAction = (
message?:
| string
| ((ctx: CommandRunnerContext, events: CommandRunnerEventType[]) => string)
) => PromisifiedFn<CommandRunnerMiddleware>
/* Command runner default actions types on which preset middleware exists */
export type CommandRunnerActionType = 'throw' | 'fail' | 'log'
@ -49,3 +52,18 @@ export type CommandRunnerOptions = Omit<
exec.ExecOptions,
'failOnStdErr' | 'ignoreReturnCode'
>
/* Matchers */
export type OutputMatcher = RegExp | string | ((output: string) => boolean)
export type ExitCodeMatcher = string | number
export type ErrorMatcher =
| RegExp
| string
| ((error: {
type: 'stderr' | 'execerr'
error: Error | null
message: string
}) => boolean)

View File

@ -0,0 +1,52 @@
/**
* Promises
*/
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export type PromisifiedFn<T extends (...args: any[]) => any> = (
...args: Parameters<T>
) => ReturnType<T> extends Promise<unknown>
? ReturnType<T>
: Promise<ReturnType<T>>
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export const promisifyFn = <T extends (...args: any[]) => any>(
fn: T
): PromisifiedFn<T> => {
const result = async (...args: Parameters<T>): Promise<unknown> => {
return new Promise((resolve, reject) => {
try {
resolve(fn(...args))
} catch (error) {
reject(error)
}
})
}
return result as PromisifiedFn<T>
}
/**
* Comparators
*/
export const lte =
(a: number) =>
(b: number): boolean =>
b <= a
export const gte =
(a: number) =>
(b: number): boolean =>
b >= a
export const lt =
(a: number) =>
(b: number): boolean =>
b < a
export const gt =
(a: number) =>
(b: number): boolean =>
b > a
export const eq =
(a: number) =>
(b: number): boolean =>
b === a