-
Notifications
You must be signed in to change notification settings - Fork 2
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: Alternative object format for runtypes (#9)
Co-authored-by: Rune Finstad Halvorsen <runefh@gmail.com>
- Loading branch information
1 parent
b435815
commit baaaa02
Showing
4 changed files
with
464 additions
and
1 deletion.
There are no files selected for viewing
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
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'); | ||
}); |
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
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('})'); | ||
} |
Oops, something went wrong.