Skip to content

Commit

Permalink
feat: Topological sort of dependencies (#114)
Browse files Browse the repository at this point in the history
  • Loading branch information
runeh committed Jun 9, 2021
1 parent c1a878f commit f0a80b5
Show file tree
Hide file tree
Showing 4 changed files with 307 additions and 26 deletions.
115 changes: 104 additions & 11 deletions src/__tests__/main.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,27 @@ describe('runtype generation', () => {
fields: [
{ name: 'name', readonly: true, type: { kind: 'string' } },
{ name: 'age', readonly: true, type: { kind: 'number' } },
{
name: 'location',
readonly: true,
type: { kind: 'named', name: 'locationRt' },
},
],
},
},

{
name: 'locationRt',
type: {
kind: 'record',
fields: [
{ name: 'address', readonly: true, type: { kind: 'string' } },
{
name: 'alternateLocation',
readonly: true,
nullable: true,
type: { kind: 'named', name: 'locationRt' },
},
],
},
},
Expand Down Expand Up @@ -104,7 +125,21 @@ describe('runtype generation', () => {
expect(raw).toMatchInlineSnapshot(`
"import * as rt from \\"runtypes\\";
const personRt = rt.Record({ name: rt.String, age: rt.Number }).asReadonly();
type LocationRt = {
readonly address: string;
readonly alternateLocation?: LocationRt;
};
const locationRt: rt.Runtype<LocationRt> = rt.Lazy(() =>
rt.Intersect(
rt.Record({ address: rt.String }).asReadonly(),
rt.Record({ alternateLocation: locationRt }).asPartial().asReadonly()
)
);
const personRt = rt
.Record({ name: rt.String, age: rt.Number, location: locationRt })
.asReadonly();
type PersonRt = rt.Static<typeof personRt>;
Expand Down Expand Up @@ -787,7 +822,16 @@ describe('runtype generation', () => {
);

expect(source).toMatchInlineSnapshot(`
"type person_Type = {
"type job_Type = {
title: string;
people: person_Type[];
};
const job_runtype: rt.Runtype<job_Type> = rt.Lazy(() =>
rt.Record({ title: rt.String, people: rt.Array(person_runtype) })
);
type person_Type = {
name: string;
parent: person_Type;
job: job_Type;
Expand All @@ -796,15 +840,6 @@ describe('runtype generation', () => {
const person_runtype: rt.Runtype<person_Type> = rt.Lazy(() =>
rt.Record({ name: rt.String, parent: person_runtype, job: job_runtype })
);
type job_Type = {
title: string;
people: person_Type[];
};
const job_runtype: rt.Runtype<job_Type> = rt.Lazy(() =>
rt.Record({ title: rt.String, people: rt.Array(person_runtype) })
);
"
`);
});
Expand Down Expand Up @@ -874,5 +909,63 @@ describe('runtype generation', () => {
"
`);
});

it('mixing lazy and non-lazy', () => {
const source = generateRuntypes(
[
{
name: 'job',
type: {
kind: 'record',
fields: [
{ name: 'title', type: { kind: 'string' } },
{
name: 'people',
type: {
kind: 'array',
type: { kind: 'named', name: 'person' },
},
},
],
},
},

{
name: 'person',
type: {
kind: 'record',
fields: [
{ name: 'name', type: { kind: 'string' } },
{ name: 'parent', type: { kind: 'named', name: 'person' } },
],
},
},
],
{
formatRuntypeName: (e) => `${e}_runtype`,
formatTypeName: (e) => `${e}_Type`,
includeImport: false,
},
);

expect(source).toMatchInlineSnapshot(`
"type person_Type = {
name: string;
parent: person_Type;
};
const person_runtype: rt.Runtype<person_Type> = rt.Lazy(() =>
rt.Record({ name: rt.String, parent: person_runtype })
);
const job_runtype = rt.Record({
title: rt.String,
people: rt.Array(person_runtype),
});
type job_Type = rt.Static<typeof job_runtype>;
"
`);
});
});
});
156 changes: 156 additions & 0 deletions src/__tests__/util.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import {
getUnknownNamedTypes,
groupFieldKinds,
rootToType,
topoSortRoots,
} from '../util';

describe('groupFieldKinds', () => {
Expand Down Expand Up @@ -178,6 +179,26 @@ describe('circular dependencies detection', () => {

expect(getCyclicDependencies(roots)).toEqual([]);
});

it('deals with type referencing unknown types', () => {
const roots: RootType[] = [
{
name: 'person',
type: {
kind: 'record',
fields: [
{ name: 'id', type: { kind: 'string' } },
{
name: 'unknown',
type: { kind: 'named', name: 'unknownObject ' },
nullable: true,
},
],
},
},
];
expect(getCyclicDependencies(roots)).toEqual([]);
});
});

describe('getNamedTypes', () => {
Expand Down Expand Up @@ -449,4 +470,139 @@ describe('anyTypeToTsType', () => {
"
`);
});

describe('topoSortRoots', () => {
it('sorts when wrong order of 2 dependencies', () => {
const roots: RootType[] = [
{
name: 'person',
type: {
kind: 'record',
fields: [
{
name: 'office',
type: { kind: 'named', name: 'office' },
},
],
},
},

{
name: 'office',
type: {
kind: 'record',
fields: [
{
name: 'address',
type: { kind: 'string' },
},
],
},
},
];

expect(topoSortRoots(roots).map((e) => e.name)).toEqual([
'office',
'person',
]);
});

it('sorts when wrong order of 4 dependencies', () => {
const roots: RootType[] = [
{
name: 'office',
type: {
kind: 'record',
fields: [
{
name: 'city',
type: { kind: 'named', name: 'city' },
},
],
},
},

{
name: 'city',
type: {
kind: 'record',
fields: [
{
name: 'country',
type: { kind: 'named', name: 'country' },
},
],
},
},

{
name: 'country',
type: {
kind: 'record',
fields: [
{
name: 'name',
type: { kind: 'string' },
},
],
},
},

{
name: 'person',
type: {
kind: 'record',
fields: [
{
name: 'office',
type: { kind: 'named', name: 'office' },
},
],
},
},
];

expect(topoSortRoots(roots).map((e) => e.name)).toEqual([
'country',
'city',
'office',
'person',
]);
});
});

it('sorts when referencing unknown types', () => {
const roots: RootType[] = [
{
name: 'person',
type: {
kind: 'record',
fields: [
{
name: 'office',
type: { kind: 'named', name: 'office' },
},
],
},
},

{
name: 'office',
type: {
kind: 'record',
fields: [
{
name: 'address',
type: { kind: 'named', name: 'unknownType' },
},
],
},
},
];

expect(topoSortRoots(roots).map((e) => e.name)).toEqual([
'office',
'person',
]);
});
});
24 changes: 9 additions & 15 deletions src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import {
anyTypeToTsType,
getCyclicDependencies,
groupFieldKinds,
topoSortRoots,
} from './util';

export type {
Expand Down Expand Up @@ -159,10 +160,14 @@ export function generateRuntypes(
'import * as rt from "runtypes";\n\n',
);

const lazyTypes = cyclicReferences.flat();
roots.forEach((root) => {
const isLazy = lazyTypes.includes(root.name);
writeRootType(allOptions, writer, root, isLazy);
const sorted = topoSortRoots(roots);
const cyclicRootNames = cyclicReferences.flat();
sorted.forEach((root) => {
if (cyclicRootNames.includes(root.name)) {
writeLazyRootType(allOptions, writer, root);
} else {
writeTerminalRootType(allOptions, writer, root);
}
});

const source = writer.getSource();
Expand All @@ -171,17 +176,6 @@ export function generateRuntypes(
: source.trim();
}

function writeRootType(
options: GenerateOptions,
w: CodeWriter,
node: RootType,
isLazy: boolean,
) {
return isLazy
? writeLazyRootType(options, w, node)
: writeTerminalRootType(options, w, node);
}

function writeLazyRootType(
options: GenerateOptions,
w: CodeWriter,
Expand Down
Loading

0 comments on commit f0a80b5

Please sign in to comment.