at first you start off by assuming a folder exists.
const functionsDir = flags.functionsDir
then you run into situations where you need to ensure the directory exists before you run the code, so this happens
const functionsDir = ensureFunctionDirExists.call(this, config);
/* get functions dir (and make it if necessary) */
function ensureFunctionDirExists(config) {
const functionsDir = config.build && config.build.functions;
if (!functionsDir) {
this.log(`${NETLIFYDEVLOG} No functions folder specified in netlify.toml`);
process.exit(1);
}
if (!fs.existsSync(functionsDir)) {
this.log(
`${NETLIFYDEVLOG} functions folder ${chalk.magenta.inverse(
functionsDir
)} specified in netlify.toml but folder not found, creating it...`
);
fs.mkdirSync(functionsDir);
this.log(
`${NETLIFYDEVLOG} functions folder ${chalk.magenta.inverse(
functionsDir
)} created`
);
}
return functionsDir;
}
certain things could be better about this:
- i exit when nothing is specified.
- i have to write code to check and make folder if it doesn’t exist
- then i return the dirname
i also dont like this repetitive api…
import chalk from 'chalk'
import { Action, State, Requirement, Config } from './types'
import { prompt } from 'enquirer'
export const createRequiredFieldInConfig = (
/** field name */
fieldName: string,
options: {
/** message to the user to prompt for field */
message?: string
/** default reply if you have one to specify */
initial?: string
} = {}
) => {
// may want to put more validation on the fieldName in future
if (fieldName.includes(' '))
throw new Error(`fieldName ${chalk.yellow(fieldName)} cannot have a space in it`)
const fieldNameMissingRequirement: Requirement = {
name: 'fieldNameMissingRequirement_' + fieldName,
getter: async (config: Config) => config[fieldName],
assert: async (field: any) => field === undefined,
}
const fieldNameExistsRequirement: Requirement = {
name: 'fieldNameExistsRequirement_' + fieldName,
getter: async (config: Config) => config[fieldName],
assert: async (field: any) => field !== undefined,
}
const fieldNameMissingState: State = {
uniqueName: 'fieldNameMissingState_' + fieldName,
requirements: [fieldNameMissingRequirement],
}
const fieldNameExistsState: State = {
uniqueName: 'fieldNameExistsState_' + fieldName,
requirements: [fieldNameExistsRequirement],
}
const fieldNamePromptAction: Action = {
uniqueId: 'fieldNamePromptAction_' + fieldName,
requiredStates: [fieldNameMissingState],
postExecuteState: fieldNameExistsState,
execute: async config => {
const question = {
type: 'input',
name: 'answer',
message: options.message || `Enter ${chalk.yellow(fieldName)}: `,
initial: options.initial,
}
const { answer } = await prompt(question)
config[fieldName] = answer
},
}
return {
fieldNameMissingRequirement,
fieldNameExistsRequirement,
fieldNameMissingState,
fieldNameExistsState,
fieldNamePromptAction,
}
}
so i am getting rid of the idea of requirements.
from
// for now, user is responsible for getting and providing all the configs
// in future we will handle fetching and caching
export type Config = {
[key: string]: any
}
// for Requirements to get values
export type Getter = (config: Config) => Promise<number | string | boolean>
// States and actions can call Asserter with Getter output to check truthiness
export type Asserter = (getterOutput: any) => Promise<Boolean>
// // failed attempt at making overloaded functions
// export function Asserter(getterOutput: string): Promise<Boolean>
// export function Asserter(getterOutput: number): Promise<Boolean>
// export async function Asserter() {
// return false
// }
// a unitary requirement that the user defines
export type Requirement = AsyncRequirement // | SyncRequirement
export type ValidatedRequirement = Requirement & { isValid: boolean }
export type AsyncRequirement = {
name: string
description?: string
getter: Getter
assert: Asserter
}
// a state is just a "dumb" collection of requirements
export type ValidatedState = State & { isValid: boolean }
export type State = {
uniqueName: string
description?: string
requirements: Requirement[]
}
/**
* an Action is the primary unit of the state machine
*
* see field comments for what each does
* */
export type Action = {
uniqueId: string
description?: string
/** "BEFORE": prerequisite states to check */
preStates: State[]
/** "DURING": the meat of the action to execute once */
execute: (config: Config) => Promise<void>
/** "AFTER": a state that should be fulfilled after execute runs */
postState?: State
/** if postExcuteState isn't fulfilled, should we repeat this Action?
*
* default falsy to blame the developer for postExecuteState not being fulfiled */
repeatable?: boolean
/** if requirements aren't fulfiled, what to do */
failure?: Function
}
// /**
// * a Healing Action is designed to explicitly take a state to another state
// *
// * */
// export type HealingAction = {
// uniqueId: string
// description?: string
// /** prerequisite state to check */
// preState: State
// /** the meat of the action to execute once */
// execute: (arg?: any) => Promise<void>
// /** the requirements that will be fulfilled after execute runs */
// postExecuteRequirements?: Requirement[]
// /** if requirements aren't fulfiled, what to do */
// failure?: Function
// }
to
// for now, user is responsible for getting and providing all the configs
// in future we will handle fetching and caching
export type Config = {
[key: string]: any
}
// a state is just a "dumb" collection of requirements
export type ValidatedState = State & { isValid: boolean }
export type State<T = any> = {
stateId: string
description?: string
value: (config: Config) => Promise<T>
assert: (getterOutput: T) => Promise<Boolean>
}
/**
* an Action is the primary unit of the state machine
*
* see field comments for what each does
* */
export type Action<T> = {
actionId: string
description?: string
/** "BEFORE": prerequisite states to check */
preStates: State[]
/** "DURING": the meat of the action to execute once */
execute: (config: Config, value: T) => Promise<void>
/** "AFTER": a state that should be fulfilled after execute runs */
postState?: State
/** if postExcuteState isn't fulfilled, should we repeat this Action?
*
* make truthy to repeat executing until postState is fulfiled
* default falsy to blame the developer for postState not being fulfiled */
repeatable?: boolean
}
this lets me get rid of code like this
/**
* iterate through an array of requirements to check they are valid (by calling .assert)
*
* return array of validatedRequirements that you can easily call `.every` on to check.
* intentionally don't run `every` for you so you can iterate through
*/
const validateRequirements = async (reqs: Requirement[], config: Config) => {
const validator = async (req: Requirement) => req.assert(await req.getter(config))
return await Promise.all(
reqs.map(async req => ({ ...req, isValid: await validator(req) } as ValidatedRequirement))
)
}
i fought really hard to keep the preStates an array… but this is impossible to type:
/**
* an Action is the primary unit of the state machine
*
* see field comments for what each does
* */
export type Action<PreStateType = any, ExecuteType = any, PostStateType = any> = {
actionId: string
description?: string
/** "BEFORE": prerequisite states to check */
preStates: State<PreStateType>[]
/** "DURING": the meat of the action to execute once */
execute: (config: Config, value: ExecuteType) => Promise<void>
/** "AFTER": a state that should be fulfilled after execute runs */
postState?: State<PostStateType>
/** if postExcuteState isn't fulfilled, should we repeat this Action?
*
* make truthy to repeat executing until postState is fulfiled
* default falsy to blame the developer for postState not being fulfiled */
repeatable?: boolean
}
the final outcome
my tests go from this:
import { Action, State, Requirement } from '../../src/types'
import { initStateMachine, processStateMachine } from '../../src'
import { blankConfig } from '../../src/index'
let password = 'oldPassword'
let loginStatus = false
const loggedInRequirement: Requirement = {
name: 'loggedIn',
getter: async () => loginStatus,
assert: async (status: boolean) => status === true,
}
const loggedOutRequirement: Requirement = {
name: 'loggedOut',
getter: async () => loginStatus,
assert: async (status: boolean) => status === false,
}
export const loggedInState: State = {
uniqueName: 'loggedIn',
requirements: [loggedInRequirement],
}
export const loggedOutState: State = {
uniqueName: 'loggedOut',
requirements: [loggedOutRequirement],
}
export const loginAction: Action = {
uniqueId: 'loginAction',
requiredStates: [loggedOutState],
postExecuteState: loggedInState,
execute: async () => {
loginStatus = true
},
}
export const logoutAction: Action = {
uniqueId: 'logoutAction',
requiredStates: [loggedInState],
postExecuteState: loggedOutState,
execute: async () => {
loginStatus = false
},
}
export const changePasswordAction: Action = {
uniqueId: 'changePassword',
requiredStates: [loggedInState],
postExecuteState: loggedInState,
execute: async () => {
password = 'newPassword'
},
}
describe('basic action', () => {
it('self heals', async () => {
initStateMachine([loginAction, logoutAction, changePasswordAction])
expect(password).toEqual('oldPassword')
expect(loginStatus).toEqual(false) // not logged in
await processStateMachine(changePasswordAction, blankConfig)
expect(password).toEqual('newPassword')
expect(loginStatus).toEqual(true) // logged in
})
})
to this:
import { Action, State } from '../../src/types'
import { initStateMachine, processStateMachine } from '../../src'
import { blankConfig } from '../../src/index'
let password = 'oldPassword'
let loginStatus = false
export const loggedInState: State = {
stateId: 'loggedIn',
getValue: async () => loginStatus,
assert: async (status: boolean) => status === true,
}
export const loggedOutState: State = {
stateId: 'loggedOut',
getValue: async () => loginStatus,
assert: async (status: boolean) => status === false,
}
export const loginAction: Action = {
actionId: 'loginAction',
beforeState: loggedOutState,
afterState: loggedInState,
execute: async () => {
// console.log('logging in')
loginStatus = true
},
}
export const logoutAction: Action = {
actionId: 'logoutAction',
beforeState: loggedInState,
afterState: loggedOutState,
execute: async () => {
// console.log('logging out')
loginStatus = false
},
}
export const changePasswordAction: Action = {
actionId: 'changePassword',
beforeState: loggedInState,
afterState: loggedInState,
execute: async () => {
password = 'newPassword'
},
}
describe('basic action', () => {
it('self heals', async () => {
initStateMachine([loginAction, logoutAction, changePasswordAction])
expect(password).toEqual('oldPassword')
expect(loginStatus).toEqual(false) // not logged in
await processStateMachine(changePasswordAction, blankConfig)
expect(password).toEqual('newPassword')
expect(loginStatus).toEqual(true) // logged in
})
})