Skip to content

Commit

Permalink
feat: Alternative object format for runtypes (#9)
Browse files Browse the repository at this point in the history
Co-authored-by: Rune Finstad Halvorsen <runefh@gmail.com>
  • Loading branch information
simenandre and runeh committed Mar 25, 2021
1 parent b435815 commit baaaa02
Show file tree
Hide file tree
Showing 4 changed files with 464 additions and 1 deletion.
174 changes: 174 additions & 0 deletions src/__tests__/main_alt.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,174 @@
import { format, resolveConfig } from 'prettier';
import { Project, SourceFile } from 'ts-morph';
import { generateRuntypes } from '../main_alt';

async function fmt(source: string) {
const config = await resolveConfig(__filename);
return format(source, config);
}

describe('runtype generation', () => {
let project: Project;
let file: SourceFile;

beforeEach(() => {
project = new Project();
file = project.createSourceFile('./test.ts');
});

it('smoke test', async () => {
generateRuntypes(
file,

{
name: 'personRt',
export: false,
type: {
kind: 'record',
fields: [
{ name: 'name', readonly: true, type: { kind: 'string' } },
{ name: 'age', readonly: true, type: { kind: 'number' } },
],
},
},

{
export: true,
name: 'smokeTest',
type: {
kind: 'record',
fields: [
{ name: 'someBoolean', type: { kind: 'boolean' } },
{ name: 'someNever', type: { kind: 'never' } },
{ name: 'someNumber', type: { kind: 'number' } },
{ name: 'someString', type: { kind: 'string' } },
{ name: 'someUnknown', type: { kind: 'unknown' } },
{ name: 'someVoid', type: { kind: 'void' } },
{
name: 'someLiteral1',
type: { kind: 'literal', value: 'string' },
},
{ name: 'someLiteral2', type: { kind: 'literal', value: 1337 } },
{ name: 'someLiteral3', type: { kind: 'literal', value: true } },
{ name: 'someLiteral4', type: { kind: 'literal', value: null } },
{
name: 'someLiteral5',
type: { kind: 'literal', value: undefined },
},
{
name: 'someDictionary',
type: { kind: 'dictionary', valueType: { kind: 'boolean' } },
},
{
name: 'someArray',
type: { kind: 'array', type: { kind: 'string' }, readonly: true },
},
{
name: 'someNamedType',
type: { kind: 'named', name: 'personRt' },
},
{
name: 'someIntersection',
type: {
kind: 'intersect',
types: [
{
kind: 'record',
fields: [{ name: 'member1', type: { kind: 'string' } }],
},
{
kind: 'record',
fields: [{ name: 'member2', type: { kind: 'number' } }],
},
],
},
},
{
name: 'someObject',
type: {
kind: 'record',
fields: [
{ name: 'name', readonly: true, type: { kind: 'string' } },
{ name: 'age', readonly: true, type: { kind: 'number' } },
{
name: 'medals',
readonly: true,
type: {
kind: 'union',
types: [
{ kind: 'literal', value: '1' },
{ kind: 'literal', value: '2' },
{ kind: 'literal', value: '3' },
{ kind: 'literal', value: 'last' },
],
},
},
],
},
},
],
},
},
);
const raw = file.getText();
const formatted = await fmt(raw);
expect(formatted).toMatchInlineSnapshot(`
"const personRt = rt.Record({
name: rt.String,
age: rt.Number,
});
export const smokeTest = rt.Record({
someBoolean: rt.Boolean,
someNever: rt.Never,
someNumber: rt.Number,
someString: rt.String,
someUnknown: rt.Unknown,
someVoid: rt.Void,
someLiteral1: rt.Literal('string'),
someLiteral2: rt.Literal(1337),
someLiteral3: rt.Literal(true),
someLiteral4: rt.Literal(null),
someLiteral5: rt.Literal(undefined),
someDictionary: rt.Dictionary(rt.Boolean),
someArray: rt.Array(rt.String).asReadonly(),
someNamedType: personRt,
someIntersection: rt.Intersect(
rt.Record({
member1: rt.String,
}),
rt.Record({
member2: rt.Number,
}),
),
someObject: rt.Record({
name: rt.String,
age: rt.Number,
medals: rt.Union(
rt.Literal('1'),
rt.Literal('2'),
rt.Literal('3'),
rt.Literal('last'),
),
}),
});
"
`);
});

it.todo('Array');
it.todo('Boolean');
it.todo('Brand');
it.todo('Constrant');
it.todo('Dictionary');
it.todo('Function');
it.todo('Literal');
it.todo('Never');
it.todo('Number');
it.todo('Record');
it.todo('String');
it.todo('Symbol');
it.todo('Tuple');
it.todo('Union');
it.todo('Unknown');
it.todo('Void');
});
123 changes: 123 additions & 0 deletions src/main_alt.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
import {
CodeBlockWriter,
OptionalKind,
SourceFile,
VariableDeclarationKind,
VariableStatementStructure,
} from 'ts-morph';
import {
AnyType,
ArrayType,
DictionaryType,
LiteralType,
NamedType,
RecordType,
RootType,
UnionType,
} from './types_alt';

export function generateRuntypes(file: SourceFile, ...roots: RootType[]): void {
file.addVariableStatements(
roots.map<OptionalKind<VariableStatementStructure>>((root) => {
return {
isExported: root.export,
declarationKind: VariableDeclarationKind.Const,
declarations: [
{ name: root.name, initializer: (w) => writeAnyType(w, root.type) },
],
};
}),
);
}

// fixme: use mapped type so `node` is typed more narrowly maybe
const writers: Record<
AnyType['kind'],
(writer: CodeBlockWriter, node: AnyType) => void
> = {
boolean: simpleWriter('rt.Boolean'),
function: simpleWriter('rt.Function'),
never: simpleWriter('rt.Never'),
number: simpleWriter('rt.Number'),
string: simpleWriter('rt.String'),
unknown: simpleWriter('rt.Unknown'),
void: simpleWriter('rt.Void'),
array: writeArrayType,
record: writeRecordType,
union: writeUnionType,
literal: writeLiteralType,
named: writeNamedType,
intersect: writeIntersectionType,
dictionary: writeDictionaryType,
};

function simpleWriter(value: string): (writer: CodeBlockWriter) => void {
return (writer) => writer.write(value);
}

function writeDictionaryType(w: CodeBlockWriter, node: DictionaryType) {
w.write('rt.Dictionary(');
writeAnyType(w, node.valueType);
w.write(')');
}

function writeNamedType(w: CodeBlockWriter, node: NamedType) {
w.write(node.name);
}

function writeAnyType(w: CodeBlockWriter, node: AnyType) {
const writer = writers[node.kind];
writer(w, node);
}

function writeLiteralType(w: CodeBlockWriter, node: LiteralType) {
const { value } = node;
w.write('rt.Literal(');
if (value === undefined) {
w.write('undefined');
} else if (value === null) {
w.write('null');
} else if (typeof value === 'string') {
w.write(`'${value}'`);
} else {
// It's a boolean or a number at this point.
w.write(String(value));
}
w.write(')');
}

function writeArrayType(w: CodeBlockWriter, node: ArrayType) {
w.write('rt.Array(');
writeAnyType(w, node.type);
w.write(')');
w.conditionalWrite(node.readonly, '.asReadonly()');
}

function writeUnionType(w: CodeBlockWriter, node: UnionType) {
w.writeLine('rt.Union(');
for (const type of node.types) {
writeAnyType(w, type);
w.write(', ');
}
w.write(') ');
}

function writeIntersectionType(w: CodeBlockWriter, node: UnionType) {
w.writeLine('rt.Intersect(');
for (const type of node.types) {
writeAnyType(w, type);
w.write(', ');
}
w.write(') ');
}

function writeRecordType(w: CodeBlockWriter, node: RecordType) {
w.writeLine('rt.Record({');
for (const field of node.fields) {
w.write(field.name);
w.write(': ');
writeAnyType(w, field.type);
w.write(',');
}
w.write('})');
}

0 comments on commit baaaa02

Please sign in to comment.