Skip to content

Instantly share code, notes, and snippets.

@psilospore
Created February 4, 2021 15:55
Show Gist options
  • Save psilospore/de2ee900e5f57bb6854dfb2bfd49f8db to your computer and use it in GitHub Desktop.
Save psilospore/de2ee900e5f57bb6854dfb2bfd49f8db to your computer and use it in GitHub Desktop.
Generate semi-realistic mock data from an io-ts codec
import { default as faker } from 'faker'
import { fold, none, Option, some } from 'fp-ts/lib/Option'
import { pipe } from 'fp-ts/lib/pipeable'
import { keys } from 'fp-ts/lib/Record'
import { evalState, State } from 'fp-ts/lib/State'
import * as t from 'io-ts'
import { NumberFromString } from 'io-ts-types/lib/NumberFromString'
import { Moment } from 'moment'
// TODO don't use lodash
import {
flow,
includes,
isArray,
isNil,
map,
mergeWith,
reduce,
times,
toLower,
} from 'lodash'
/**
* This works kinda like intermock expect it uses io-ts.
* I attempted to use intermock and it didn't work so well in our codebase.
* This was created when I was first learning fp-ts and probably could be better.
* There might be other more current solutions but this might be useful as a reference.
*/
/**
* TODO Intermock seems to have a way to detect the type name and choose an appropriate faker function
* For now just statically check
* I could match on available method names
*/
const genStr = (maybeProp: Option<string>): string => {
return pipe(
maybeProp,
fold(
() => faker.lorem.word(),
(prop) => {
if (includes(toLower(prop), 'email')) {
return faker.internet.email()
} else if (includes(toLower(prop), 'firstname')) {
return faker.name.firstName()
} else if (includes(toLower(prop), 'lastname')) {
return faker.name.lastName()
} else {
return faker.lorem.word()
}
},
),
)
}
type GenState = {
propName: Option<string> //The name of the key. For a T.InterfaceType it needs to pass down the key of the child.
}
const EmptyGenState: GenState = {
propName: none,
}
const initialState = EmptyGenState
const consistentObjectTraversal = flow(toPairs, sortBy(0), fromPairs)
//Credit: https://github.com/giogonzo/fast-check-io-ts/blob/master/src/index.ts
const getProps = (
//eslint-disable-next-line @typescript-eslint/no-explicit-any
codec: t.InterfaceType<any> | t.ExactType<any> | t.PartialType<any>,
): t.Props => {
switch (codec._tag) {
case 'InterfaceType':
case 'PartialType':
return consistentObjectTraversal(codec.props)
case 'ExactType':
return getProps(codec.type)
}
}
const emptyState = <A>(nextValue?: unknown): [A | undefined, GenState] => [
nextValue as A | undefined,
EmptyGenState,
]
/**
* Generates mock data from a io-ts codec using Faker.
* Detect property name and generate some sensical mock data.
*
* genFromCodec(io.type({
* name: io.string,
* email: io.string
* }))
*
* {
* "name": "Rerence McKenna",
* "email": "greengoblins@zenithdimension.com"
* }
*
* TODO I want to not hardcode the generator (e.g. remove explicit calls to faker)
*
* TODO use EitherT (actually there's a StateEither)
* TODO use NonEmptyList<Error> with Error Accumulation vs short circuiting
* @param codec Codec to generate from
*/
const typedMockS = <A, O>(
codec: t.Type<A, O>,
): State<GenState, A | undefined> => {
return ({ propName }: GenState): [A | undefined, GenState] => {
if (codec instanceof t.StringType) {
//Where StringType extends Type<string> so A is string
return emptyState(genStr(propName))
} else if (codec instanceof t.NumberType) {
return emptyState(faker.random.number()) //TODO pick appropriate faker function
} else if (codec instanceof t.BooleanType) {
return emptyState(faker.random.boolean())
} else if (codec instanceof t.UnknownType) {
return emptyState(faker.lorem.word())
} else if (codec instanceof t.LiteralType) {
return emptyState(codec.value)
} else if (codec instanceof t.ReadonlyType) {
return emptyState(
evalState(typedMockS(codec.type), {
propName: none,
}),
)
} else if (
codec instanceof t.VoidType ||
codec instanceof t.UndefinedType
) {
return emptyState()
} else if (codec instanceof t.NullType) {
// eslint-disable-next-line unicorn/no-null
return emptyState(null)
} else if (
codec instanceof t.ArrayType ||
codec instanceof t.ReadonlyArrayType
) {
const nextArr = times(faker.random.number({ min: 1, max: 4 }), () =>
evalState(typedMockS(codec.type), { propName }),
)
return emptyState(nextArr)
} else if (codec instanceof t.KeyofType) {
const consistentKeys = keys(consistentObjectTraversal(codec.keys))
const randomKey = faker.random.arrayElement(consistentKeys)
return emptyState(randomKey)
} else if (
codec instanceof t.InterfaceType ||
codec instanceof t.PartialType ||
codec instanceof t.ExactType
) {
//Object like codec
const keyValueArr = map(
Object.entries(getProps(codec)),
([propName, propCodec]) => ({
[propName]: evalState(typedMockS(propCodec), {
propName: some(propName),
}),
}),
)
const record = reduce(keyValueArr, (acc, val) => ({ ...acc, ...val }))
return emptyState<A>(record)
} else if (codec instanceof t.UnionType) {
const randomElement = faker.random.arrayElement(codec.types)
const oneOfUnion = evalState(
typedMockS(randomElement as t.Type<unknown>),
{
propName,
},
)
return emptyState(oneOfUnion)
} else if (codec instanceof t.IntersectionType) {
//mocked value for each type in intersection
// eslint-disable-next-line @typescript-eslint/ban-types
const intersectionEnumerated: ReadonlyArray<Object | undefined> = map(
codec.types,
(type) =>
evalState(typedMockS(type), {
propName,
}),
)
const intersectionMerged = reduce(
intersectionEnumerated,
(acc, val) => ({ ...acc, ...val }),
{},
)
return emptyState(intersectionMerged)
} //Custom types TODO this is hardcoded into this function
else if (codec.name === CustomBrandedTypeMoney.name) {
return emptyState(`${faker.finance.amount()}`)
} else {
console.error('Codec not supported yet', codec)
//left(new Error("TODO ${codec.name} not yet supported")) //TODO EitherT
return [(undefined as unknown) as A, EmptyGenState]
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment