Skip to content

Commit

Permalink
make it work
Browse files Browse the repository at this point in the history
  • Loading branch information
domoritz committed Apr 19, 2024
1 parent d6be863 commit f6fbef1
Show file tree
Hide file tree
Showing 9 changed files with 179 additions and 39 deletions.
4 changes: 4 additions & 0 deletions src/Type/LiteralType.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,4 +14,8 @@ export class LiteralType extends BaseType {
public getValue(): LiteralValue {
return this.value;
}

public isString(): boolean {
return typeof this.value === "string";
}
}
4 changes: 1 addition & 3 deletions src/TypeFormatter/AnnotatedTypeFormatter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,9 +61,7 @@ export class AnnotatedTypeFormatter implements SubTypeFormatter {
delete annotations.discriminator;
} else {
throw new Error(
`Cannot assign discriminator tag to type: ${JSON.stringify(
derefed,
)}. This tag can only be assigned to union types.`,
`Cannot assign discriminator tag to type: ${derefed.getName()}. This tag can only be assigned to union types.`,
);
}
}
Expand Down
63 changes: 42 additions & 21 deletions src/TypeFormatter/LiteralUnionTypeFormatter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,41 +2,48 @@ import { Definition } from "../Schema/Definition.js";
import { RawTypeName } from "../Schema/RawType.js";
import { SubTypeFormatter } from "../SubTypeFormatter.js";
import { BaseType } from "../Type/BaseType.js";
import { LiteralType } from "../Type/LiteralType.js";
import { LiteralType, LiteralValue } from "../Type/LiteralType.js";
import { NullType } from "../Type/NullType.js";
import { StringType } from "../Type/StringType.js";
import { UnionType } from "../Type/UnionType.js";
import { derefAliasedType } from "../Utils/derefType.js";
import { typeName } from "../Utils/typeName.js";
import { uniqueArray } from "../Utils/uniqueArray.js";

export class LiteralUnionTypeFormatter implements SubTypeFormatter {
public supportsType(type: BaseType): boolean {
return type instanceof UnionType && type.getTypes().length > 0 && this.isLiteralUnion(type);
return type instanceof UnionType && type.getTypes().length > 0 && isLiteralUnion(type);
}
public getDefinition(type: UnionType): Definition {
let hasString = false;
let preserveLiterals = false;
const types = type.getTypes().filter((t) => {
let allStrings = true;

const flattenedTypes = flattenTypes(type);

// filter out String types since we need to be more careful about them
const types = flattenedTypes.filter((t) => {
if (t instanceof StringType) {
hasString = true;
preserveLiterals = preserveLiterals || t.getPreserveLiterals();
return false;
}

if (t instanceof LiteralType && !t.isString()) {
allStrings = false;
}

return true;
});

if (hasString && !preserveLiterals) {
if (allStrings && hasString && !preserveLiterals) {
return {
type: "string",
};
}

const values = uniqueArray(
types.map((item: LiteralType | NullType | StringType) => this.getLiteralValue(item)),
);
const typeNames = uniqueArray(
types.map((item: LiteralType | NullType | StringType) => this.getLiteralType(item)),
);
const values = uniqueArray(types.map(getLiteralValue));
const typeNames = uniqueArray(types.map(getLiteralType));

const ret = {
type: typeNames.length === 1 ? typeNames[0] : typeNames,
Expand All @@ -59,16 +66,30 @@ export class LiteralUnionTypeFormatter implements SubTypeFormatter {
public getChildren(type: UnionType): BaseType[] {
return [];
}
}

protected isLiteralUnion(type: UnionType): boolean {
return type
.getTypes()
.every((item) => item instanceof LiteralType || item instanceof NullType || item instanceof StringType);
}
protected getLiteralValue(value: LiteralType | NullType): string | number | boolean | null {
return value instanceof LiteralType ? value.getValue() : null;
}
protected getLiteralType(value: LiteralType | NullType): RawTypeName {
return value instanceof LiteralType ? typeName(value.getValue()) : "null";
}
function flattenTypes(type: UnionType): (StringType | LiteralType | NullType)[] {
return type
.getTypes()
.map(derefAliasedType)
.flatMap((t) => {
if (t instanceof UnionType) {
return flattenTypes(t);
}
return t as StringType | LiteralType | NullType;
});
}

function isLiteralUnion(type: UnionType): boolean {
return flattenTypes(type).every(
(item) => item instanceof LiteralType || item instanceof NullType || item instanceof StringType,
);
}

function getLiteralValue(value: LiteralType | NullType): LiteralValue | null {
return value instanceof LiteralType ? value.getValue() : null;
}

function getLiteralType(value: LiteralType | NullType): RawTypeName {
return value instanceof LiteralType ? typeName(value.getValue()) : "null";
}
4 changes: 1 addition & 3 deletions src/TypeFormatter/UnionTypeFormatter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,9 +40,7 @@ export class UnionTypeFormatter implements SubTypeFormatter {

if (undefinedIndex != -1) {
throw new Error(
`Cannot find discriminator keyword "${discriminator}" in type ${JSON.stringify(
type.getTypes()[undefinedIndex],
)}.`,
`Cannot find discriminator keyword "${discriminator}" in type ${type.getTypes()[undefinedIndex].getName()}.`,
);
}

Expand Down
8 changes: 8 additions & 0 deletions src/Utils/derefType.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,3 +25,11 @@ export function derefAnnotatedType(type: BaseType): BaseType {

return type;
}

export function derefAliasedType(type: BaseType): BaseType {
if (type instanceof AliasType) {
return derefAliasedType(type.getType());
}

return type;
}
14 changes: 2 additions & 12 deletions test/invalid-data.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,24 +36,14 @@ describe("invalid-data", () => {
it("duplicates", assertSchema("duplicates", "MyType", `Type "A" has multiple definitions.`));
it(
"missing-discriminator",
assertSchema(
"missing-discriminator",
"MyType",
'Cannot find discriminator keyword "type" in type ' +
'{"name":"B","type":{"id":"interface-1119825560-40-63-1119825560-0-124",' +
'"baseTypes":[],"properties":[],"additionalProperties":false,"nonPrimitive":false}}.',
),
assertSchema("missing-discriminator", "MyType", 'Cannot find discriminator keyword "type" in type B.'),
);
it(
"non-union-discriminator",
assertSchema(
"non-union-discriminator",
"MyType",
"Cannot assign discriminator tag to type: " +
'{"id":"interface-2103469249-0-76-2103469249-0-77","baseTypes":[],' +
'"properties":[{"name":"name","type":{},"required":true}],' +
'"additionalProperties":false,"nonPrimitive":false}. ' +
"This tag can only be assigned to union types.",
"Cannot assign discriminator tag to type: interface-2103469249-0-76-2103469249-0-77. This tag can only be assigned to union types.",
),
);
it(
Expand Down
1 change: 1 addition & 0 deletions test/valid-data-other.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ describe("valid-data-other", () => {
it("string-literals-inline", assertValidSchema("string-literals-inline", "MyObject"));
it("string-literals-intrinsic", assertValidSchema("string-literals-intrinsic", "MyObject"));
it("string-literals-null", assertValidSchema("string-literals-null", "MyObject"));
it("string-literals-hack", assertValidSchema("string-literals-hack", "MyObject"));
it("string-template-literals", assertValidSchema("string-template-literals", "MyObject"));
it("string-template-expression-literals", assertValidSchema("string-template-expression-literals", "MyObject"));
it(
Expand Down
13 changes: 13 additions & 0 deletions test/valid-data/string-literals-hack/main.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
type Union = "a" | "b";

export type MyObject = {
literals: "foo" | "bar";
stringWithNull: string | null;
literalWithNull: "foo" | "bar" | null;
literalWithString: "foo" | "bar" | string;
withRef: "foo" | Union;
withRefWithString: Union | string;
withHack: "foo" | "bar" | (string & {});
withHackRecord: "foo" | "bar" | (string & Record<never, never>);
withHackNull: "foo" | "bar" | null | (string & Record<never, never>);
};
107 changes: 107 additions & 0 deletions test/valid-data/string-literals-hack/schema.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
{
"$ref": "#/definitions/MyType",
"$schema": "http://json-schema.org/draft-07/schema#",
"definitions": {
"MyObject": {
"additionalProperties": false,
"properties": {
"literalWithNull": {
"enum": [
"foo",
"bar",
null
],
"type": [
"string",
"null"
]
},
"literalWithString": {
"type": "string"
},
"literals": {
"enum": [
"foo",
"bar"
],
"type": "string"
},
"stringWithNull": {
"type": [
"string",
"null"
]
},
"withHack": {
"anyOf": [
{
"type": "string"
},
{
"enum": [
"foo",
"bar"
],
"type": "string"
}
]
},
"withHackNull": {
"anyOf": [
{
"type": "string"
},
{
"enum": [
"foo",
"bar",
null
],
"type": [
"string",
"null"
]
}
]
},
"withHackRecord": {
"anyOf": [
{
"type": "string"
},
{
"enum": [
"foo",
"bar"
],
"type": "string"
}
]
},
"withRef": {
"enum": [
"foo",
"a",
"b"
],
"type": "string"
},
"withRefWithString": {
"type": "string"
}
},
"required": [
"literals",
"stringWithNull",
"literalWithNull",
"literalWithString",
"withRef",
"withRefWithString",
"withHack",
"withHackRecord",
"withHackNull"
],
"type": "object"
}
}
}

0 comments on commit f6fbef1

Please sign in to comment.