Created
February 4, 2021 15:55
-
-
Save psilospore/de2ee900e5f57bb6854dfb2bfd49f8db to your computer and use it in GitHub Desktop.
Generate semi-realistic mock data from an io-ts codec
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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