Skip to content

Commit

Permalink
Merge pull request #280 from quicktype/inference-in-ts
Browse files Browse the repository at this point in the history
Inference in ts
  • Loading branch information
schani committed Nov 23, 2017
2 parents 4580dde + d12dab1 commit e3c3c14
Show file tree
Hide file tree
Showing 19 changed files with 1,072 additions and 185 deletions.
2 changes: 1 addition & 1 deletion .travis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ language: node_js
node_js: node # latest
sudo: required
env:
- FIXTURE=golang,cplusplus,schema,schema-json-golang
- FIXTURE=golang,cplusplus,schema #,schema-json-golang
- FIXTURE=swift3,swift4,java
- FIXTURE=elm,typescript,csharp
services:
Expand Down
9 changes: 5 additions & 4 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "quicktype",
"version": "3.3.0",
"version": "4.0.0",
"license": "Apache-2.0",
"main": "quicktype.js",
"types": "quicktype.d.ts",
Expand All @@ -17,6 +17,7 @@
"chalk": "^2.1.0",
"command-line-args": "^4.0.6",
"command-line-usage": "^4.0.0",
"get-stream": "^3.0.0",
"immutable": "^4.0.0-rc.9",
"json-to-ast": "^2.0.2",
"lodash": "^4.17.4",
Expand All @@ -27,6 +28,8 @@
"unicode-properties": "quicktype/unicode-properties#dist"
},
"devDependencies": {
"@types/get-stream": "^3.0.1",
"@types/pluralize": "0.0.28",
"@types/lodash": "^4.14.72",
"@types/node": "^8.0.19",
"@types/string-hash": "^1.1.1",
Expand All @@ -45,8 +48,6 @@
"uglify-js": "^3.0.26",
"watch": "^1.0.2"
},
"files": [
"*"
],
"files": ["*"],
"bin": "quicktype.js"
}
158 changes: 158 additions & 0 deletions src-ts/CombineClasses.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,158 @@
"use strict";

import { Map, OrderedMap, OrderedSet } from "immutable";

import { TopLevels, ClassType, Type, allNamedTypesSeparated, removeNull, makeNullable } from "./Type";
import { assert, panic } from "./Support";

const REQUIRED_OVERLAP = 3 / 4;

function canBeCombined(c1: ClassType, c2: ClassType): boolean {
const p1 = c1.properties;
const p2 = c2.properties;
if (p1.size < p2.size * REQUIRED_OVERLAP || p2.size < p1.size * REQUIRED_OVERLAP) {
return false;
}
let larger: Map<string, Type>;
let smaller: Map<string, Type>;
if (p1.size > p2.size) {
larger = p1;
smaller = p2;
} else {
larger = p2;
smaller = p1;
}
const minOverlap = Math.ceil(larger.size * REQUIRED_OVERLAP);
const maxFaults = smaller.size - minOverlap;
assert(maxFaults >= 0, "Max faults negative");
const commonProperties: string[] = [];
let faults = 0;
smaller.forEach((_, name) => {
if (larger.has(name)) {
commonProperties.push(name);
} else {
faults += 1;
if (faults > maxFaults) return false;
}
});
if (faults > maxFaults) return false;
for (const name of commonProperties) {
let ts = smaller.get(name);
let tl = larger.get(name);
if (ts === undefined || tl === undefined) {
return panic("Both of these should have this property");
}
ts = removeNull(ts);
tl = removeNull(tl);
// Removing null can make unions not referentially equal.
// We allow null properties to unify with any other.
// FIXME: Allow some type combinations to unify, like different enums,
// enums with strings, integers with doubles, maps with objects of
// the correct type.
if (ts.kind !== "null" && tl.kind !== "null" && !ts.equals(tl)) {
return false;
}
}
return true;
}

function isPartOfClique(c: ClassType, clique: ClassType[]): boolean {
for (const cc of clique) {
if (!canBeCombined(c, cc)) {
return false;
}
}
return true;
}

function makeCliqueClass(clique: ClassType[]): ClassType {
let names = OrderedSet<string>();
for (const c of clique) {
names = names.union(c.names);
}
return new ClassType(names);
}

export function combineClasses(graph: TopLevels): TopLevels {
let unprocessedClasses = allNamedTypesSeparated(graph).classes.toArray();
const cliques: ClassType[][] = [];

while (unprocessedClasses.length > 0) {
const classesLeft: ClassType[] = [];
const clique = [unprocessedClasses[0]];

for (let i = 1; i < unprocessedClasses.length; i++) {
const c = unprocessedClasses[i];
if (isPartOfClique(c, clique)) {
clique.push(c);
} else {
classesLeft.push(c);
}
}

if (clique.length > 1) {
cliques.push(clique);
}

unprocessedClasses = classesLeft;
}

const combinedCliques: ClassType[] = [];
let replacements: Map<ClassType, ClassType> = Map();

for (const clique of cliques) {
const combined = makeCliqueClass(clique);
combinedCliques.push(combined);
for (const c of clique) {
replacements = replacements.set(c, combined);
}
}

const replaceClasses = (t: Type): Type => {
if (t instanceof ClassType) {
if (combinedCliques.indexOf(t) >= 0) return t;
const c = replacements.get(t);
if (c) return c;
}
return t.map(replaceClasses);
};

const setCliqueProperties = (combined: ClassType, clique: ClassType[]): void => {
let properties = OrderedMap<string, [Type, number, boolean]>();
for (const c of clique) {
c.properties.forEach((t, name) => {
const p = properties.get(name);
if (p) {
p[1] += 1;
// If one of the clique class's properties is nullable,
// the combined property must be nullable, too, so we
// just set it to this one. Of course it can't be one of
// the properties that is just null.
if (t.kind !== "null") {
p[0] = t;
}
if (t.isNullable) {
p[2] = true;
}
} else {
properties = properties.set(name, [t, 1, t.isNullable]);
}
});
}
combined.setProperties(
properties.map(([t, count, haveNullable], name) => {
t = replaceClasses(t);
if (haveNullable || count < clique.length) {
t = makeNullable(t, name);
}
return t;
})
);
};

for (let i = 0; i < cliques.length; i++) {
setCliqueProperties(combinedCliques[i], cliques[i]);
}

return graph.map(replaceClasses);
}

0 comments on commit e3c3c14

Please sign in to comment.