Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

switchPipe() #69

Merged
merged 7 commits into from
Sep 5, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
82 changes: 80 additions & 2 deletions src/botmation/actions/assembly-lines.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,14 @@ import {
pipeInjects,
wrapValueInPipe,
getInjectsPipeOrEmptyPipe,
createEmptyPipe
createEmptyPipe,
getInjectsPipeValue
} from "../helpers/pipe"
import { PipeValue } from "../types/pipe-value"
import { AbortLineSignal, isAbortLineSignal } from "../types/abort-signal"
import { AbortLineSignal, isAbortLineSignal } from "../types/abort-line-signal"
import { processAbortLineSignal } from "../helpers/abort"
import { isMatchesSignal, MatchesSignal } from "botmation/types/matches-signal"
import { hasAtLeastOneMatch } from "botmation/helpers/matches"

/**
* @description chain() BotAction for running a chain of BotAction's safely and optimized
Expand Down Expand Up @@ -106,6 +109,81 @@ export const pipe =
}
}

/**
* switchPipe is similar to Pipe in that is supports piping, EXCEPT every assembled BotAction gets the same pipe object
* Before each assembled BotAction is ran, the pipe is switched back to whatever is set `toPipe`
* `toPipe` is optional and can be provided by an injected pipe object value (if nothing provided, default is undefined)
*
* AbortLineSignal default abort(1) is ignored until a MatchesSignal is returned by an assembled BotAction, marking that at least one Case has ran
* to break that, you can abort(2+)
* This is to support the classic switch/case/break flow where its switchPipe/pipeCase/abort
* Therefore, if a pipeCase() does run, its returning MatcheSignal will be recognized by switchPipe and then lower the required abort count by 1
* @param toPipe BotAction to resolve and inject as a wrapped Pipe object in EACH assembled BotAction
*/
export const switchPipe =
(toPipe?: BotAction | Exclude<PipeValue, Function>) =>
(...actions: BotAction<PipeValue|AbortLineSignal|MatchesSignal|void>[]): BotAction<any[]|AbortLineSignal|PipeValue> =>
async(page, ...injects) => {
// fallback is injects pipe value
if (!toPipe) {
toPipe = getInjectsPipeValue(injects)
}

// if function, it's an async BotAction function
// resolve it to Pipe the resolved value
if (typeof toPipe === 'function') {
if(injectsHavePipe(injects)) {
toPipe = await toPipe(page, ...injects)
} else {
// simulate pipe
toPipe = await toPipe(page, ...injects, createEmptyPipe())
}

if (isAbortLineSignal(toPipe)) {
return processAbortLineSignal(toPipe)
}
}

// remove pipe from injects if there is one to set the one for all actions
if (injectsHavePipe(injects)) {
injects = injects.slice(0, injects.length - 1)
}

// inject toPipe wrapped in a pipe object
injects.push(wrapValueInPipe(toPipe))

// run the assembled BotAction's with the same Pipe object
let atLeastOneCaseMatched = false
const actionsResults = []
for(const action of actions) {
const resolvedActionResult = await action(page, ...injects)

if (isMatchesSignal(resolvedActionResult) && hasAtLeastOneMatch(resolvedActionResult)) {
atLeastOneCaseMatched = true
} else if (isAbortLineSignal(resolvedActionResult)) {
// switchPipe has unique AbortLineSignal behavior where it takes at least one succesful case()() to reduce
// the necessary count to abort out
const processedAbortSignal = processAbortLineSignal(resolvedActionResult)

// takes abort(1) to abort out of switchPipe if at least one case has matched
if (atLeastOneCaseMatched) {
return processedAbortSignal
} else {
// otherwise it takes at least abort(2) to abort out of the case matching test & line of botaction's
if (isAbortLineSignal(processedAbortSignal)) {
return processAbortLineSignal(processedAbortSignal)
} else {
actionsResults.push(processedAbortSignal)
}
}
} else {
actionsResults.push(resolvedActionResult)
}
}

return actionsResults
}

/**
* @description Efficiently run actions in a pipe or a chain by detecting the higher order assembly line runner
* Detects by checking if injects provided are piped. If piped, runs it in a pipe()() else runs it in a chain()
Expand Down
2 changes: 1 addition & 1 deletion src/botmation/actions/inject.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { BotAction } from "../interfaces"
import { PipeValue } from "../types/pipe-value"
import { assemblyLine } from "./assembly-lines"
import { AbortLineSignal } from "../types/abort-signal"
import { AbortLineSignal } from "../types/abort-line-signal"

/**
* @description Higher-order to set the first few injects for wrapped BotAction's
Expand Down
57 changes: 55 additions & 2 deletions src/botmation/actions/pipe.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
import { BotAction } from "../interfaces"
import { PipeValue } from "../types/pipe-value"
import { getInjectsPipeValue } from "../helpers/pipe"
import { getInjectsPipeValue, injectsHavePipe } from "../helpers/pipe"
import { MatchesSignal } from "../types/matches-signal"
import { AbortLineSignal, Dictionary, isAbortLineSignal } from "../types"
import { pipe } from "./assembly-lines"
import { createMatchesSignal } from "../helpers/matches"

//
// BotAction's Focused on Piping
Expand All @@ -26,4 +30,53 @@ export const pipeValue = <R extends PipeValue = PipeValue>(valueToPipe: R|undefi
/**
* @description Empty the Pipe, which sets the Pipe's value to undefined
*/
export const emptyPipe: BotAction = async () => undefined
export const emptyPipe: BotAction = async () => undefined

/**
* Similar to givenThat except instead of evaluating a BotAction for TRUE, its testing an array of values against the pipe object value for truthy.
* A value can be a function. In this case, the function is treated as a callback, expected to return a truthy expression, is passed in the pipe object's value
* @param valuesToTest
* @return AbortLineSignal|MatchesSignal
* If no matches are found or matches are found, a MatchesSignal is returned
* It is determined if signal has matches by using hasAtLeastOneMatch() helper
* If assembled BotAction aborts(1), it still returns a MatchesSignal with the matches
* If assembled BotAction aborts(2+), it returns a processed AbortLineSignal
*/
export const pipeCase =
(...valuesToTest: PipeValue[]) =>
(...actions: BotAction<PipeValue|AbortLineSignal|void>[]): BotAction<AbortLineSignal|MatchesSignal> =>
async(page, ...injects) => {
// if any of the values matches the injected pipe object value
// then run the assembled actions
if (injectsHavePipe(injects)) {
const pipeObjectValue = getInjectsPipeValue(injects)

const matches: Dictionary = valuesToTest.reduce((foundMatches, value, index) => {
if (typeof value === 'function') {
if (value(pipeObjectValue)) {
(foundMatches as Dictionary)[index] = value
}
} else {
if (value === pipeObjectValue) {
(foundMatches as Dictionary)[index] = value
}
}

return foundMatches
}, {}) as Dictionary

if (Object.keys(matches).length > 0) {
const returnValue:PipeValue|AbortLineSignal = await pipe()(...actions)(page, ...injects)

if (isAbortLineSignal(returnValue)) {
return returnValue // processed by pipe()
} else {
// signal that a case matched
return createMatchesSignal(matches, returnValue)
}
}
}

// no pipe (nothing to test) or the test resulted in no matches
return createMatchesSignal() // empty matches signal (no matches)
}
59 changes: 3 additions & 56 deletions src/botmation/actions/utilities.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,15 +5,13 @@
*/

import { ConditionalBotAction, BotAction } from '../interfaces/bot-actions'
import { pipeInjects, getInjectsPipeValue, removePipe, wrapValueInPipe, injectsHavePipe } from '../helpers/pipe'
import { pipeInjects, getInjectsPipeValue, removePipe, wrapValueInPipe } from '../helpers/pipe'
import { pipeActionOrActions, pipe } from './assembly-lines'
import { logWarning } from '../helpers/console'
import { Collection, isDictionary, Dictionary } from '../types/objects'
import { Collection, isDictionary } from '../types/objects'
import { PipeValue } from '../types/pipe-value'
import { AbortLineSignal, isAbortLineSignal } from '../types/abort-signal'
import { AbortLineSignal, isAbortLineSignal } from '../types/abort-line-signal'
import { processAbortLineSignal } from '../helpers/abort'
import { createMatchesSignal } from '../helpers/matches'
import { MatchesSignal } from '../types/matches-signal'

/**
* @description Higher Order BotAction that accepts a ConditionalBotAction (pipeable, that returns a boolean) and based on what boolean it resolves,
Expand Down Expand Up @@ -211,55 +209,4 @@ export const forAsLong =
return processAbortLineSignal(resolvedCondition)
}
}
}

/**
* Similar to givenThat except instead of evaluating a BotAction for TRUE, its testing an array of values against the pipe object value for truthy.
* A value can be a function. In this case, the function is treated as a callback, expected to return a truthy expression, is passed in the pipe object's value
* @param valuesToTest
* @return AbortLineSignal|MatchesSignal
* If no matches are found or matches are found, a MatchesSignal is returned
* It is determined if signal has matches by using hasAtLeastOneMatch() helper
* If assembled BotAction aborts(1), it still returns a MatchesSignal with the matches
* If assembled BotAction aborts(2+), it returns a processed AbortLineSignal
*/
export const pipeCase =
(...valuesToTest: PipeValue[]) =>
(...actions: BotAction[]): BotAction<AbortLineSignal|MatchesSignal> =>
async(page, ...injects) => {
// if any of the values matches the injected pipe object value
// then run the assembled actions
if (injectsHavePipe(injects)) {
const pipeValue = getInjectsPipeValue(injects)

let matches: Dictionary = {} // key -> values :: index -> value
let matchEvaluation: boolean

valuesToTest.forEach((value, index) => {
// using callbacks to test if pipeValue matches criteria
if (typeof value === "function") {
matchEvaluation = value(pipeValue)
} else {
matchEvaluation = value === pipeValue
}

if (matchEvaluation) {
matches[index] = value
}
})

if (Object.keys(matches).length > 0) {
const returnValue:PipeValue|AbortLineSignal = await pipe()(...actions)(page, ...injects)

if (isAbortLineSignal(returnValue)) {
return returnValue // processed by pipe()
} else {
// signal that a case matched
return createMatchesSignal(matches, returnValue)
}
}
}

// no pipe (nothing to test) or the test resulted in no matches
return createMatchesSignal() // empty matches signal (no matches)
}
2 changes: 1 addition & 1 deletion src/botmation/helpers/abort.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { PipeValue } from "../types/pipe-value"
import { AbortLineSignal } from "../types/abort-signal"
import { AbortLineSignal } from "../types/abort-line-signal"

/**
*
Expand Down
2 changes: 1 addition & 1 deletion src/botmation/interfaces/bot-actions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { Page } from 'puppeteer'
import { BotFilesInjects } from '../types/bot-files-inject'
import { BotIndexedDBInjects } from '../types/bot-indexed-db-inject'
import { ScraperBotInjects } from '../types/scraper-bot-injects'
import { AbortLineSignal } from '../types/abort-signal'
import { AbortLineSignal } from '../types/abort-line-signal'

/**
* @description All BotAction Interfaces
Expand Down
2 changes: 1 addition & 1 deletion src/botmation/types/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,4 @@ export * from './bot-indexed-db-inject'
export * from './database'
export * from './objects'
export * from './pipe-value'
export * from './abort-signal'
export * from './abort-line-signal'
100 changes: 97 additions & 3 deletions src/tests/botmation/actions/assembly-lines.spec.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
import { Page } from 'puppeteer'
import { chainRunner, pipeRunner, pipeActionOrActions, chain, pipe, assemblyLine } from 'botmation/actions/assembly-lines'
import { chainRunner, pipeRunner, pipeActionOrActions, chain, pipe, assemblyLine, switchPipe } from 'botmation/actions/assembly-lines'
import { abort } from 'botmation/actions/abort'
import { AbortLineSignal } from 'botmation/types/abort-signal'
import { createEmptyPipe } from 'botmation/helpers/pipe'
import { AbortLineSignal } from 'botmation/types/abort-line-signal'
import { createEmptyPipe, wrapValueInPipe } from 'botmation/helpers/pipe'
import { createAbortLineSignal } from 'botmation/helpers/abort'
import { pipeCase } from 'botmation/actions/pipe'

/**
* @description Assembly-Lines BotAction's
Expand Down Expand Up @@ -831,4 +833,96 @@ describe('[Botmation] actions/assembly-lines', () => {
expect(mockActionDoesntRun).not.toHaveBeenCalled()
})

it('switchPipe() should pipe in the same value for every assembled BotAction while supporting a BotAction to resolve in a pipe, as the value to be piped in to every assembled BotAction', async() => {
const mockActionReturnsTwo = jest.fn(() => Promise.resolve(2))
const mockActionPassThrough = jest.fn((p, pO) => Promise.resolve(pO.value))

// toPipe is a PipeValue
const toPipeValue = await switchPipe(77)(
mockActionReturnsTwo,
mockActionPassThrough
)(mockPage)

expect(toPipeValue).toEqual([2, 77])
expect(mockActionReturnsTwo).toHaveBeenNthCalledWith(1, {}, wrapValueInPipe(77))
expect(mockActionPassThrough).toHaveBeenNthCalledWith(1, {}, wrapValueInPipe(77))

// toPipe does not exist, PipeValue is obtained from injects (and if missing, it's undefined)
const toPipeValueFromInjectsPipeObject = await switchPipe()(
mockActionReturnsTwo,
mockActionPassThrough
)(mockPage, wrapValueInPipe(55))

expect(toPipeValueFromInjectsPipeObject).toEqual([2, 55])
expect(mockActionReturnsTwo).toHaveBeenNthCalledWith(2, {}, wrapValueInPipe(55))
expect(mockActionPassThrough).toHaveBeenNthCalledWith(2, {}, wrapValueInPipe(55))

// toPipe is a mock BotAction, injects don't have pipe objects
const mockToPipeAction = jest.fn(() => Promise.resolve(99))

const toPipeIsBotAction = await switchPipe(mockToPipeAction)(
mockActionPassThrough,
mockActionReturnsTwo
)(mockPage)

expect(toPipeIsBotAction).toEqual([99, 2])
expect(mockActionReturnsTwo).toHaveBeenNthCalledWith(3, {}, wrapValueInPipe(99))
expect(mockActionPassThrough).toHaveBeenNthCalledWith(3, {}, wrapValueInPipe(99))
expect(mockToPipeAction).toHaveBeenNthCalledWith(1, {}, createEmptyPipe())

// toPipe is a mock BotAction and injects have Pipe object
const toPipeIsBotActionWithInjectedPipeValue = await switchPipe(mockToPipeAction)(
mockActionReturnsTwo,
mockActionPassThrough,
mockActionReturnsTwo
)(mockPage, {brand: 'Pipe', value: 200})

expect(toPipeIsBotActionWithInjectedPipeValue).toEqual([2, 99, 2])
expect(mockActionReturnsTwo).toHaveBeenNthCalledWith(3, {}, wrapValueInPipe(99))
expect(mockActionPassThrough).toHaveBeenNthCalledWith(3, {}, wrapValueInPipe(99))
expect(mockActionReturnsTwo).toHaveBeenNthCalledWith(4, {}, wrapValueInPipe(99))
expect(mockToPipeAction).toHaveBeenNthCalledWith(2, {}, wrapValueInPipe(200))
})

it('switchPipe() supports AbortLineSignal with special behavior where the assembledLines required to abort out of the BotAction is dependent on a MatchesSignal having at least 1 match', async() => {
const mockActionReturnsFive = jest.fn(() => Promise.resolve(5))
const mockActionPassThrough = jest.fn((p, pO) => Promise.resolve(pO.value))

// toPipe is a BotAction that aborts
const toPipeBotActionAborts = await switchPipe(abort(1, 'an abort value'))()(mockPage)
expect(toPipeBotActionAborts).toEqual('an abort value')

// toPipe is Value with assembled BotAction that aborts(1) without a matching case
const toPipeValueAbortOne = await switchPipe(8000)(
mockActionReturnsFive,
abort(1, 'another abort value')
)(mockPage)

expect(toPipeValueAbortOne).toEqual([5, 'another abort value']) // no case matching, takes 2
expect(mockActionReturnsFive).toHaveBeenNthCalledWith(1, {}, wrapValueInPipe(8000))

// toPipe is value with assembled BotAction that aborts(2+) without matching case
const toPipeValueAbortMulti = await switchPipe(10000)(
mockActionPassThrough,
abort(7, 'aborted pipe value to check')
)(mockPage)

expect(toPipeValueAbortMulti).toEqual(createAbortLineSignal(5, 'aborted pipe value to check'))

// toPipe is value with assembled BotAction that aborts(1) BUT with matching case so aborts entirely
const mockActionDoesntRun = jest.fn(() => Promise.resolve())
const toPipeValueAbortOneWithMatchingCase = await switchPipe(300)(
abort(), // no matching case, so the following still runs
pipeCase(300)(
mockActionReturnsFive,
mockActionPassThrough
),
mockActionReturnsFive,
abort(1, 'the case was 300'),
mockActionDoesntRun
)(mockPage)

expect(toPipeValueAbortOneWithMatchingCase).toEqual('the case was 300')
expect(mockActionDoesntRun).not.toHaveBeenCalled()
})
})
Loading