Skip to content

Commit

Permalink
fix: Merge compatible definitions in union types
Browse files Browse the repository at this point in the history
Most validation keywords apply to only one of the basic types, so a
"string" type and an "array" type can share a definition without
colliding as a ["string", "array"] type, as long as they don't have any
incompatibilities with each other. Modify UnionTypeFormatter to collapse
these disjoint types into a single definition, without using anyOf.
  • Loading branch information
dmchurch committed Mar 25, 2021
1 parent 541871b commit 246b65c
Show file tree
Hide file tree
Showing 2 changed files with 38 additions and 0 deletions.
13 changes: 13 additions & 0 deletions src/TypeFormatter/UnionTypeFormatter.ts
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 Down Expand Up @@ -45,6 +46,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
25 changes: 25 additions & 0 deletions src/Utils/mergeDefinitions.ts
@@ -0,0 +1,25 @@
import { Definition } from "../Schema/Definition";
import { uniqueArray } from "./uniqueArray";

/**
* Attempt to merge two disjoint definitions into one. Definitions are disjoint
* (and therefore mergeable) if all of the following are true:
* 1) Each has a 'type' property, and they share no types in common,
* 2) The cross-type validation properties 'enum' and 'const' are not on either definition, and
* 3) The two definitions have no properties besides 'type' in common.
*
* Returns the merged definition, or null if the two defs were not disjoint.
*/
export function mergeDefinitions(def1: Definition, def2: Definition): Definition | null {
const { type: type1, ...props1 } = def1;
const { type: type2, ...props2 } = def2;
const types = [type1!, type2!].flat();
if (!type1 || !type2 || uniqueArray(types).length !== types.length) {
return null;
}
const keys = [Object.keys(props1), Object.keys(props2)].flat();
if (keys.includes("enum") || keys.includes("const") || uniqueArray(keys).length !== keys.length) {
return null;
}
return { type: types, ...props1, ...props2 };
}

0 comments on commit 246b65c

Please sign in to comment.