Skip to content

Instantly share code, notes, and snippets.

@dyerw
Created June 26, 2020 22:05
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save dyerw/4f3bcc0c04e5ceda54e7d0f1a23d0a7f to your computer and use it in GitHub Desktop.
Save dyerw/4f3bcc0c04e5ceda54e7d0f1a23d0a7f to your computer and use it in GitHub Desktop.
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