Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: merge compatible definitions in union types #722

Draft
wants to merge 7 commits into
base: next
Choose a base branch
from
Draft
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
32 changes: 13 additions & 19 deletions src/TypeFormatter/UnionTypeFormatter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { SubTypeFormatter } from "../SubTypeFormatter";
import { BaseType } from "../Type/BaseType";
import { UnionType } from "../Type/UnionType";
import { TypeFormatter } from "../TypeFormatter";
import { mergeDefinitions } from "../Utils/mergeDefinitions";
import { uniqueArray } from "../Utils/uniqueArray";

export class UnionTypeFormatter implements SubTypeFormatter {
Expand All @@ -15,25 +16,6 @@ export class UnionTypeFormatter implements SubTypeFormatter {
public getDefinition(type: UnionType): Definition {
const definitions = type.getTypes().map((item) => this.childTypeFormatter.getDefinition(item));

// TODO: why is this not covered by LiteralUnionTypeFormatter?
// special case for string literals | string -> string
let stringType = true;
let oneNotEnum = false;
for (const def of definitions) {
if (def.type !== "string") {
stringType = false;
break;
}
if (def.enum === undefined) {
oneNotEnum = true;
}
}
if (stringType && oneNotEnum) {
return {
type: "string",
};
}

const flattenedDefinitions: JSONSchema7[] = [];

// Flatten anyOf inside anyOf unless the anyOf has an annotation
Expand All @@ -45,6 +27,18 @@ export class UnionTypeFormatter implements SubTypeFormatter {
}
}

for (let idx = 0; idx < flattenedDefinitions.length - 1; idx++) {
for (let comp = idx + 1; comp < flattenedDefinitions.length; ) {
const merged = mergeDefinitions(flattenedDefinitions[idx], flattenedDefinitions[comp]);
if (merged) {
flattenedDefinitions[idx] = merged;
flattenedDefinitions.splice(comp, 1);
} else {
comp++;
}
}
}

return flattenedDefinitions.length > 1
? {
anyOf: flattenedDefinitions,
Expand Down
230 changes: 230 additions & 0 deletions src/Utils/makeExemplar.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,230 @@
import { AliasType } from "../Type/AliasType";
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This file is only used in tests, right? Then it shouldn't be in lib utils.

import { AnnotatedType } from "../Type/AnnotatedType";
import { ArrayType } from "../Type/ArrayType";
import { BaseType } from "../Type/BaseType";
import { BooleanType } from "../Type/BooleanType";
import { DefinitionType } from "../Type/DefinitionType";
import { EnumType } from "../Type/EnumType";
import { IntersectionType } from "../Type/IntersectionType";
import { LiteralType } from "../Type/LiteralType";
import { NullType } from "../Type/NullType";
import { NumberType } from "../Type/NumberType";
import { ObjectType } from "../Type/ObjectType";
import { OptionalType } from "../Type/OptionalType";
import { ReferenceType } from "../Type/ReferenceType";
import { RestType } from "../Type/RestType";
import { StringType } from "../Type/StringType";
import { SymbolType } from "../Type/SymbolType";
import { TupleType } from "../Type/TupleType";
import { UndefinedType } from "../Type/UndefinedType";
import { UnionType } from "../Type/UnionType";

export function makeExemplar(type: BaseType | undefined): unknown {
return makeExemplars(type)[0];

Check warning on line 23 in src/Utils/makeExemplar.ts

View check run for this annotation

Codecov / codecov/patch

src/Utils/makeExemplar.ts#L22-L23

Added lines #L22 - L23 were not covered by tests
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There is a lot of untested code in this file.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, I definitely need to refactor this into something cleaner. On my to-do list, certainly.

}

export function makeExemplars(type: BaseType | undefined): readonly unknown[] {
while (type) {
if (
type instanceof AliasType ||
type instanceof AnnotatedType ||
type instanceof DefinitionType ||
type instanceof ReferenceType
) {
type = type.getType();
} else if (type instanceof ArrayType) {
const itemExemplars = makeExemplars(type.getItem());
return [[], itemExemplars].concat(itemExemplars.map((e) => [e, e]));

Check warning on line 37 in src/Utils/makeExemplar.ts

View check run for this annotation

Codecov / codecov/patch

src/Utils/makeExemplar.ts#L36-L37

Added lines #L36 - L37 were not covered by tests
} else if (type instanceof BooleanType) {
return [true, false];

Check warning on line 39 in src/Utils/makeExemplar.ts

View check run for this annotation

Codecov / codecov/patch

src/Utils/makeExemplar.ts#L39

Added line #L39 was not covered by tests
} else if (type instanceof EnumType) {
return type.getValues();

Check warning on line 41 in src/Utils/makeExemplar.ts

View check run for this annotation

Codecov / codecov/patch

src/Utils/makeExemplar.ts#L41

Added line #L41 was not covered by tests
} else if (type instanceof IntersectionType) {
return makeIntersectionExemplars(type);

Check warning on line 43 in src/Utils/makeExemplar.ts

View check run for this annotation

Codecov / codecov/patch

src/Utils/makeExemplar.ts#L43

Added line #L43 was not covered by tests
} else if (type instanceof LiteralType) {
return [type.getValue()];
} else if (type instanceof NullType) {
return [null];

Check warning on line 47 in src/Utils/makeExemplar.ts

View check run for this annotation

Codecov / codecov/patch

src/Utils/makeExemplar.ts#L47

Added line #L47 was not covered by tests
} else if (type instanceof NumberType) {
return [0, 1, -1];

Check warning on line 49 in src/Utils/makeExemplar.ts

View check run for this annotation

Codecov / codecov/patch

src/Utils/makeExemplar.ts#L49

Added line #L49 was not covered by tests
} else if (type instanceof ObjectType) {
return makeObjectExemplars(type);
} else if (type instanceof OptionalType) {
return makeExemplars(type.getType()).concat([undefined]);

Check warning on line 53 in src/Utils/makeExemplar.ts

View check run for this annotation

Codecov / codecov/patch

src/Utils/makeExemplar.ts#L53

Added line #L53 was not covered by tests
} else if (type instanceof RestType) {
const exemplar = makeExemplars(type.getType());
return [[], [exemplar], [exemplar, exemplar]];

Check warning on line 56 in src/Utils/makeExemplar.ts

View check run for this annotation

Codecov / codecov/patch

src/Utils/makeExemplar.ts#L55-L56

Added lines #L55 - L56 were not covered by tests
} else if (type instanceof StringType) {
return ["", "lorem ipsum"];

Check warning on line 58 in src/Utils/makeExemplar.ts

View check run for this annotation

Codecov / codecov/patch

src/Utils/makeExemplar.ts#L58

Added line #L58 was not covered by tests
} else if (type instanceof SymbolType) {
return [Symbol(), Symbol()];

Check warning on line 60 in src/Utils/makeExemplar.ts

View check run for this annotation

Codecov / codecov/patch

src/Utils/makeExemplar.ts#L60

Added line #L60 was not covered by tests
} else if (type instanceof TupleType) {
return makeTupleExemplars(type);

Check warning on line 62 in src/Utils/makeExemplar.ts

View check run for this annotation

Codecov / codecov/patch

src/Utils/makeExemplar.ts#L62

Added line #L62 was not covered by tests
} else if (type instanceof UndefinedType) {
return [undefined];

Check warning on line 64 in src/Utils/makeExemplar.ts

View check run for this annotation

Codecov / codecov/patch

src/Utils/makeExemplar.ts#L64

Added line #L64 was not covered by tests
} else if (type instanceof UnionType) {
return type
.getTypes()
.map((t) => makeExemplars(t))
.reduce((list, choice) => list.concat(choice), []);
} else {
throw new Error(`Can't make exemplar from type ${type.constructor.name}: ${type}`);

Check warning on line 71 in src/Utils/makeExemplar.ts

View check run for this annotation

Codecov / codecov/patch

src/Utils/makeExemplar.ts#L71

Added line #L71 was not covered by tests
}
}
return [undefined];

Check warning on line 74 in src/Utils/makeExemplar.ts

View check run for this annotation

Codecov / codecov/patch

src/Utils/makeExemplar.ts#L74

Added line #L74 was not covered by tests
}

type UnknownObject = Record<string, unknown>;

function makeIntersectionExemplars(type: IntersectionType): unknown[] {
const warnings: string[] = [];

Check warning on line 80 in src/Utils/makeExemplar.ts

View check run for this annotation

Codecov / codecov/patch

src/Utils/makeExemplar.ts#L79-L80

Added lines #L79 - L80 were not covered by tests
function intersectExemplars(
exemplars: (readonly unknown[])[],
currentResult: unknown,
members: UnknownObject[]
): unknown[] {
for (let i = 0; i < exemplars.length; i++) {
const choices = exemplars[i];

Check warning on line 87 in src/Utils/makeExemplar.ts

View check run for this annotation

Codecov / codecov/patch

src/Utils/makeExemplar.ts#L85-L87

Added lines #L85 - L87 were not covered by tests
if (choices.length > 1) {
return choices
.map((choice) => {
const subExemplars = exemplars.slice(i); // including the one with the multiple-choice element
subExemplars[0] = [choice]; // ...and overwriting it with a single choice
const subMembers = members.slice();
return intersectExemplars(subExemplars, currentResult, subMembers);

Check warning on line 94 in src/Utils/makeExemplar.ts

View check run for this annotation

Codecov / codecov/patch

src/Utils/makeExemplar.ts#L89-L94

Added lines #L89 - L94 were not covered by tests
})
.reduce((list, choice) => list.concat(choice), []);

Check warning on line 96 in src/Utils/makeExemplar.ts

View check run for this annotation

Codecov / codecov/patch

src/Utils/makeExemplar.ts#L96

Added line #L96 was not covered by tests
} else if (choices.length === 0) {
return [];

Check warning on line 98 in src/Utils/makeExemplar.ts

View check run for this annotation

Codecov / codecov/patch

src/Utils/makeExemplar.ts#L98

Added line #L98 was not covered by tests
}
const exemplar = choices[0];

Check warning on line 100 in src/Utils/makeExemplar.ts

View check run for this annotation

Codecov / codecov/patch

src/Utils/makeExemplar.ts#L100

Added line #L100 was not covered by tests
if (exemplar == null) {
warnings.push(`Can't make exemplar from intersection with null/undefined`);
return [];

Check warning on line 103 in src/Utils/makeExemplar.ts

View check run for this annotation

Codecov / codecov/patch

src/Utils/makeExemplar.ts#L102-L103

Added lines #L102 - L103 were not covered by tests
} else if (exemplar && typeof exemplar === "object" && !Array.isArray(exemplar)) {
members.push(exemplar as UnknownObject);

Check warning on line 105 in src/Utils/makeExemplar.ts

View check run for this annotation

Codecov / codecov/patch

src/Utils/makeExemplar.ts#L105

Added line #L105 was not covered by tests
} else {
// We can only have one non-object member. It will become the base we add all the others to.
if (currentResult !== undefined && exemplar !== currentResult) {
warnings.push(`Can't make exemplar from complex intersection`);
return [];

Check warning on line 110 in src/Utils/makeExemplar.ts

View check run for this annotation

Codecov / codecov/patch

src/Utils/makeExemplar.ts#L109-L110

Added lines #L109 - L110 were not covered by tests
} else {
currentResult = exemplar;

Check warning on line 112 in src/Utils/makeExemplar.ts

View check run for this annotation

Codecov / codecov/patch

src/Utils/makeExemplar.ts#L112

Added line #L112 was not covered by tests
}
}
}

// We've gotten here, which means we have exactly one choice at this level of recursion.
// Now we just need to merge the intersection members.

if (members.length === 0) {
// no properties to add, just return the base value
return [currentResult];

Check warning on line 122 in src/Utils/makeExemplar.ts

View check run for this annotation

Codecov / codecov/patch

src/Utils/makeExemplar.ts#L122

Added line #L122 was not covered by tests
}
let result: UnknownObject;
if (currentResult === undefined) {
result = {};

Check warning on line 126 in src/Utils/makeExemplar.ts

View check run for this annotation

Codecov / codecov/patch

src/Utils/makeExemplar.ts#L126

Added line #L126 was not covered by tests
} else if (typeof currentResult !== "object") {
// for primitive values, box them to allow adding properties
result = new (currentResult as any).constructor(currentResult);

Check warning on line 129 in src/Utils/makeExemplar.ts

View check run for this annotation

Codecov / codecov/patch

src/Utils/makeExemplar.ts#L129

Added line #L129 was not covered by tests
} else if (Array.isArray(currentResult)) {
result = currentResult.slice() as any;

Check warning on line 131 in src/Utils/makeExemplar.ts

View check run for this annotation

Codecov / codecov/patch

src/Utils/makeExemplar.ts#L131

Added line #L131 was not covered by tests
} else {
result = Object.assign({}, currentResult);

Check warning on line 133 in src/Utils/makeExemplar.ts

View check run for this annotation

Codecov / codecov/patch

src/Utils/makeExemplar.ts#L133

Added line #L133 was not covered by tests
}

const collisions: Record<string, unknown[]> = {};

Check warning on line 136 in src/Utils/makeExemplar.ts

View check run for this annotation

Codecov / codecov/patch

src/Utils/makeExemplar.ts#L136

Added line #L136 was not covered by tests

for (const member of members) {
for (const [key, value] of Object.entries(member)) {

Check warning on line 139 in src/Utils/makeExemplar.ts

View check run for this annotation

Codecov / codecov/patch

src/Utils/makeExemplar.ts#L138-L139

Added lines #L138 - L139 were not covered by tests
if (Object.getOwnPropertyDescriptor(result, key)) {
if (!(key in collisions)) {
collisions[key] = [result[key]];

Check warning on line 142 in src/Utils/makeExemplar.ts

View check run for this annotation

Codecov / codecov/patch

src/Utils/makeExemplar.ts#L142

Added line #L142 was not covered by tests
}
collisions[key].push(value);

Check warning on line 144 in src/Utils/makeExemplar.ts

View check run for this annotation

Codecov / codecov/patch

src/Utils/makeExemplar.ts#L144

Added line #L144 was not covered by tests
} else {
result[key] = value;

Check warning on line 146 in src/Utils/makeExemplar.ts

View check run for this annotation

Codecov / codecov/patch

src/Utils/makeExemplar.ts#L146

Added line #L146 was not covered by tests
}
}
}

return resolveObjectChoices(result, Object.entries(collisions));

Check warning on line 151 in src/Utils/makeExemplar.ts

View check run for this annotation

Codecov / codecov/patch

src/Utils/makeExemplar.ts#L151

Added line #L151 was not covered by tests
}

const choices = intersectExemplars(
type.getTypes().map((t) => makeExemplars(t)),

Check warning on line 155 in src/Utils/makeExemplar.ts

View check run for this annotation

Codecov / codecov/patch

src/Utils/makeExemplar.ts#L154-L155

Added lines #L154 - L155 were not covered by tests
undefined,
[]
);
if (choices.length === 0) {
throw new Error(`Could not make intersection; warnings=${JSON.stringify(warnings)}`);

Check warning on line 160 in src/Utils/makeExemplar.ts

View check run for this annotation

Codecov / codecov/patch

src/Utils/makeExemplar.ts#L160

Added line #L160 was not covered by tests
}
return choices;

Check warning on line 162 in src/Utils/makeExemplar.ts

View check run for this annotation

Codecov / codecov/patch

src/Utils/makeExemplar.ts#L162

Added line #L162 was not covered by tests
}

function resolveObjectChoices(exemplar: UnknownObject, choiceEntries: [string, readonly unknown[]][]): UnknownObject[] {
if (choiceEntries.length === 0) {
return [exemplar];
}
const [prop, choices] = choiceEntries[0];
const results: UnknownObject[] = [];
for (const choice of choices) {
const newExemplar = new (exemplar.constructor as new (val: UnknownObject) => UnknownObject)(exemplar);
newExemplar[prop] = choice;
results.push(...resolveObjectChoices(exemplar, choiceEntries.slice(1)));

Check warning on line 174 in src/Utils/makeExemplar.ts

View check run for this annotation

Codecov / codecov/patch

src/Utils/makeExemplar.ts#L169-L174

Added lines #L169 - L174 were not covered by tests
}
return results;

Check warning on line 176 in src/Utils/makeExemplar.ts

View check run for this annotation

Codecov / codecov/patch

src/Utils/makeExemplar.ts#L176

Added line #L176 was not covered by tests
}

function makeObjectExemplars(type: ObjectType): readonly unknown[] {
const fullObject: UnknownObject = {};
const emptyObject: UnknownObject = {};
const choices: [string, readonly unknown[]][] = [];
let hasOptional = false;
for (const prop of type.getProperties()) {
const name = prop.getName();
const values = makeExemplars(prop.getType());
if (values.length === 0) {
throw new Error(`Cannot make object exemplar with invalid property for type ${type}`);

Check warning on line 188 in src/Utils/makeExemplar.ts

View check run for this annotation

Codecov / codecov/patch

src/Utils/makeExemplar.ts#L188

Added line #L188 was not covered by tests
} else if (values.length > 1) {
choices.push([name, values]);

Check warning on line 190 in src/Utils/makeExemplar.ts

View check run for this annotation

Codecov / codecov/patch

src/Utils/makeExemplar.ts#L190

Added line #L190 was not covered by tests
}
const value = values[0];
fullObject[name] = value;
if (prop.isRequired()) {
emptyObject[name] = value;
} else {
hasOptional = true;

Check warning on line 197 in src/Utils/makeExemplar.ts

View check run for this annotation

Codecov / codecov/patch

src/Utils/makeExemplar.ts#L197

Added line #L197 was not covered by tests
}
}
const additional = type.getAdditionalProperties();
if (additional === true) {
hasOptional = true;
fullObject["<UNLIKELY PROPERTY>"] = "UNLIKELY VALUE";

Check warning on line 203 in src/Utils/makeExemplar.ts

View check run for this annotation

Codecov / codecov/patch

src/Utils/makeExemplar.ts#L202-L203

Added lines #L202 - L203 were not covered by tests
} else if (additional) {
hasOptional = true;
choices.push(["<UNLIKELY PROPERTY>", makeExemplars(additional)]);

Check warning on line 206 in src/Utils/makeExemplar.ts

View check run for this annotation

Codecov / codecov/patch

src/Utils/makeExemplar.ts#L205-L206

Added lines #L205 - L206 were not covered by tests
}
const allChoices = resolveObjectChoices(fullObject, choices);
if (hasOptional) {
allChoices.push(emptyObject);

Check warning on line 210 in src/Utils/makeExemplar.ts

View check run for this annotation

Codecov / codecov/patch

src/Utils/makeExemplar.ts#L210

Added line #L210 was not covered by tests
}
return allChoices;
}

function makeTupleExemplars(type: TupleType): readonly unknown[] {
const exemplars = type.getTypes().map((t) => makeExemplars(t));
function makeTuples(prefix: readonly unknown[], items: (readonly unknown[])[]): (readonly unknown[])[] {

Check warning on line 217 in src/Utils/makeExemplar.ts

View check run for this annotation

Codecov / codecov/patch

src/Utils/makeExemplar.ts#L215-L217

Added lines #L215 - L217 were not covered by tests
if (items.length === 0) {
return [prefix];

Check warning on line 219 in src/Utils/makeExemplar.ts

View check run for this annotation

Codecov / codecov/patch

src/Utils/makeExemplar.ts#L219

Added line #L219 was not covered by tests
}
const [head, ...tail] = items;
const results: (readonly unknown[])[] = [];
for (const choice of head) {
results.push(...makeTuples(prefix.concat([choice]), tail));

Check warning on line 224 in src/Utils/makeExemplar.ts

View check run for this annotation

Codecov / codecov/patch

src/Utils/makeExemplar.ts#L221-L224

Added lines #L221 - L224 were not covered by tests
}
return results;

Check warning on line 226 in src/Utils/makeExemplar.ts

View check run for this annotation

Codecov / codecov/patch

src/Utils/makeExemplar.ts#L226

Added line #L226 was not covered by tests
}

return makeTuples([], exemplars);

Check warning on line 229 in src/Utils/makeExemplar.ts

View check run for this annotation

Codecov / codecov/patch

src/Utils/makeExemplar.ts#L229

Added line #L229 was not covered by tests
}
Loading