Merge pull request #696 from quicktype/object-type

Full object type
This commit is contained in:
Mark Probst 2018-03-28 22:12:00 -07:00 коммит произвёл GitHub
Родитель 227ca1d97f a52396958b
Коммит d1a14554c9
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: 4AEE18F83AFDEB23
18 изменённых файлов: 599 добавлений и 421 удалений

Просмотреть файл

@ -2,12 +2,11 @@
import { Map, Set, OrderedSet } from "immutable";
import { ClassType, Type, nonNullTypeCases, ClassProperty } from "./Type";
import { ClassType, Type, nonNullTypeCases, ClassProperty, combineTypeAttributesOfTypes } from "./Type";
import { GraphRewriteBuilder, TypeRef, StringTypeMapping } from "./TypeBuilder";
import { assert, panic } from "./Support";
import { TypeGraph } from "./TypeGraph";
import { unifyTypes, unionBuilderForUnification } from "./UnifyClasses";
import { combineTypeAttributes } from "./TypeAttributes";
const REQUIRED_OVERLAP = 3 / 4;
@ -138,12 +137,12 @@ export function combineClasses(
forwardingRef: TypeRef
): TypeRef {
assert(clique.size > 0, "Clique can't be empty");
const attributes = combineTypeAttributes(clique.toArray().map(c => c.getAttributes()));
const attributes = combineTypeAttributesOfTypes(clique);
return unifyTypes(
clique,
attributes,
builder,
unionBuilderForUnification(builder, false, false, conflateNumbers),
unionBuilderForUnification(builder, false, false, false, conflateNumbers),
conflateNumbers,
forwardingRef
);

Просмотреть файл

@ -530,6 +530,9 @@ export abstract class ConvenienceRenderer extends Renderer {
arrayType => typeNameForUnionMember(arrayType.items) + "_array",
classType => lookup(this.nameForNamedType(classType)),
mapType => typeNameForUnionMember(mapType.values) + "_map",
_objectType => {
return panic("Object types not supported in the renderer yet");
},
_enumType => "enum",
_unionType => "union",
_dateType => "date",

Просмотреть файл

@ -33,12 +33,13 @@ function unionMembersRecursively(...unions: UnionType[]): OrderedSet<Type> {
export function flattenUnions(
graph: TypeGraph,
stringTypeMapping: StringTypeMapping,
conflateNumbers: boolean
conflateNumbers: boolean,
makeObjectTypes: boolean
): [TypeGraph, boolean] {
let needsRepeat = false;
function replace(types: Set<Type>, builder: GraphRewriteBuilder<Type>, forwardingRef: TypeRef): TypeRef {
const unionBuilder = new UnifyUnionBuilder(builder, true, true, (trefs, attributes) => {
const unionBuilder = new UnifyUnionBuilder(builder, true, makeObjectTypes, true, (trefs, attributes) => {
assert(trefs.length > 0, "Must have at least one type to build union");
trefs = trefs.map(tref => builder.reconstituteType(tref.deref()[0]));
if (trefs.length === 1) {

Просмотреть файл

@ -2,7 +2,7 @@
import { Set, OrderedMap, OrderedSet } from "immutable";
import { Type, StringType, UnionType } from "./Type";
import { Type, StringType, UnionType, combineTypeAttributesOfTypes } from "./Type";
import { TypeGraph } from "./TypeGraph";
import { GraphRewriteBuilder, TypeRef, StringTypeMapping } from "./TypeBuilder";
import { assert, defined } from "./Support";
@ -51,7 +51,7 @@ function replaceUnion(group: Set<UnionType>, builder: GraphRewriteBuilder<UnionT
assert(group.size === 1);
const u = defined(group.first());
const stringMembers = defined(unionNeedsReplacing(u));
const stringAttributes = combineTypeAttributes(stringMembers.toArray().map(t => t.getAttributes()));
const stringAttributes = combineTypeAttributesOfTypes(stringMembers);
const types: TypeRef[] = [];
u.members.forEach(t => {
if (stringMembers.has(t)) return;

Просмотреть файл

@ -106,16 +106,18 @@ export function inferMaps(graph: TypeGraph, stringTypeMapping: StringTypeMapping
// Reconstituting a type means generating the "same" type in the new
// type graph. Except we don't get Type objects but TypeRef objects,
// which is a type-to-be.
return builder.getMapType(
const tref = builder.getMapType(
unifyTypes(
shouldBe,
c.getAttributes(),
builder,
unionBuilderForUnification(builder, false, false, conflateNumbers),
unionBuilderForUnification(builder, false, false, false, conflateNumbers),
conflateNumbers
),
forwardingRef
);
builder.addAttributes(tref, c.getAttributes());
return tref;
}
const allClasses = graph.allNamedTypesSeparated().classes;

Просмотреть файл

@ -3,7 +3,7 @@
import { OrderedMap, Map } from "immutable";
import { Value, Tag, valueTag, CompressedJSON } from "./CompressedJSON";
import { assertNever, assert } from "./Support";
import { assertNever } from "./Support";
import { TypeBuilder, UnionBuilder, TypeRef, UnionAccumulator } from "./TypeBuilder";
import { isTime, isDateTime, isDate } from "./DateTime";
import { ClassProperty } from "./Type";
@ -31,7 +31,7 @@ function forEachValueInNestedValueArray(va: NestedValueArray, f: (v: Value) => v
forEachArrayInNestedValueArray(va, a => a.forEach(f));
}
class InferenceUnionBuilder extends UnionBuilder<TypeBuilder, NestedValueArray, NestedValueArray, any> {
class InferenceUnionBuilder extends UnionBuilder<TypeBuilder, NestedValueArray, NestedValueArray> {
constructor(
typeBuilder: TypeBuilder,
private readonly _typeInference: TypeInference,
@ -51,14 +51,12 @@ class InferenceUnionBuilder extends UnionBuilder<TypeBuilder, NestedValueArray,
return this.typeBuilder.getStringType(typeAttributes, caseMap, forwardingRef);
}
protected makeClass(
classes: NestedValueArray,
maps: any[],
protected makeObject(
objects: NestedValueArray,
typeAttributes: TypeAttributes,
forwardingRef: TypeRef | undefined
): TypeRef {
assert(maps.length === 0);
return this._typeInference.inferClassType(this._cjson, typeAttributes, classes, this._fixed, forwardingRef);
return this._typeInference.inferClassType(this._cjson, typeAttributes, objects, this._fixed, forwardingRef);
}
protected makeArray(
@ -91,7 +89,7 @@ export class TypeInference {
fixed: boolean,
forwardingRef?: TypeRef
): TypeRef {
const accumulator = new UnionAccumulator<NestedValueArray, NestedValueArray, any>(true);
const accumulator = new UnionAccumulator<NestedValueArray, NestedValueArray>(true);
forEachValueInNestedValueArray(valueArray, value => {
const t = valueTag(value);
@ -125,7 +123,7 @@ export class TypeInference {
accumulator.addStringType("string", emptyTypeAttributes);
break;
case Tag.Object:
accumulator.addClass(cjson.getObjectForValue(value), emptyTypeAttributes);
accumulator.addObject(cjson.getObjectForValue(value), emptyTypeAttributes);
break;
case Tag.Array:
accumulator.addArray(cjson.getArrayForValue(value), emptyTypeAttributes);

Просмотреть файл

@ -17,7 +17,7 @@ import {
forEachSync,
checkArray
} from "./Support";
import { TypeGraphBuilder, TypeRef } from "./TypeBuilder";
import { TypeBuilder, TypeRef } from "./TypeBuilder";
import { TypeNames } from "./TypeNames";
import { makeNamesTypeAttributes, modifyTypeNames, singularizeTypeNames } from "./TypeNames";
import {
@ -429,7 +429,7 @@ function checkTypeList(typeOrTypes: any): OrderedSet<string> {
}
export async function addTypesInSchema(
typeBuilder: TypeGraphBuilder,
typeBuilder: TypeBuilder,
store: JSONSchemaStore,
references: Map<string, Ref>
): Promise<void> {
@ -456,12 +456,12 @@ export async function addTypesInSchema(
typeForCanonicalRef = typeForCanonicalRef.set(loc.canonicalRef, t);
}
async function makeClass(
async function makeObject(
loc: Location,
attributes: TypeAttributes,
properties: StringMap,
requiredArray: string[],
additionalPropertiesType: boolean | TypeRef
additionalProperties: any
): Promise<TypeRef> {
const required = OrderedSet(requiredArray);
const propertiesMap = Map(properties);
@ -484,35 +484,36 @@ export async function addTypesInSchema(
// we would use a JSON parser that preserves order.
let props = (await mapSync(propertiesMap, async (propSchema, propName) => {
const t = await toType(
checkStringMap(propSchema),
checkJSONSchema(propSchema),
loc.push("properties", propName),
makeNamesTypeAttributes(pluralize.singular(propName), true)
);
const isOptional = !required.has(propName);
return new ClassProperty(t, isOptional);
})).toOrderedMap();
let additionalPropertiesType: TypeRef | undefined;
if (additionalProperties === undefined || additionalProperties === true) {
additionalPropertiesType = typeBuilder.getPrimitiveType("any");
} else if (additionalProperties === false) {
additionalPropertiesType = undefined;
} else {
additionalPropertiesType = await toType(
checkJSONSchema(additionalProperties),
loc.push("additionalProperties"),
singularizeTypeNames(attributes)
);
}
const additionalRequired = required.subtract(props.keySeq());
if (!additionalRequired.isEmpty()) {
let t: TypeRef;
if (additionalPropertiesType === false) {
const t = additionalPropertiesType;
if (t === undefined) {
return panic("Can't have non-specified required properties but forbidden additionalTypes");
}
if (additionalPropertiesType === true) {
t = typeBuilder.getPrimitiveType("any");
} else {
t = additionalPropertiesType;
}
const additionalProps = additionalRequired.toOrderedMap().map(_name => new ClassProperty(t, true));
props = props.merge(additionalProps);
}
return typeBuilder.getUniqueClassType(attributes, true, props);
}
async function makeMap(loc: Location, typeAttributes: TypeAttributes, additional: StringMap): Promise<[TypeRef, TypeRef]> {
loc = loc.push("additionalProperties");
const valuesType = await toType(additional, loc, singularizeTypeNames(typeAttributes));
return [typeBuilder.getMapType(valuesType), valuesType];
return typeBuilder.getUniqueObjectType(attributes, props, additionalPropertiesType);
}
async function convertToType(schema: StringMap, loc: Location, typeAttributes: TypeAttributes): Promise<TypeRef> {
@ -536,7 +537,7 @@ export async function addTypesInSchema(
}
return typeBuilder.getStringType(inferredAttributes, undefined);
}
async function makeArrayType(): Promise<TypeRef> {
if (schema.items !== undefined) {
loc = loc.push("items");
@ -547,7 +548,7 @@ export async function addTypesInSchema(
return typeBuilder.getArrayType(typeBuilder.getPrimitiveType("any"));
}
async function makeObjectTypes(): Promise<TypeRef[]> {
async function makeObjectType(): Promise<TypeRef> {
let required: string[];
if (schema.required === undefined) {
required = [];
@ -555,39 +556,18 @@ export async function addTypesInSchema(
required = checkStringArray(schema.required);
}
const typesInUnion: TypeRef[] = [];
let additionalPropertiesType: boolean | TypeRef = true;
if (schema.additionalProperties !== undefined) {
const additional = schema.additionalProperties;
// FIXME: We don't treat `additional === true`, which is also the default,
// not according to spec. It should be translated into a map type to any,
// though that's not what the intention usually is. Ideally, we'd find a
// way to store additional attributes on regular classes.
if (additional === false) {
if (schema.properties === undefined) {
typesInUnion.push(await makeClass(loc, inferredAttributes, {}, required, false));
}
additionalPropertiesType = false;
} else if (typeof additional === "object") {
const [mapType, valuesType] = await makeMap(loc, inferredAttributes, checkStringMap(additional));
typesInUnion.push(await mapType);
additionalPropertiesType = valuesType;
}
let properties: StringMap;
if (schema.properties === undefined) {
properties = {};
} else {
properties = checkStringMap(schema.properties);
}
if (schema.properties !== undefined) {
typesInUnion.push(
await makeClass(loc, inferredAttributes, checkStringMap(schema.properties), required, additionalPropertiesType)
);
}
const additionalProperties = schema.additionalProperties;
if (typesInUnion.length === 0) {
typesInUnion.push(typeBuilder.getMapType(typeBuilder.getPrimitiveType("any")));
}
return typesInUnion;
return await makeObject(loc, inferredAttributes, properties, required, additionalProperties);
}
async function makeTypesFromCases(cases: any, kind: string): Promise<TypeRef[]> {
if (!Array.isArray(cases)) {
return panic(`Cases are not an array: ${cases}`);
@ -658,8 +638,16 @@ export async function addTypesInSchema(
const includeObject = enumArray === undefined && (typeSet === undefined || typeSet.has("object"));
const includeArray = enumArray === undefined && (typeSet === undefined || typeSet.has("array"));
const needStringEnum = includePrimitiveType("string") && enumArray !== undefined && enumArray.find((x: any) => typeof x === "string") !== undefined;
const needUnion = typeSet !== undefined || schema.properties !== undefined || schema.additionalProperties !== undefined || schema.items !== undefined || enumArray !== undefined;
const needStringEnum =
includePrimitiveType("string") &&
enumArray !== undefined &&
enumArray.find((x: any) => typeof x === "string") !== undefined;
const needUnion =
typeSet !== undefined ||
schema.properties !== undefined ||
schema.additionalProperties !== undefined ||
schema.items !== undefined ||
enumArray !== undefined;
const intersectionType = typeBuilder.getUniqueIntersectionType(typeAttributes, undefined);
await setTypeForLocation(loc, intersectionType);
@ -668,7 +656,12 @@ export async function addTypesInSchema(
if (needUnion) {
const unionTypes: TypeRef[] = [];
for (const [name, kind] of [["null", "null"], ["number", "double"], ["integer", "integer"], ["boolean", "bool"]] as [string, PrimitiveTypeKind][]) {
for (const [name, kind] of [
["null", "null"],
["number", "double"],
["integer", "integer"],
["boolean", "bool"]
] as [string, PrimitiveTypeKind][]) {
if (!includePrimitiveType(name)) continue;
unionTypes.push(typeBuilder.getPrimitiveType(kind));
@ -686,7 +679,7 @@ export async function addTypesInSchema(
unionTypes.push(await makeArrayType());
}
if (includeObject) {
unionTypes.push(...(await makeObjectTypes()));
unionTypes.push(await makeObjectType());
}
types.push(typeBuilder.getUniqueUnionType(inferredAttributes, OrderedSet(unionTypes)));
@ -701,7 +694,7 @@ export async function addTypesInSchema(
});
types.push(await toType(target, newLoc, attributes));
}
if (schema.allOf !== undefined) {
types.push(...(await makeTypesFromCases(schema.allOf, "allOf")));
}

Просмотреть файл

@ -113,6 +113,9 @@ export class JSONSchemaRenderer extends ConvenienceRenderer {
arrayType => ({ type: "array", items: this.schemaForType(arrayType.items) }),
classType => this.makeRef(classType),
mapType => ({ type: "object", additionalProperties: this.schemaForType(mapType.values) }),
_objectType => {
return panic("FIXME: support object types");
},
enumType => this.makeRef(enumType),
unionType => {
if (this.unionNeedsName(unionType)) {

85
src/ReplaceObjectType.ts Normal file
Просмотреть файл

@ -0,0 +1,85 @@
"use strict";
import { Set, OrderedMap } from "immutable";
import { TypeGraph } from "./TypeGraph";
import { StringTypeMapping, GraphRewriteBuilder, TypeRef } from "./TypeBuilder";
import { ObjectType, ClassProperty } from "./Type";
import { defined } from "./Support";
import { emptyTypeAttributes } from "./TypeAttributes";
export function replaceObjectType(
graph: TypeGraph,
stringTypeMapping: StringTypeMapping,
_conflateNumbers: boolean
): TypeGraph {
function replace(
setOfOneType: Set<ObjectType>,
builder: GraphRewriteBuilder<ObjectType>,
forwardingRef: TypeRef
): TypeRef {
const o = defined(setOfOneType.first());
const attributes = o.getAttributes();
const properties = o.properties;
const additionalProperties = o.additionalProperties;
function reconstituteProperties(): OrderedMap<string, ClassProperty> {
return properties.map(cp => new ClassProperty(builder.reconstituteTypeRef(cp.typeRef), cp.isOptional));
}
function makeClass(): TypeRef {
return builder.getUniqueClassType(attributes, true, reconstituteProperties(), forwardingRef);
}
function reconstituteAdditionalProperties(): TypeRef {
return builder.reconstituteType(defined(additionalProperties));
}
if (additionalProperties === undefined) {
return makeClass();
}
if (properties.isEmpty()) {
const tref = builder.getMapType(reconstituteAdditionalProperties(), forwardingRef);
builder.addAttributes(tref, attributes);
return tref;
}
if (additionalProperties.kind === "any") {
// FIXME: Warn that we're losing additional property semantics.
builder.setLostTypeAttributes();
return makeClass();
}
// FIXME: Warn that we're losing class semantics.
const propertyTypes = properties
.map(cp => cp.type)
.toOrderedSet()
.add(additionalProperties);
let union = builder.lookupTypeRefs(propertyTypes.toArray().map(t => t.typeRef));
if (union === undefined) {
const reconstitutedTypes = propertyTypes.map(t => builder.reconstituteType(t));
union = builder.getUniqueUnionType(emptyTypeAttributes, reconstitutedTypes);
// This is the direct unification alternative. Weirdly enough, it is a tiny
// bit slower. It gives the same results.
/*
union = unifyTypes(
propertyTypes,
combineTypeAttributes(propertyTypes.toArray().map(t => t.getAttributes())),
builder,
unionBuilderForUnification(builder, false, false, false, conflateNumbers),
conflateNumbers
);
*/
}
const mapType = builder.getMapType(union, forwardingRef);
builder.addAttributes(mapType, attributes);
return mapType;
}
const allObjectTypes = graph.allTypesUnordered().filter(t => t.kind === "object") as Set<ObjectType>;
const groups = allObjectTypes.toArray().map(t => [t]);
return graph.rewrite("replace object type", stringTypeMapping, false, groups, replace);
}

Просмотреть файл

@ -16,7 +16,6 @@ import {
import {
IntersectionType,
Type,
ClassType,
ClassProperty,
EnumType,
UnionType,
@ -25,12 +24,13 @@ import {
StringType,
ArrayType,
matchTypeExhaustive,
MapType,
isPrimitiveStringTypeKind,
isPrimitiveTypeKind,
isNumberTypeKind,
GenericClassProperty,
TypeKind
TypeKind,
combineTypeAttributesOfTypes,
ObjectType
} from "./Type";
import { assert, defined, panic } from "./Support";
import {
@ -70,13 +70,10 @@ function attributesForTypes<T extends TypeKind>(types: OrderedSet<Type>): TypeAt
.mapKeys(t => t.kind) as Map<T, TypeAttributes>;
}
type PropertyMap = OrderedMap<string, GenericClassProperty<OrderedSet<Type>>>;
class IntersectionAccumulator
implements
UnionTypeProvider<
OrderedSet<Type>,
OrderedMap<string, GenericClassProperty<OrderedSet<Type>>> | undefined,
OrderedSet<Type> | undefined
> {
implements UnionTypeProvider<OrderedSet<Type>, [PropertyMap, OrderedSet<Type> | undefined] | undefined> {
private _primitiveStringTypes: OrderedSet<PrimitiveStringTypeKind> | undefined;
private _primitiveStringAttributes: TypeAttributeMap<PrimitiveStringTypeKind> = OrderedMap();
@ -92,24 +89,17 @@ class IntersectionAccumulator
private _arrayItemTypes: OrderedSet<Type> | undefined | false;
private _arrayAttributes: TypeAttributes = emptyTypeAttributes;
// We allow only either maps, classes, or neither. States:
// We start out with all object types allowed, which means
// _additionalPropertyTypes is empty - no restrictions - and
// _classProperties is empty - no defined properties so far.
//
// 1. Start: No types seen yet, both are allowed, _mapValueTypes is
// the empty set, _classProperties is undefined.
// 2. At least one type seen, all of them can be maps: _mapValueTypes
// is a non-empty set, _classProperties is undefined.
// 3. All types seen can be maps or classes, at least one of them
// can only be a class: Maps are not allowed anymore, but classes
// are. _mapValueTypes is undefined, _classProperties is defined.
// 4. At least one type seen that can't be map or class: Neither map
// nor class is allowed anymore. _mapValueTypes and _classProperties
// are both undefined.
private _mapValueTypes: OrderedSet<Type> | undefined = OrderedSet();
private _mapAttributes: TypeAttributes = emptyTypeAttributes;
private _classProperties: OrderedMap<string, GenericClassProperty<OrderedSet<Type>>> | undefined;
private _classAttributes: TypeAttributes = emptyTypeAttributes;
// If _additionalPropertyTypes is undefined, no additional
// properties are allowed anymore. If _classProperties is
// undefined, no object types are allowed, in which case
// _additionalPropertyTypes must also be undefined;
private _objectProperties: PropertyMap | undefined = OrderedMap();
private _objectAttributes: TypeAttributes = emptyTypeAttributes;
private _additionalPropertyTypes: OrderedSet<Type> | undefined = OrderedSet();
private _lostTypeAttributes: boolean = false;
@ -155,7 +145,7 @@ class IntersectionAccumulator
private updateEnumCases(members: OrderedSet<Type>): void {
const enums = members.filter(t => t instanceof EnumType) as OrderedSet<EnumType>;
const attributes = combineTypeAttributes(enums.toArray().map(t => t.getAttributes()));
const attributes = combineTypeAttributesOfTypes(enums);
this._enumAttributes = combineTypeAttributes(this._enumAttributes, attributes);
if (members.find(t => t instanceof StringType) !== undefined) {
return;
@ -184,72 +174,50 @@ class IntersectionAccumulator
}
}
private updateMapValueTypesAndClassProperties(members: OrderedSet<Type>): void {
function makeProperties(): OrderedMap<string, GenericClassProperty<OrderedSet<Type>>> {
if (maybeClass === undefined) return panic("Didn't we just check for this?");
return maybeClass.properties.map(cp => new GenericClassProperty(OrderedSet([cp.type]), cp.isOptional));
}
const maybeClass = members.find(t => t instanceof ClassType) as ClassType | undefined;
const maybeMap = members.find(t => t instanceof MapType) as MapType | undefined;
assert(
maybeClass === undefined || maybeMap === undefined,
"Can't have both class and map type in a canonical union"
);
if (maybeClass !== undefined) {
this._classAttributes = combineTypeAttributes(this._classAttributes, maybeClass.getAttributes());
}
if (maybeMap !== undefined) {
this._mapAttributes = combineTypeAttributes(this._mapAttributes, maybeMap.getAttributes());
}
if (maybeMap === undefined && maybeClass === undefined) {
// Moving to state 4.
this._mapValueTypes = undefined;
this._classProperties = undefined;
private updateObjectProperties(members: OrderedSet<Type>): void {
const maybeObject = members.find(t => t instanceof ObjectType) as ObjectType | undefined;
if (maybeObject === undefined) {
this._objectProperties = undefined;
this._additionalPropertyTypes = undefined;
return;
}
if (this._mapValueTypes !== undefined) {
// We're in state 1 or 2.
assert(this._classProperties === undefined, "One of _mapValueTypes and _classProperties must be undefined");
this._objectAttributes = combineTypeAttributes(this._objectAttributes, maybeObject.getAttributes());
if (maybeMap !== undefined) {
// Moving to state 2.
this._mapValueTypes = this._mapValueTypes.add(maybeMap.values);
} else {
// Moving to state 3.
this._mapValueTypes = undefined;
this._classProperties = makeProperties();
this._lostTypeAttributes = true;
}
} else if (this._classProperties !== undefined) {
// We're in state 3.
if (maybeMap !== undefined) {
this._classProperties = this._classProperties.map(
cp => new GenericClassProperty(cp.typeData.add(maybeMap.values), cp.isOptional)
);
} else {
// Staying in state 3.
if (maybeClass === undefined) return panic("Didn't we just check for this?");
this._classProperties = this._classProperties.mergeWith(
(cp1, cp2) =>
new GenericClassProperty(cp1.typeData.union(cp2.typeData), cp1.isOptional || cp2.isOptional),
makeProperties()
);
}
} else {
// We're in state 4. No way out of state 4.
this._lostTypeAttributes = true;
if (this._objectProperties === undefined) {
assert(this._additionalPropertyTypes === undefined);
return;
}
assert(
this._mapValueTypes === undefined || this._classProperties === undefined,
"We screwed up our sacred state machine."
);
const allPropertyNames = this._objectProperties.keySeq().toOrderedSet().union(maybeObject.properties.keySeq());
allPropertyNames.forEach(name => {
const existing = defined(this._objectProperties).get(name);
const newProperty = maybeObject.properties.get(name);
if (existing !== undefined && newProperty !== undefined) {
const cp = new GenericClassProperty(existing.typeData.add(newProperty.type), existing.isOptional || newProperty.isOptional);
this._objectProperties = defined(this._objectProperties).set(name, cp);
} else if (existing !== undefined && maybeObject.additionalProperties !== undefined) {
const cp = new GenericClassProperty(existing.typeData.add(maybeObject.additionalProperties), existing.isOptional);
this._objectProperties = defined(this._objectProperties).set(name, cp);
} else if (existing !== undefined) {
this._objectProperties = defined(this._objectProperties).remove(name);
} else if (newProperty !== undefined && this._additionalPropertyTypes !== undefined) {
const types = this._additionalPropertyTypes.add(newProperty.type);
this._objectProperties = defined(this._objectProperties).set(name, new GenericClassProperty(types, newProperty.isOptional));
} else if (newProperty !== undefined) {
this._objectProperties = defined(this._objectProperties).remove(name);
} else {
return panic("This should not happen");
}
});
if (this._additionalPropertyTypes !== undefined && maybeObject.additionalProperties) {
this._additionalPropertyTypes = this._additionalPropertyTypes.add(maybeObject.additionalProperties);
} else if (this._additionalPropertyTypes !== undefined || maybeObject.additionalProperties) {
this._additionalPropertyTypes = undefined;
this._lostTypeAttributes = true;
}
}
private addUnionSet(members: OrderedSet<Type>): void {
@ -257,7 +225,7 @@ class IntersectionAccumulator
this.updateOtherPrimitiveTypes(members);
this.updateEnumCases(members);
this.updateArrayItemTypes(members);
this.updateMapValueTypesAndClassProperties(members);
this.updateObjectProperties(members);
}
addType(t: Type): TypeAttributes {
@ -276,8 +244,9 @@ class IntersectionAccumulator
doubleType => this.addUnionSet(OrderedSet([doubleType])),
stringType => this.addUnionSet(OrderedSet([stringType])),
arrayType => this.addUnionSet(OrderedSet([arrayType])),
classType => this.addUnionSet(OrderedSet([classType])),
mapType => this.addUnionSet(OrderedSet([mapType])),
_classType => panic("We should never see class types in intersections"),
_mapType => panic("We should never see map types in intersections"),
objectType => this.addUnionSet(OrderedSet([objectType])),
enumType => this.addUnionSet(OrderedSet([enumType])),
unionType => {
attributes = combineTypeAttributes(
@ -299,12 +268,13 @@ class IntersectionAccumulator
return this._arrayItemTypes;
}
get mapData(): OrderedSet<Type> | undefined {
return this._mapValueTypes;
}
get objectData(): [PropertyMap, OrderedSet<Type> | undefined] | undefined {
if (this._objectProperties === undefined) {
assert(this._additionalPropertyTypes === undefined);
return undefined;
}
get classData(): OrderedMap<string, GenericClassProperty<OrderedSet<Type>>> | undefined {
return this._classProperties;
return [this._objectProperties, this._additionalPropertyTypes];
}
get enumCases(): string[] {
@ -357,12 +327,9 @@ class IntersectionAccumulator
this._lostTypeAttributes = true;
}
const objectAttributes = combineTypeAttributes(this._classAttributes, this._mapAttributes);
if (this._mapValueTypes !== undefined) {
kinds = kinds.set("map", objectAttributes);
} else if (this._classProperties !== undefined) {
kinds = kinds.set("class", objectAttributes);
} else if (!objectAttributes.isEmpty()) {
if (this._objectProperties !== undefined) {
kinds = kinds.set("object", this._objectAttributes);
} else if (!this._objectAttributes.isEmpty()) {
this._lostTypeAttributes = true;
}
@ -377,8 +344,7 @@ class IntersectionAccumulator
class IntersectionUnionBuilder extends UnionBuilder<
TypeBuilder & TypeLookerUp,
OrderedSet<Type>,
OrderedMap<string, GenericClassProperty<OrderedSet<Type>>> | undefined,
OrderedSet<Type> | undefined
[PropertyMap, OrderedSet<Type> | undefined] | undefined
> {
private _createdNewIntersections: boolean = false;
@ -408,30 +374,24 @@ class IntersectionUnionBuilder extends UnionBuilder<
return this.typeBuilder.getEnumType(typeAttributes, OrderedSet(cases), forwardingRef);
}
protected makeClass(
maybeProperties: OrderedMap<string, GenericClassProperty<OrderedSet<Type>>> | undefined,
maybeMapValueTypes: OrderedSet<Type> | undefined,
protected makeObject(
maybeData: [PropertyMap, OrderedSet<Type> | undefined] | undefined,
typeAttributes: TypeAttributes,
forwardingRef: TypeRef | undefined
): TypeRef {
if (maybeProperties !== undefined) {
assert(maybeMapValueTypes === undefined);
const tref = this.typeBuilder.getUniqueClassType(typeAttributes, true, undefined, forwardingRef);
// FIXME: attributes
const properties = maybeProperties.map(
cp => new ClassProperty(this.makeIntersection(cp.typeData, Map()), cp.isOptional)
);
this.typeBuilder.setClassProperties(tref, properties);
return tref;
} else if (maybeMapValueTypes !== undefined) {
// FIXME: attributes
const valuesType = this.makeIntersection(maybeMapValueTypes, Map());
const mapType = this.typeBuilder.getMapType(valuesType, forwardingRef);
this.typeBuilder.addAttributes(mapType, typeAttributes);
return mapType;
} else {
return panic("Either classes or maps must be given");
if (maybeData === undefined) {
return panic("Either properties or additional properties must be given to make an object type");
}
const [propertyTypes, maybeAdditionalProperties] = maybeData;
const properties = propertyTypes.map(
cp => new ClassProperty(this.makeIntersection(cp.typeData, Map()), cp.isOptional)
);
const additionalProperties =
maybeAdditionalProperties === undefined
? undefined
: this.makeIntersection(maybeAdditionalProperties, emptyTypeAttributes);
return this.typeBuilder.getUniqueObjectType(typeAttributes, properties, additionalProperties, forwardingRef);
}
protected makeArray(

Просмотреть файл

@ -15,6 +15,13 @@ export function setUnion<T, TSet extends Set<T>>(a: TSet, b: TSet): TSet {
return a.union(b) as TSet;
}
export function unionOfSets<T, TSet extends Set<T>>(sets: TSet[]): TSet {
if (sets.length === 0) {
return Set() as TSet;
}
return sets[0].union(...sets.slice(1)) as TSet;
}
export type StringMap = { [name: string]: any };
export function isStringMap(x: any): x is StringMap;
@ -128,19 +135,28 @@ export async function forEachSync<K, V>(coll: Collection<K, V> | V[], f: (v: V,
}
} else {
// I don't understand why we can't directly cast to `Collection.Set`.
for (const v of (coll as any as Collection.Set<V>).toArray()) {
for (const v of ((coll as any) as Collection.Set<V>).toArray()) {
// If the collection is a set, then `K` is the same as `v`,
// but TypeScript doesn't know this.
await f(v, v as any);
}
}
}
}
export async function mapSync<V, U>(coll: V[], f: (v: V, k: number) => Promise<U>): Promise<U[]>;
export async function mapSync<K, V, U>(coll: Collection.Keyed<K, V>, f: (v: V, k: K) => Promise<U>): Promise<Collection.Keyed<K, U>>;
export async function mapSync<K, V, U>(
coll: Collection.Keyed<K, V>,
f: (v: V, k: K) => Promise<U>
): Promise<Collection.Keyed<K, U>>;
export async function mapSync<V, U>(coll: Collection.Set<V>, f: (v: V, k: V) => Promise<U>): Promise<Collection.Set<U>>;
export async function mapSync<V, U>(coll: Collection.Indexed<V>, f: (v: V, k: number) => Promise<U>): Promise<Collection.Indexed<U>>;
export async function mapSync<K, V, U>(coll: Collection<K, V> | V[], f: (v: V, k: K) => Promise<U>): Promise<Collection<K, U> | U[]> {
export async function mapSync<V, U>(
coll: Collection.Indexed<V>,
f: (v: V, k: number) => Promise<U>
): Promise<Collection.Indexed<U>>;
export async function mapSync<K, V, U>(
coll: Collection<K, V> | V[],
f: (v: V, k: K) => Promise<U>
): Promise<Collection<K, U> | U[]> {
const results: U[] = [];
await forEachSync(coll as any, async (v, k) => {
results.push(await f(v as any, k as any));

Просмотреть файл

@ -5,12 +5,12 @@ import { OrderedSet, OrderedMap, Collection, Set, is, hash } from "immutable";
import { defined, panic, assert, assertNever } from "./Support";
import { TypeRef, TypeReconstituter } from "./TypeBuilder";
import { TypeNames, namesTypeAttributeKind } from "./TypeNames";
import { TypeAttributes } from "./TypeAttributes";
import { TypeAttributes, combineTypeAttributes } from "./TypeAttributes";
export type PrimitiveStringTypeKind = "string" | "date" | "time" | "date-time";
export type PrimitiveTypeKind = "none" | "any" | "null" | "bool" | "integer" | "double" | PrimitiveStringTypeKind;
export type NamedTypeKind = "class" | "enum" | "union";
export type TypeKind = PrimitiveTypeKind | NamedTypeKind | "array" | "map" | "intersection";
export type TypeKind = PrimitiveTypeKind | NamedTypeKind | "array" | "object" | "map" | "intersection";
export function isPrimitiveStringTypeKind(kind: TypeKind): kind is PrimitiveStringTypeKind {
return kind === "string" || kind === "date" || kind === "time" || kind === "date-time";
@ -261,57 +261,6 @@ export class ArrayType extends Type {
}
}
export class MapType extends Type {
// @ts-ignore: This is initialized in the Type constructor
readonly kind: "map";
constructor(typeRef: TypeRef, private _valuesRef?: TypeRef) {
super(typeRef, "map");
}
setValues(valuesRef: TypeRef) {
if (this._valuesRef !== undefined) {
return panic("Can only set map values once");
}
this._valuesRef = valuesRef;
}
private getValuesRef(): TypeRef {
if (this._valuesRef === undefined) {
return panic("Map values accessed before they were set");
}
return this._valuesRef;
}
get values(): Type {
return this.getValuesRef().deref()[0];
}
get children(): OrderedSet<Type> {
return OrderedSet([this.values]);
}
get isNullable(): boolean {
return false;
}
isPrimitive(): this is PrimitiveType {
return false;
}
map(builder: TypeReconstituter, f: (tref: TypeRef) => TypeRef): TypeRef {
// console.log(`${mapIndentation()}mapping ${this.kind}`);
// mapPath.push("{}");
const result = builder.getMapType(f(this.getValuesRef()));
// mapPath.pop();
return result;
}
protected structuralEqualityStep(other: MapType, queue: (a: Type, b: Type) => boolean): boolean {
return queue(this.values, other.values);
}
}
export class GenericClassProperty<T> {
constructor(readonly typeData: T, readonly isOptional: boolean) {}
@ -341,42 +290,27 @@ export class ClassProperty extends GenericClassProperty<TypeRef> {
}
}
function propertiesAreSorted(_props: OrderedMap<string, ClassProperty>): boolean {
/*
const keys = props.keySeq().toArray();
for (let i = 1; i < keys.length; i++) {
if (keys[i - 1] > keys[i]) return false;
}
*/
return true;
}
export class ObjectType extends Type {
constructor(
typeRef: TypeRef,
kind: TypeKind,
readonly isFixed: boolean,
readonly properties: OrderedMap<string, ClassProperty>,
private _additionalPropertiesRef: TypeRef | undefined
) {
super(typeRef, kind);
export class ClassType extends Type {
// @ts-ignore: This is initialized in the Type constructor
kind: "class";
constructor(typeRef: TypeRef, readonly isFixed: boolean, private _properties?: OrderedMap<string, ClassProperty>) {
super(typeRef, "class");
if (_properties !== undefined) {
assert(propertiesAreSorted(_properties));
assert(kind === "object" || kind === "map" || kind === "class");
if (kind === "map") {
assert(properties.isEmpty());
assert(!isFixed);
} else if (kind === "class") {
assert(_additionalPropertiesRef === undefined);
} else {
assert(isFixed);
}
}
setProperties(properties: OrderedMap<string, ClassProperty>): void {
assert(propertiesAreSorted(properties));
if (this._properties !== undefined) {
return panic("Can only set class properties once");
}
this._properties = properties;
}
get properties(): OrderedMap<string, ClassProperty> {
if (this._properties === undefined) {
return panic("Properties are not set yet");
}
return this._properties;
}
get sortedProperties(): OrderedMap<string, ClassProperty> {
const properties = this.properties;
const sortedKeys = properties.keySeq().sort();
@ -384,8 +318,18 @@ export class ClassType extends Type {
return OrderedMap(props);
}
get additionalProperties(): Type | undefined {
if (this._additionalPropertiesRef === undefined) return undefined;
return this._additionalPropertiesRef.deref()[0];
}
get children(): OrderedSet<Type> {
return this.sortedProperties.map(p => p.type).toOrderedSet();
const children = this.sortedProperties.map(p => p.type).toOrderedSet();
const additionalProperties = this.additionalProperties;
if (additionalProperties === undefined) {
return children;
}
return children.add(additionalProperties);
}
get isNullable(): boolean {
@ -407,14 +351,26 @@ export class ClassType extends Type {
// mapPath.pop();
return result;
});
if (this.isFixed) {
return builder.getUniqueClassType(true, properties);
} else {
return builder.getClassType(properties);
const additionalProperties =
this._additionalPropertiesRef === undefined ? undefined : f(this._additionalPropertiesRef);
switch (this.kind) {
case "object":
assert(this.isFixed);
return builder.getUniqueObjectType(properties, additionalProperties);
case "map":
return builder.getMapType(defined(additionalProperties));
case "class":
if (this.isFixed) {
return builder.getUniqueClassType(true, properties);
} else {
return builder.getClassType(properties);
}
default:
return panic(`Invalid object type kind ${this.kind}`);
}
}
protected structuralEqualityStep(other: ClassType, queue: (a: Type, b: Type) => boolean): boolean {
protected structuralEqualityStep(other: ObjectType, queue: (a: Type, b: Type) => boolean): boolean {
const pa = this.properties;
const pb = other.properties;
if (pa.size !== pb.size) return false;
@ -426,10 +382,43 @@ export class ClassType extends Type {
return false;
}
});
return !failed;
if (failed) return false;
if ((this.additionalProperties === undefined) !== (other.additionalProperties === undefined)) return false;
if (this.additionalProperties === undefined || other.additionalProperties === undefined) return true;
return queue(this.additionalProperties, other.additionalProperties);
}
}
export class ClassType extends ObjectType {
// @ts-ignore: This is initialized in the Type constructor
kind: "class";
constructor(typeRef: TypeRef, readonly isFixed: boolean, readonly properties: OrderedMap<string, ClassProperty>) {
super(typeRef, "class", isFixed, properties, undefined);
}
}
export class MapType extends ObjectType {
// @ts-ignore: This is initialized in the Type constructor
readonly kind: "map";
constructor(typeRef: TypeRef, valuesRef: TypeRef) {
super(typeRef, "map", false, OrderedMap(), valuesRef);
}
get values(): Type {
return defined(this.additionalProperties);
}
}
export function assertIsObject(t: Type): ObjectType {
if (t instanceof ObjectType) {
return t;
}
return panic("Supposed object type is not an object type");
}
export function assertIsClass(t: Type): ClassType {
if (!(t instanceof ClassType)) {
return panic("Supposed class type is not a class type");
@ -563,7 +552,13 @@ export class UnionType extends SetOperationType {
if (kinds.has("union") || kinds.has("intersection")) return false;
if (kinds.has("none") || kinds.has("any")) return false;
if (kinds.has("string") && kinds.has("enum")) return false;
if (kinds.has("class") && kinds.has("map")) return false;
let numObjectTypes = 0;
if (kinds.has("class")) numObjectTypes += 1;
if (kinds.has("map")) numObjectTypes += 1;
if (kinds.has("object")) numObjectTypes += 1;
if (numObjectTypes > 1) return false;
return true;
}
@ -573,6 +568,15 @@ export class UnionType extends SetOperationType {
}
}
export function combineTypeAttributesOfTypes(types: Collection<any, Type>): TypeAttributes {
return combineTypeAttributes(
types
.valueSeq()
.toArray()
.map(t => t.getAttributes())
);
}
export function setOperationCasesEqual(
ma: OrderedSet<Type>,
mb: OrderedSet<Type>,
@ -700,6 +704,7 @@ export function matchTypeExhaustive<U>(
arrayType: (arrayType: ArrayType) => U,
classType: (classType: ClassType) => U,
mapType: (mapType: MapType) => U,
objectType: (objectType: ObjectType) => U,
enumType: (enumType: EnumType) => U,
unionType: (unionType: UnionType) => U,
dateType: (dateType: PrimitiveType) => U,
@ -724,13 +729,14 @@ export function matchTypeExhaustive<U>(
} else if (t instanceof ArrayType) return arrayType(t);
else if (t instanceof ClassType) return classType(t);
else if (t instanceof MapType) return mapType(t);
else if (t instanceof ObjectType) return objectType(t);
else if (t instanceof EnumType) return enumType(t);
else if (t instanceof UnionType) return unionType(t);
return panic("Unknown Type");
return panic(`Unknown type ${t.kind}`);
}
export function matchType<U>(
t: Type,
type: Type,
anyType: (anyType: PrimitiveType) => U,
nullType: (nullType: PrimitiveType) => U,
boolType: (boolType: PrimitiveType) => U,
@ -744,8 +750,8 @@ export function matchType<U>(
unionType: (unionType: UnionType) => U,
stringTypeMatchers?: StringTypeMatchers<U>
): U {
function typeNotSupported(_: Type) {
return panic("Unsupported PrimitiveType");
function typeNotSupported(t: Type) {
return panic(`Unsupported type ${t.kind} in non-exhaustive match`);
}
if (stringTypeMatchers === undefined) {
@ -753,7 +759,7 @@ export function matchType<U>(
}
/* tslint:disable:strict-boolean-expressions */
return matchTypeExhaustive(
t,
type,
typeNotSupported,
anyType,
nullType,
@ -764,6 +770,7 @@ export function matchType<U>(
arrayType,
classType,
mapType,
typeNotSupported,
enumType,
unionType,
stringTypeMatchers.dateType || typeNotSupported,

Просмотреть файл

@ -17,7 +17,8 @@ import {
ClassProperty,
TypeKind,
matchTypeExhaustive,
IntersectionType
IntersectionType,
ObjectType
} from "./Type";
import { TypeGraph } from "./TypeGraph";
import {
@ -147,7 +148,7 @@ export const NoStringTypeMapping: StringTypeMapping = {
dateTime: "date-time"
};
export abstract class TypeBuilder {
export class TypeBuilder {
readonly typeGraph: TypeGraph;
protected topLevels: Map<string, TypeRef> = Map();
@ -317,6 +318,20 @@ export abstract class TypeBuilder {
return tref;
}
getUniqueObjectType(
attributes: TypeAttributes,
properties: OrderedMap<string, ClassProperty>,
additionalProperties: TypeRef | undefined,
forwardingRef?: TypeRef
): TypeRef {
properties = this.modifyPropertiesIfNecessary(properties);
return this.addType(
forwardingRef,
tref => new ObjectType(tref, "object", true, properties, additionalProperties),
attributes
);
}
getMapType(values: TypeRef, forwardingRef?: TypeRef): TypeRef {
let tref = this._mapTypes.get(values);
if (tref === undefined) {
@ -369,22 +384,11 @@ export abstract class TypeBuilder {
getUniqueClassType(
attributes: TypeAttributes,
isFixed: boolean,
properties?: OrderedMap<string, ClassProperty>,
properties: OrderedMap<string, ClassProperty>,
forwardingRef?: TypeRef
): TypeRef {
if (properties !== undefined) {
properties = this.modifyPropertiesIfNecessary(properties);
}
return this.addType(forwardingRef, tref => new ClassType(tref, isFixed, properties), attributes);
}
setClassProperties(ref: TypeRef, properties: OrderedMap<string, ClassProperty>): void {
const type = ref.deref()[0];
if (!(type instanceof ClassType)) {
return panic("Tried to set properties of non-class type");
}
properties = this.modifyPropertiesIfNecessary(properties);
type.setProperties(properties);
return this.addType(forwardingRef, tref => new ClassType(tref, isFixed, properties), attributes);
}
getUnionType(attributes: TypeAttributes, members: OrderedSet<TypeRef>, forwardingRef?: TypeRef): TypeRef {
@ -423,7 +427,9 @@ export abstract class TypeBuilder {
type.setMembers(members);
}
abstract setLostTypeAttributes(): void;
setLostTypeAttributes(): void {
return;
}
}
export interface TypeLookerUp {
@ -432,20 +438,6 @@ export interface TypeLookerUp {
registerUnion(typeRefs: TypeRef[], reconstituted: TypeRef): void;
}
export class TypeGraphBuilder extends TypeBuilder {
protected typeForEntry(entry: Type | undefined): Type | undefined {
return entry;
}
getLazyMapType(valuesCreator: () => TypeRef | undefined): TypeRef {
return this.addType(undefined, tref => new MapType(tref, valuesCreator()), undefined);
}
setLostTypeAttributes(): void {
return;
}
}
export class TypeReconstituter {
private _wasUsed: boolean = false;
@ -487,6 +479,15 @@ export class TypeReconstituter {
return this.addAttributes(this.useBuilder().getArrayType(items, this._forwardingRef));
}
getUniqueObjectType(
properties: OrderedMap<string, ClassProperty>,
additionalProperties: TypeRef | undefined
): TypeRef {
return this.addAttributes(
this.useBuilder().getUniqueObjectType(defined(this._typeAttributes), properties, additionalProperties)
);
}
getClassType(properties: OrderedMap<string, ClassProperty>): TypeRef {
if (this._makeClassUnique) {
return this.getUniqueClassType(false, properties);
@ -494,7 +495,7 @@ export class TypeReconstituter {
return this.useBuilder().getClassType(defined(this._typeAttributes), properties, this._forwardingRef);
}
getUniqueClassType(isFixed: boolean, properties?: OrderedMap<string, ClassProperty>): TypeRef {
getUniqueClassType(isFixed: boolean, properties: OrderedMap<string, ClassProperty>): TypeRef {
return this.useBuilder().getUniqueClassType(
defined(this._typeAttributes),
isFixed,
@ -694,10 +695,9 @@ export class GraphRewriteBuilder<T extends Type> extends TypeBuilder implements
// means we'll have to expose primitive types, too.
//
// FIXME: Also, only UnionAccumulator seems to implement it.
export interface UnionTypeProvider<TArrayData, TClassData, TMapData> {
export interface UnionTypeProvider<TArrayData, TObjectData> {
readonly arrayData: TArrayData;
readonly mapData: TMapData;
readonly classData: TClassData;
readonly objectData: TObjectData;
// FIXME: We're losing order here.
enumCaseMap: { [name: string]: number };
enumCases: string[];
@ -731,13 +731,12 @@ function moveAttributes<T extends TypeKind>(map: TypeAttributeMap<T>, fromKind:
return setAttributes(map, toKind, fromAttributes);
}
export class UnionAccumulator<TArray, TClass, TMap> implements UnionTypeProvider<TArray[], TClass[], TMap[]> {
export class UnionAccumulator<TArray, TObject> implements UnionTypeProvider<TArray[], TObject[]> {
private _nonStringTypeAttributes: TypeAttributeMap<TypeKind> = OrderedMap();
private _stringTypeAttributes: TypeAttributeMap<PrimitiveStringTypeKind | "enum"> = OrderedMap();
readonly arrayData: TArray[] = [];
readonly mapData: TMap[] = [];
readonly classData: TClass[] = [];
readonly objectData: TObject[] = [];
private _lostTypeAttributes: boolean = false;
@ -795,13 +794,9 @@ export class UnionAccumulator<TArray, TClass, TMap> implements UnionTypeProvider
this.arrayData.push(t);
this._nonStringTypeAttributes = setAttributes(this._nonStringTypeAttributes, "array", attributes);
}
addClass(t: TClass, attributes: TypeAttributes): void {
this.classData.push(t);
this._nonStringTypeAttributes = setAttributes(this._nonStringTypeAttributes, "class", attributes);
}
addMap(t: TMap, attributes: TypeAttributes): void {
this.mapData.push(t);
this._nonStringTypeAttributes = setAttributes(this._nonStringTypeAttributes, "map", attributes);
addObject(t: TObject, attributes: TypeAttributes): void {
this.objectData.push(t);
this._nonStringTypeAttributes = setAttributes(this._nonStringTypeAttributes, "object", attributes);
}
addEnumCases(cases: OrderedMap<string, number>, attributes: TypeAttributes): void {
@ -904,7 +899,7 @@ function attributesForTypes(types: Set<Type>): [OrderedMap<Type, TypeAttributes>
}
// FIXME: Move this to UnifyClasses.ts?
export class TypeRefUnionAccumulator extends UnionAccumulator<TypeRef, TypeRef, TypeRef> {
export class TypeRefUnionAccumulator extends UnionAccumulator<TypeRef, TypeRef> {
// There is a method analogous to this in the IntersectionAccumulator. It might
// make sense to find a common interface.
private addType(t: Type, attributes: TypeAttributes): void {
@ -927,8 +922,9 @@ export class TypeRefUnionAccumulator extends UnionAccumulator<TypeRef, TypeRef,
}
},
arrayType => this.addArray(arrayType.items.typeRef, attributes),
classType => this.addClass(classType.typeRef, attributes),
mapType => this.addMap(mapType.values.typeRef, attributes),
classType => this.addObject(classType.typeRef, attributes),
mapType => this.addObject(mapType.typeRef, attributes),
objectType => this.addObject(objectType.typeRef, attributes),
// FIXME: We're not carrying counts, so this is not correct if we do enum
// inference. JSON Schema input uses this case, however, without enum
// inference, which is fine, but still a bit ugly.
@ -949,7 +945,7 @@ export class TypeRefUnionAccumulator extends UnionAccumulator<TypeRef, TypeRef,
}
}
export abstract class UnionBuilder<TBuilder extends TypeBuilder, TArrayData, TClassData, TMapData> {
export abstract class UnionBuilder<TBuilder extends TypeBuilder, TArrayData, TObjectData> {
constructor(protected readonly typeBuilder: TBuilder) {}
protected abstract makeEnum(
@ -958,9 +954,8 @@ export abstract class UnionBuilder<TBuilder extends TypeBuilder, TArrayData, TCl
typeAttributes: TypeAttributes,
forwardingRef: TypeRef | undefined
): TypeRef;
protected abstract makeClass(
classes: TClassData,
maps: TMapData,
protected abstract makeObject(
objects: TObjectData,
typeAttributes: TypeAttributes,
forwardingRef: TypeRef | undefined
): TypeRef;
@ -971,7 +966,7 @@ export abstract class UnionBuilder<TBuilder extends TypeBuilder, TArrayData, TCl
): TypeRef;
private makeTypeOfKind(
typeProvider: UnionTypeProvider<TArrayData, TClassData, TMapData>,
typeProvider: UnionTypeProvider<TArrayData, TObjectData>,
kind: TypeKind,
typeAttributes: TypeAttributes,
forwardingRef: TypeRef | undefined
@ -993,12 +988,12 @@ export abstract class UnionBuilder<TBuilder extends TypeBuilder, TArrayData, TCl
return this.typeBuilder.getStringType(typeAttributes, undefined, forwardingRef);
case "enum":
return this.makeEnum(typeProvider.enumCases, typeProvider.enumCaseMap, typeAttributes, forwardingRef);
case "class":
return this.makeClass(typeProvider.classData, typeProvider.mapData, typeAttributes, forwardingRef);
case "object":
return this.makeObject(typeProvider.objectData, typeAttributes, forwardingRef);
case "array":
return this.makeArray(typeProvider.arrayData, typeAttributes, forwardingRef);
default:
if (kind === "union" || kind === "map" || kind === "intersection") {
if (kind === "union" || kind === "class" || kind === "map" || kind === "intersection") {
return panic(`getMemberKinds() shouldn't return ${kind}`);
}
return assertNever(kind);
@ -1006,7 +1001,7 @@ export abstract class UnionBuilder<TBuilder extends TypeBuilder, TArrayData, TCl
}
buildUnion(
typeProvider: UnionTypeProvider<TArrayData, TClassData, TMapData>,
typeProvider: UnionTypeProvider<TArrayData, TObjectData>,
unique: boolean,
typeAttributes: TypeAttributes,
forwardingRef?: TypeRef

Просмотреть файл

@ -2,7 +2,7 @@
import { Set, OrderedMap, OrderedSet, Map } from "immutable";
import { ClassType, Type, assertIsClass, ClassProperty, UnionType } from "./Type";
import { Type, ClassProperty, UnionType, ObjectType, combineTypeAttributesOfTypes, assertIsObject } from "./Type";
import {
TypeRef,
UnionBuilder,
@ -11,44 +11,99 @@ import {
GraphRewriteBuilder,
TypeRefUnionAccumulator
} from "./TypeBuilder";
import { panic, assert, defined } from "./Support";
import { panic, assert, defined, unionOfSets } from "./Support";
import { TypeNames, namesTypeAttributeKind } from "./TypeNames";
import { TypeAttributes, combineTypeAttributes } from "./TypeAttributes";
import { TypeAttributes, combineTypeAttributes, emptyTypeAttributes } from "./TypeAttributes";
function getCliqueProperties(
clique: ClassType[],
clique: ObjectType[],
makePropertyType: (attributes: TypeAttributes, types: OrderedSet<Type>) => TypeRef
): OrderedMap<string, ClassProperty> {
let properties = OrderedMap<string, [OrderedSet<Type>, number, boolean]>();
for (const c of clique) {
c.properties.forEach((cp, name) => {
let p = properties.get(name);
if (p === undefined) {
p = [OrderedSet(), 0, false];
properties = properties.set(name, p);
}
p[1] += 1;
p[0] = p[0].add(cp.type);
if (cp.isOptional) {
p[2] = true;
}
});
): [OrderedMap<string, ClassProperty>, TypeRef | undefined, boolean] {
let lostTypeAttributes = false;
let propertyNames = OrderedSet<string>();
for (const o of clique) {
propertyNames = propertyNames.union(o.properties.keySeq());
}
return properties.map(([types, count, isOptional], name) => {
isOptional = isOptional || count < clique.length;
let attributes = combineTypeAttributes(types.toArray().map(t => t.getAttributes()));
let properties = propertyNames
.toArray()
.map(name => [name, OrderedSet(), false] as [string, OrderedSet<Type>, boolean]);
let additionalProperties: OrderedSet<Type> | undefined = undefined;
for (const o of clique) {
let additional = o.additionalProperties;
if (additional !== undefined) {
if (additionalProperties === undefined) {
additionalProperties = OrderedSet();
}
if (additional !== undefined) {
additionalProperties = additionalProperties.add(additional);
}
}
for (let i = 0; i < properties.length; i++) {
let [name, types, isOptional] = properties[i];
const maybeProperty = o.properties.get(name);
if (maybeProperty === undefined) {
isOptional = true;
if (additional !== undefined && additional.kind !== "any") {
types = types.add(additional);
}
} else {
if (maybeProperty.isOptional) {
isOptional = true;
}
types = types.add(maybeProperty.type);
}
properties[i][1] = types;
properties[i][2] = isOptional;
}
}
const unifiedAdditionalProperties =
additionalProperties === undefined
? undefined
: makePropertyType(combineTypeAttributesOfTypes(additionalProperties), additionalProperties);
const unifiedPropertiesArray = properties.map(([name, types, isOptional]) => {
let attributes = combineTypeAttributesOfTypes(types);
attributes = namesTypeAttributeKind.setDefaultInAttributes(
attributes,
() => new TypeNames(OrderedSet([name]), OrderedSet(), true)
);
return new ClassProperty(makePropertyType(attributes, types), isOptional);
return [name, new ClassProperty(makePropertyType(attributes, types), isOptional)] as [string, ClassProperty];
});
const unifiedProperties = OrderedMap(unifiedPropertiesArray);
return [unifiedProperties, unifiedAdditionalProperties, lostTypeAttributes];
}
export class UnifyUnionBuilder extends UnionBuilder<TypeBuilder & TypeLookerUp, TypeRef[], TypeRef[], TypeRef[]> {
function countProperties(
clique: ObjectType[]
): { hasProperties: boolean; hasAdditionalProperties: boolean; hasNonAnyAdditionalProperties: boolean } {
let hasProperties = false;
let hasAdditionalProperties = false;
let hasNonAnyAdditionalProperties = false;
for (const o of clique) {
if (!o.properties.isEmpty()) {
hasProperties = true;
}
const additional = o.additionalProperties;
if (additional !== undefined) {
hasAdditionalProperties = true;
if (additional.kind !== "any") {
hasNonAnyAdditionalProperties = true;
}
}
}
return { hasProperties, hasAdditionalProperties, hasNonAnyAdditionalProperties };
}
export class UnifyUnionBuilder extends UnionBuilder<TypeBuilder & TypeLookerUp, TypeRef[], TypeRef[]> {
constructor(
typeBuilder: TypeBuilder & TypeLookerUp,
private readonly _makeEnums: boolean,
private readonly _makeObjectTypes: boolean,
private readonly _makeClassesFixed: boolean,
private readonly _unifyTypes: (typesToUnify: TypeRef[], typeAttributes: TypeAttributes) => TypeRef
) {
@ -68,30 +123,12 @@ export class UnifyUnionBuilder extends UnionBuilder<TypeBuilder & TypeLookerUp,
}
}
protected makeClass(
classes: TypeRef[],
maps: TypeRef[],
protected makeObject(
objectRefs: TypeRef[],
typeAttributes: TypeAttributes,
forwardingRef: TypeRef | undefined
): TypeRef {
if (maps.length > 0) {
const propertyTypes = maps.slice();
for (let classRef of classes) {
const c = assertIsClass(classRef.deref()[0]);
c.properties.forEach(cp => {
propertyTypes.push(cp.typeRef);
});
}
const t = this.typeBuilder.getMapType(this._unifyTypes(propertyTypes, Map()), forwardingRef);
this.typeBuilder.addAttributes(t, typeAttributes);
return t;
}
if (classes.length === 1) {
const t = this.typeBuilder.reconstituteTypeRef(classes[0], forwardingRef);
this.typeBuilder.addAttributes(t, typeAttributes);
return t;
}
const maybeTypeRef = this.typeBuilder.lookupTypeRefs(classes, forwardingRef);
const maybeTypeRef = this.typeBuilder.lookupTypeRefs(objectRefs, forwardingRef);
// FIXME: Comparing this to `forwardingRef` feels like it will come
// crashing on our heads eventually. The reason we need it here is
// because `unifyTypes` registers the union that we're supposed to
@ -102,19 +139,48 @@ export class UnifyUnionBuilder extends UnionBuilder<TypeBuilder & TypeLookerUp,
return maybeTypeRef;
}
const actualClasses: ClassType[] = classes.map(c => assertIsClass(c.deref()[0]));
const objectTypes = objectRefs.map(r => assertIsObject(r.deref()[0]));
const { hasProperties, hasAdditionalProperties, hasNonAnyAdditionalProperties } = countProperties(objectTypes);
let ref: TypeRef;
ref = this.typeBuilder.getUniqueClassType(typeAttributes, this._makeClassesFixed, undefined, forwardingRef);
const properties = getCliqueProperties(actualClasses, (names, types) => {
assert(types.size > 0, "Property has no type");
return this._unifyTypes(types.map(t => t.typeRef).toArray(), names);
});
this.typeBuilder.setClassProperties(ref, properties);
return ref;
if (!this._makeObjectTypes && (hasNonAnyAdditionalProperties || (!hasProperties && hasAdditionalProperties))) {
const propertyTypes = unionOfSets(objectTypes.map(o => o.properties.map(cp => cp.typeRef).toOrderedSet()));
const additionalPropertyTypes = OrderedSet(
objectTypes
.filter(o => o.additionalProperties !== undefined)
.map(o => defined(o.additionalProperties).typeRef)
);
const allPropertyTypes = propertyTypes.union(additionalPropertyTypes).toArray();
const tref = this.typeBuilder.getMapType(this._unifyTypes(allPropertyTypes, emptyTypeAttributes));
this.typeBuilder.addAttributes(tref, typeAttributes);
return tref;
} else {
const [properties, additionalProperties, lostTypeAttributes] = getCliqueProperties(
objectTypes,
(names, types) => {
assert(types.size > 0, "Property has no type");
return this._unifyTypes(types.map(t => t.typeRef).toArray(), names);
}
);
if (lostTypeAttributes) {
this.typeBuilder.setLostTypeAttributes();
}
if (this._makeObjectTypes) {
return this.typeBuilder.getUniqueObjectType(
typeAttributes,
properties,
additionalProperties,
forwardingRef
);
} else {
assert(additionalProperties === undefined, "We have additional properties but want to make a class");
return this.typeBuilder.getUniqueClassType(
typeAttributes,
this._makeClassesFixed,
properties,
forwardingRef
);
}
}
}
protected makeArray(
@ -131,15 +197,16 @@ export class UnifyUnionBuilder extends UnionBuilder<TypeBuilder & TypeLookerUp,
export function unionBuilderForUnification<T extends Type>(
typeBuilder: GraphRewriteBuilder<T>,
makeEnums: boolean,
makeObjectTypes: boolean,
makeClassesFixed: boolean,
conflateNumbers: boolean
): UnionBuilder<TypeBuilder & TypeLookerUp, TypeRef[], TypeRef[], TypeRef[]> {
return new UnifyUnionBuilder(typeBuilder, makeEnums, makeClassesFixed, (trefs, names) =>
): UnionBuilder<TypeBuilder & TypeLookerUp, TypeRef[], TypeRef[]> {
return new UnifyUnionBuilder(typeBuilder, makeEnums, makeObjectTypes, makeClassesFixed, (trefs, names) =>
unifyTypes(
Set(trefs.map(tref => tref.deref()[0])),
names,
typeBuilder,
unionBuilderForUnification(typeBuilder, makeEnums, makeClassesFixed, conflateNumbers),
unionBuilderForUnification(typeBuilder, makeEnums, makeObjectTypes, makeClassesFixed, conflateNumbers),
conflateNumbers
)
);
@ -150,7 +217,7 @@ export function unifyTypes<T extends Type>(
types: Set<Type>,
typeAttributes: TypeAttributes,
typeBuilder: GraphRewriteBuilder<T>,
unionBuilder: UnionBuilder<TypeBuilder & TypeLookerUp, TypeRef[], TypeRef[], TypeRef[]>,
unionBuilder: UnionBuilder<TypeBuilder & TypeLookerUp, TypeRef[], TypeRef[]>,
conflateNumbers: boolean,
maybeForwardingRef?: TypeRef
): TypeRef {

Просмотреть файл

@ -14,7 +14,7 @@ import { addTypesInSchema, Ref, definitionRefsInSchema, checkJSONSchema } from "
import { JSONSchema, JSONSchemaStore } from "./JSONSchemaStore";
import { TypeInference } from "./Inference";
import { inferMaps } from "./InferMaps";
import { TypeGraphBuilder } from "./TypeBuilder";
import { TypeBuilder } from "./TypeBuilder";
import { TypeGraph, noneToAny, optionalToNullable } from "./TypeGraph";
import { makeNamesTypeAttributes } from "./TypeNames";
import { makeGraphQLQueryTypes } from "./GraphQL";
@ -23,6 +23,7 @@ import { inferEnums, flattenStrings } from "./InferEnums";
import { descriptionTypeAttributeKind } from "./TypeAttributes";
import { flattenUnions } from "./FlattenUnions";
import { resolveIntersections } from "./ResolveIntersections";
import { replaceObjectType } from "./ReplaceObjectType";
// Re-export essential types and functions
export { TargetLanguage } from "./TargetLanguage";
@ -179,7 +180,7 @@ export class Run {
const stringTypeMapping = targetLanguage.stringTypeMapping;
const conflateNumbers = !targetLanguage.supportsUnionsWithBothNumberTypes;
const haveSchemas = Object.getOwnPropertyNames(this._allInputs.schemas).length > 0;
const typeBuilder = new TypeGraphBuilder(
const typeBuilder = new TypeBuilder(
stringTypeMapping,
this._options.alphabetizeProperties,
this._options.allPropertiesOptional,
@ -232,16 +233,16 @@ export class Run {
graph.printGraph();
}
let unionsDone = false;
if (haveSchemas) {
let intersectionsDone = false;
let unionsDone = false;
do {
const graphBeforeRewrites = graph;
if (!intersectionsDone) {
[graph, intersectionsDone] = resolveIntersections(graph, stringTypeMapping);
}
if (!unionsDone) {
[graph, unionsDone] = flattenUnions(graph, stringTypeMapping, conflateNumbers);
[graph, unionsDone] = flattenUnions(graph, stringTypeMapping, conflateNumbers, true);
}
if (graph === graphBeforeRewrites) {
@ -250,6 +251,11 @@ export class Run {
} while (!intersectionsDone || !unionsDone);
}
graph = replaceObjectType(graph, stringTypeMapping, conflateNumbers);
do {
[graph, unionsDone] = flattenUnions(graph, stringTypeMapping, conflateNumbers, false);
} while (!unionsDone);
if (this._options.findSimilarClassesSchemaURI !== undefined) {
return graph;
}

Просмотреть файл

@ -0,0 +1,4 @@
{
"foo": "abc",
"bar": "def"
}

Просмотреть файл

@ -0,0 +1,3 @@
{
"foo": "abc"
}

Просмотреть файл

@ -0,0 +1,36 @@
{
"allOf": [
{
"type": "object",
"properties": {
"foo": {
"type": "string"
}
},
"required": [
"foo"
]
},
{
"oneOf": [
{
"type": "object",
"properties": {
"bar": {
"type": "string"
}
},
"required": [
"bar"
]
},
{
"type": "object",
"additionalProperties": {
"type": "string"
}
}
]
}
]
}