Created
June 26, 2020 22:05
-
-
Save dyerw/4f3bcc0c04e5ceda54e7d0f1a23d0a7f to your computer and use it in GitHub Desktop.
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 * as t from "io-ts"; | |
import * as R from "fp-ts/lib/Record"; | |
type AvroStringSchema = "string"; | |
type AvroNullSchema = "null"; | |
type AvroFieldSchema = { name: string; type: AvroSchema }; | |
type AvroRecordSchema = { | |
type: "record"; | |
namespace: string; | |
name: string; | |
fields: AvroFieldSchema[]; | |
}; | |
type AvroUnionSchema = AvroSchema[]; | |
type AvroSchema = | |
| AvroStringSchema | |
| AvroNullSchema | |
| AvroRecordSchema | |
| AvroUnionSchema | |
| "boolean" | |
| "int" | |
| "long" | |
| "float" | |
| "double"; | |
interface AvroT<A> { | |
codec: t.Type<A>; | |
schema: AvroSchema; | |
} | |
type AvroMixed = AvroT<any>; | |
type TypeOf<C extends AvroMixed> = t.TypeOf<C["codec"]>; | |
const avUnion = <TS extends [AvroMixed, AvroMixed, ...Array<AvroMixed>]>( | |
ts: TS | |
): AvroT<TypeOf<TS[number]>> => { | |
const codecs = ts.map(getCodec); | |
return { | |
codec: t.union([codecs[0], codecs[1], ...codecs.slice(1)]), | |
schema: ts.map((t) => t.schema), | |
}; | |
}; | |
const avString: AvroT<string> = { | |
codec: t.string, | |
schema: "string", | |
}; | |
const avNull: AvroT<null> = { codec: t.null, schema: "null" }; | |
const avBoolean: AvroT<boolean> = { codec: t.boolean, schema: "boolean" }; | |
const avInt: AvroT<number> = { codec: t.number, schema: "int" }; | |
const avLong: AvroT<number> = { codec: t.number, schema: "long" }; | |
const avFloat: AvroT<number> = { codec: t.number, schema: "float" }; | |
const avDouble: AvroT<number> = { codec: t.number, schema: "double" }; | |
interface RecordParams<A> { | |
namespace: string; | |
name: string; | |
fields: { [K in keyof A]: AvroT<A[K]> }; | |
} | |
const mapValues = <A>( | |
obj: { [K in keyof A]: any }, | |
f: (b: any) => any | |
): { [K in keyof A]: any } => { | |
const entries: [string, any][] = Object.entries(obj); | |
const mappedEntries: [string, any][] = entries.map(([k, v]) => [k, f(v)]); | |
return mappedEntries.reduce((prev, [k, v]) => ({ ...prev, [k]: v }), {}) as { | |
[K in keyof A]: any; | |
}; | |
}; | |
const getCodec = <C>(avroT: AvroT<C>): t.Type<C> => { | |
return avroT.codec; | |
}; | |
/*type MyRecord = { | |
foo1: string; | |
foo2: null; | |
foo3: string; | |
} | |
t.Type<MyRecord> */ | |
const getRecordCodec = <A>(params: RecordParams<A>): t.Type<A> => { | |
return t.type(mapValues(params.fields, getCodec) as any); | |
}; | |
const getRecordSchema = <A>(params: RecordParams<A>): AvroRecordSchema => { | |
const fieldSchemas: AvroFieldSchema[] = Object.entries( | |
params.fields | |
).map(([k, v]) => ({ name: k, type: (v as AvroT<any>).schema })); | |
return { | |
type: "record", | |
name: params.name, | |
namespace: params.namespace, | |
fields: fieldSchemas, | |
}; | |
}; | |
const avRecord = <A>(params: RecordParams<A>): AvroT<A> => { | |
return { | |
codec: getRecordCodec(params), | |
schema: getRecordSchema(params), | |
}; | |
}; | |
type GetTSType<C extends AvroMixed> = t.TypeOf<C["codec"]>; | |
export const test = avRecord({ | |
namespace: "com.foo", | |
name: "Foo", | |
fields: { | |
foo1: avString, | |
foo2: avNull, | |
foo3: avUnion([avNull, avString]), | |
foo4: avUnion([avBoolean, avLong]), | |
}, | |
}); | |
type Test = GetTSType<typeof test>; | |
JSON.stringify(test.schema); | |
/* | |
Outputs: | |
'{"type":"record", | |
"name":"Foo", | |
"namespace":"com.foo", | |
"fields":[ | |
{"name":"foo1","type":"string"}, | |
{"name":"foo2","type":"null"}, | |
{"name":"foo3","type":["null","string"]}, | |
{"name":"foo4","type":["boolean","long"]}]}' | |
*/ | |
// Type-level... unit test? | |
type CheckTest = { | |
foo1: string; | |
foo2: null; | |
foo3: null | string; | |
foo4: boolean | number; | |
}; | |
const x: CheckTest = { foo1: "a", foo2: null, foo3: "c", foo4: 2 }; | |
let y: Test; | |
y = x; // type Test and type CheckTest are structurally equivalent | |
test.codec.decode; // Runtime validation function from io-ts |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment