Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions packages/quicktype-core/src/Run.ts
Original file line number Diff line number Diff line change
Expand Up @@ -342,6 +342,7 @@ class Run implements RunContext {
stringTypeMapping,
conflateNumbers,
true,
targetLanguage.supportsUnionsWithMultipleObjectTypes,
debugPrintReconstitution,
);
});
Expand Down Expand Up @@ -372,6 +373,7 @@ class Run implements RunContext {
stringTypeMapping,
conflateNumbers,
false,
targetLanguage.supportsUnionsWithMultipleObjectTypes,
debugPrintReconstitution,
);
});
Expand Down Expand Up @@ -436,6 +438,7 @@ class Run implements RunContext {
stringTypeMapping,
conflateNumbers,
false,
targetLanguage.supportsUnionsWithMultipleObjectTypes,
debugPrintReconstitution,
);
});
Expand Down Expand Up @@ -485,6 +488,7 @@ class Run implements RunContext {
stringTypeMapping,
conflateNumbers,
false,
targetLanguage.supportsUnionsWithMultipleObjectTypes,
debugPrintReconstitution,
);
});
Expand Down
4 changes: 4 additions & 0 deletions packages/quicktype-core/src/TargetLanguage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,10 @@ export abstract class TargetLanguage<
return false;
}

public get supportsUnionsWithMultipleObjectTypes(): boolean {
return false;
}

public needsTransformerForType(_t: Type): boolean {
return false;
}
Expand Down
81 changes: 56 additions & 25 deletions packages/quicktype-core/src/Type/Type.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ import {
hashCodeOf,
iterableEvery,
iterableFind,
iterableSome,
mapFilter,
mapMap,
mapSome,
Expand All @@ -20,6 +19,7 @@ import {
} from "collection-utils";

import type { TypeAttributes } from "../attributes/TypeAttributes.js";
import { isUnionMemberDistinct } from "../attributes/UnionMembers.js";
import {
type TypeNames,
namesTypeAttributeKind,
Expand Down Expand Up @@ -780,25 +780,45 @@ export function setOperationCasesEqual(
const ma = toReadonlySet(typesA);
const mb = toReadonlySet(typesB);
if (ma.size !== mb.size) return false;
return iterableEvery(ma, (ta) => {
const tb = iterableFind(mb, (t) => t.kind === ta.kind);
if (tb !== undefined && membersEqual(ta, tb)) return true;

if (conflateNumbers) {
if (
ta.kind === "integer" &&
iterableSome(mb, (t) => t.kind === "double")
)
return true;
if (
ta.kind === "double" &&
iterableSome(mb, (t) => t.kind === "integer")
)
return true;
function kindForComparison(t: Type): TypeKind {
if (conflateNumbers && (t.kind === "integer" || t.kind === "double")) {
return "double";
}

return false;
});
return t.kind;
}

function groupByKind(types: ReadonlySet<Type>): Map<TypeKind, Type[]> {
const groups = new Map<TypeKind, Type[]>();
for (const t of types) {
const kind = kindForComparison(t);
const group = groups.get(kind);
if (group === undefined) {
groups.set(kind, [t]);
} else {
group.push(t);
}
}

return groups;
}

const groupsA = groupByKind(ma);
const groupsB = groupByKind(mb);
if (groupsA.size !== groupsB.size) return false;
for (const [kind, groupA] of groupsA) {
const groupB = groupsB.get(kind);
if (groupB === undefined || groupA.length !== groupB.length) {
return false;
}

for (let i = 0; i < groupA.length; i++) {
if (!membersEqual(groupA[i], groupB[i])) return false;
}
}

return true;
}

export function setOperationTypeIdentity(
Expand Down Expand Up @@ -861,7 +881,6 @@ export abstract class SetOperationType extends Type {
}

public getNonAttributeChildren(): Set<Type> {
// FIXME: We're assuming no two members of the same kind.
return setSortBy(this.members, (t) => t.kind);
}

Expand Down Expand Up @@ -980,18 +999,30 @@ export class UnionType extends SetOperationType {
const members = this.members;
if (members.size <= 1) return false;
const kinds = setMap(members, (t) => t.kind);
if (kinds.size < members.size) return false;
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;

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;
const objectMembers = setFilter(
members,
(member) => member instanceof ObjectType,
);
const nonObjectMembers = setFilter(
members,
(member) => !(member instanceof ObjectType),
);
const nonObjectKinds = setMap(
nonObjectMembers,
(member) => member.kind,
);
if (nonObjectKinds.size < nonObjectMembers.size) return false;

return true;
return (
objectMembers.size <= 1 ||
iterableEvery(objectMembers, (member) =>
isUnionMemberDistinct(member.getAttributes()),
)
);
}

public reconstitute<T extends BaseGraphRewriteBuilder>(
Expand Down
39 changes: 39 additions & 0 deletions packages/quicktype-core/src/UnifyClasses.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,10 @@ import {
combineTypeAttributes,
emptyTypeAttributes,
} from "./attributes/TypeAttributes.js";
import {
isUnionMemberDistinct,
unionMemberDistinctAttributes,
} from "./attributes/UnionMembers.js";
import type {
BaseGraphRewriteBuilder,
GraphRewriteBuilder,
Expand Down Expand Up @@ -125,11 +129,45 @@ export class UnifyUnionBuilder extends UnionBuilder<
typeBuilder: BaseGraphRewriteBuilder,
private readonly _makeObjectTypes: boolean,
private readonly _makeClassesFixed: boolean,
private readonly _preserveDistinctObjects: boolean,
private readonly _unifyTypes: (typesToUnify: TypeRef[]) => TypeRef,
) {
super(typeBuilder);
}

protected objectsAreDistinct(
objectRefs: TypeRef[],
attributes: TypeAttributes,
): boolean {
return (
this._preserveDistinctObjects &&
objectRefs.length > 1 &&
isUnionMemberDistinct(attributes) &&
// Renderers cannot represent properties that are exceptions to a
// typed additionalProperties index signature.
objectRefs.every((ref) => {
const object = assertIsObject(
derefTypeRef(ref, this.typeBuilder),
);
const additional = object.getAdditionalProperties();
return (
object.getProperties().size === 0 ||
additional === undefined ||
additional.kind === "any"
);
})
);
}

protected makeDistinctObjects(objectRefs: TypeRef[]): TypeRef[] {
return objectRefs.map((ref) =>
this.typeBuilder.reconstituteTypeRef(
ref,
unionMemberDistinctAttributes,
),
);
}

protected makeObject(
objectRefs: TypeRef[],
typeAttributes: TypeAttributes,
Expand Down Expand Up @@ -249,6 +287,7 @@ export function unionBuilderForUnification<T extends Type>(
typeBuilder,
makeObjectTypes,
makeClassesFixed,
false,
(trefs) =>
unifyTypes(
new Set(trefs.map((tref) => derefTypeRef(tref, typeBuilder))),
Expand Down
59 changes: 52 additions & 7 deletions packages/quicktype-core/src/UnionBuilder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -503,6 +503,17 @@ export abstract class UnionBuilder<
forwardingRef: TypeRef | undefined,
): TypeRef;

protected objectsAreDistinct(
_objects: TObjectData,
_attributes: TypeAttributes,
): boolean {
return false;
}

protected makeDistinctObjects(_objects: TObjectData): TypeRef[] {
return panic("This union builder cannot make distinct object types");
}

private makeTypeOfKind(
typeProvider: UnionTypeProvider<TArrayData, TObjectData>,
kind: TypeKind,
Expand Down Expand Up @@ -574,6 +585,28 @@ export abstract class UnionBuilder<
// right now, it's just a very bad way of surfacing that error.
if (kinds.size === 1) {
const [[kind, memberAttributes]] = Array.from(kinds);
if (
kind === "object" &&
this.objectsAreDistinct(
typeProvider.objectData,
memberAttributes,
)
) {
const objects = this.makeDistinctObjects(
typeProvider.objectData,
);
const objectSet = new Set(objects);
assert(
objectSet.size > 1,
"Distinct object union must have multiple members",
);
return this.typeBuilder.getUnionType(
typeAttributes,
objectSet,
forwardingRef,
);
}

const allAttributes = combineTypeAttributes(
"union",
typeAttributes,
Expand All @@ -598,14 +631,26 @@ export abstract class UnionBuilder<

const types: TypeRef[] = [];
for (const [kind, memberAttributes] of kinds) {
types.push(
this.makeTypeOfKind(
typeProvider,
kind,
if (
kind === "object" &&
this.objectsAreDistinct(
typeProvider.objectData,
memberAttributes,
undefined,
),
);
)
) {
types.push(
...this.makeDistinctObjects(typeProvider.objectData),
);
} else {
types.push(
this.makeTypeOfKind(
typeProvider,
kind,
memberAttributes,
undefined,
),
);
}
}

const typesSet = new Set(types);
Expand Down
61 changes: 61 additions & 0 deletions packages/quicktype-core/src/attributes/UnionMembers.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import { TypeAttributeKind, type TypeAttributes } from "./TypeAttributes.js";

class UnionMemberDistinctTypeAttributeKind extends TypeAttributeKind<boolean> {
public constructor() {
super("unionMemberDistinct");
}

public combine(values: boolean[]): boolean {
return values.some(Boolean);
}

public intersect(values: boolean[]): boolean {
return values.some(Boolean);
}

public makeInferred(value: boolean): boolean {
return value;
}
}

class UnionIsExclusiveTypeAttributeKind extends TypeAttributeKind<boolean> {
public constructor() {
super("unionIsExclusive");
}

public combine(values: boolean[]): boolean {
return values.some(Boolean);
}

public intersect(values: boolean[]): boolean {
return values.some(Boolean);
}

public makeInferred(value: boolean): boolean {
return value;
}
}

export const unionMemberDistinctTypeAttributeKind =
new UnionMemberDistinctTypeAttributeKind();
const unionIsExclusiveTypeAttributeKind =
new UnionIsExclusiveTypeAttributeKind();

export const unionMemberDistinctAttributes: TypeAttributes =
unionMemberDistinctTypeAttributeKind.makeAttributes(true);
export const unionIsExclusiveAttributes: TypeAttributes =
unionIsExclusiveTypeAttributeKind.makeAttributes(true);

export function isUnionMemberDistinct(attributes: TypeAttributes): boolean {
return (
unionMemberDistinctTypeAttributeKind.tryGetInAttributes(attributes) ===
true
);
}

export function isUnionExclusive(attributes: TypeAttributes): boolean {
return (
unionIsExclusiveTypeAttributeKind.tryGetInAttributes(attributes) ===
true
);
}
Loading
Loading