Put string enum cases in a type attribute

This commit is contained in:
Mark Probst 2018-04-26 08:12:50 -07:00
Родитель 9387370253
Коммит 94887379cf
15 изменённых файлов: 119 добавлений и 140 удалений

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

@ -157,7 +157,7 @@ export function combineClasses(
clique,
attributes,
builder,
unionBuilderForUnification(builder, false, false, false, conflateNumbers),
unionBuilderForUnification(builder, false, false, conflateNumbers),
conflateNumbers,
forwardingRef
);

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

@ -22,7 +22,7 @@ export function flattenUnions(
let needsRepeat = false;
function replace(types: Set<Type>, builder: GraphRewriteBuilder<Type>, forwardingRef: TypeRef): TypeRef {
const unionBuilder = new UnifyUnionBuilder(builder, true, makeObjectTypes, true, trefs => {
const unionBuilder = new UnifyUnionBuilder(builder, makeObjectTypes, true, trefs => {
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) {

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

@ -113,7 +113,7 @@ function makeScalar(builder: TypeBuilder, ft: GQLType): TypeRef {
return builder.getPrimitiveType("double");
default:
// FIXME: support ID specifically?
return builder.getStringType(emptyTypeAttributes, undefined);
return builder.getStringType(emptyTypeAttributes, null);
}
}
@ -427,7 +427,7 @@ export function makeGraphQLQueryTypes(
namesTypeAttributeKind.makeAttributes(
TypeNames.make(OrderedSet(["error"]), OrderedSet(["graphQLError"]), false)
),
OrderedMap({ message: new ClassProperty(builder.getStringType(emptyTypeAttributes, undefined), false) })
OrderedMap({ message: new ClassProperty(builder.getStringType(emptyTypeAttributes, null), false) })
);
const errorArray = builder.getArrayType(errorType);
builder.addAttributes(

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

@ -79,11 +79,7 @@ export class TypeReconstituter<TBuilder extends BaseGraphRewriteBuilder> {
}
getPrimitiveType(kind: PrimitiveTypeKind): void {
this.registerAndAddAttributes(this.builderForNewType().getPrimitiveType(kind, this._forwardingRef));
}
getStringType(enumCases: OrderedMap<string, number> | undefined): void {
this.register(this.builderForNewType().getStringType(this._typeAttributes, enumCases, this._forwardingRef));
this.register(this.builderForNewType().getPrimitiveType(kind, this._typeAttributes, this._forwardingRef));
}
getEnumType(cases: OrderedSet<string>): void {

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

@ -2,8 +2,8 @@
import { Set, OrderedMap, OrderedSet } from "immutable";
import { Type, StringType, UnionType } from "./Type";
import { combineTypeAttributesOfTypes } from "./TypeUtils";
import { Type, PrimitiveType, UnionType, stringEnumCasesTypeAttributeKind } from "./Type";
import { combineTypeAttributesOfTypes, stringEnumCases } from "./TypeUtils";
import { TypeGraph } from "./TypeGraph";
import { TypeRef, StringTypeMapping } from "./TypeBuilder";
import { GraphRewriteBuilder } from "./GraphRewriting";
@ -12,32 +12,32 @@ import { combineTypeAttributes } from "./TypeAttributes";
const MIN_LENGTH_FOR_ENUM = 10;
function shouldBeEnum(t: StringType): OrderedMap<string, number> | undefined {
const enumCases = t.enumCases;
function shouldBeEnum(t: PrimitiveType): OrderedMap<string, number> | undefined {
const enumCases = stringEnumCases(t);
if (enumCases !== undefined) {
assert(enumCases.size > 0, "How did we end up with zero enum cases?");
const someCaseIsNotNumber = enumCases.keySeq().some(key => /^(\-|\+)?[0-9]+(\.[0-9]+)?$/.test(key) === false);
const numValues = enumCases.map(n => n).reduce<number>((a, b) => a + b);
if (numValues >= MIN_LENGTH_FOR_ENUM && enumCases.size < Math.sqrt(numValues) && someCaseIsNotNumber) {
return t.enumCases;
return enumCases;
}
}
return undefined;
}
function replaceString(
group: Set<StringType>,
builder: GraphRewriteBuilder<StringType>,
group: Set<PrimitiveType>,
builder: GraphRewriteBuilder<PrimitiveType>,
forwardingRef: TypeRef
): TypeRef {
assert(group.size === 1);
const t = defined(group.first());
const attributes = t.getAttributes();
const attributes = t.getAttributes().filterNot((_, k) => k === stringEnumCasesTypeAttributeKind);
const maybeEnumCases = shouldBeEnum(t);
if (maybeEnumCases !== undefined) {
return builder.getEnumType(attributes, maybeEnumCases.keySeq().toOrderedSet(), forwardingRef);
}
return builder.getStringType(attributes, undefined, forwardingRef);
return builder.getStringType(attributes, null, forwardingRef);
}
// A union needs replacing if it contains more than one string type, one of them being
@ -78,9 +78,9 @@ export function inferEnums(
): TypeGraph {
const allStrings = graph
.allTypesUnordered()
.filter(t => t instanceof StringType)
.filter(t => t.kind === "string")
.map(t => [t])
.toArray() as StringType[][];
.toArray() as PrimitiveType[][];
return graph.rewrite("infer enums", stringTypeMapping, false, allStrings, debugPrintReconstitution, replaceString);
}

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

@ -127,7 +127,7 @@ export function inferMaps(
shouldBe,
c.getAttributes(),
builder,
unionBuilderForUnification(builder, false, false, false, conflateNumbers),
unionBuilderForUnification(builder, false, false, conflateNumbers),
conflateNumbers
),
forwardingRef

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

@ -616,10 +616,10 @@ export async function addTypesInSchema(
default:
// FIXME: Output a warning here instead to indicate that
// the format is uninterpreted.
return typeBuilder.getStringType(inferredAttributes, undefined);
return typeBuilder.getStringType(inferredAttributes, null);
}
}
return typeBuilder.getStringType(inferredAttributes, undefined);
return typeBuilder.getStringType(inferredAttributes, null);
}
async function makeArrayType(): Promise<TypeRef> {

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

@ -14,7 +14,6 @@ import {
UnionType,
PrimitiveStringTypeKind,
PrimitiveTypeKind,
StringType,
ArrayType,
isPrimitiveStringTypeKind,
isPrimitiveTypeKind,
@ -96,7 +95,7 @@ class IntersectionAccumulator
// If the unrestricted string type is part of the union, this doesn't add
// any more restrictions.
if (members.find(t => t instanceof StringType) === undefined) {
if (members.find(t => t.kind === "string") === undefined) {
this._primitiveStringTypes = this._primitiveStringTypes.intersect(kinds);
}
}
@ -127,7 +126,7 @@ class IntersectionAccumulator
const enums = members.filter(t => t instanceof EnumType) as OrderedSet<EnumType>;
const attributes = combineTypeAttributesOfTypes(enums);
this._enumAttributes = combineTypeAttributes(this._enumAttributes, attributes);
if (members.find(t => t instanceof StringType) !== undefined) {
if (members.find(t => t.kind === "string") !== undefined) {
return;
}
const newCases = OrderedSet<string>().union(...enums.map(t => t.cases).toArray());
@ -358,15 +357,6 @@ class IntersectionUnionBuilder extends UnionBuilder<
return this._createdNewIntersections;
}
protected makeEnum(
cases: string[],
_counts: { [name: string]: number },
typeAttributes: TypeAttributes,
forwardingRef: TypeRef | undefined
): TypeRef {
return this.typeBuilder.getEnumType(typeAttributes, OrderedSet(cases), forwardingRef);
}
protected makeObject(
maybeData: [PropertyMap, OrderedSet<Type> | undefined] | undefined,
typeAttributes: TypeAttributes,
@ -411,8 +401,7 @@ export function resolveIntersections(
const intersections = types.filter(t => t instanceof IntersectionType) as Set<IntersectionType>;
const [members, intersectionAttributes] = setOperationMembersRecursively(intersections.toArray());
if (members.isEmpty()) {
const t = builder.getPrimitiveType("any", forwardingRef);
builder.addAttributes(t, intersectionAttributes);
const t = builder.getPrimitiveType("any", intersectionAttributes, forwardingRef);
return t;
}
if (members.size === 1) {

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

@ -6,7 +6,7 @@ import { defined, panic, assert, mapOptional } from "./Support";
import { TypeRef } from "./TypeBuilder";
import { TypeReconstituter, BaseGraphRewriteBuilder } from "./GraphRewriting";
import { TypeNames, namesTypeAttributeKind } from "./TypeNames";
import { TypeAttributes } from "./TypeAttributes";
import { TypeAttributes, TypeAttributeKind } from "./TypeAttributes";
import { ErrorMessage, messageAssert } from "./Messages";
export type PrimitiveStringTypeKind = "string" | "date" | "time" | "date-time";
@ -44,6 +44,19 @@ function orderedSetUnion<T>(sets: OrderedSet<OrderedSet<T>>): OrderedSet<T> {
return setArray[0].union(...setArray.slice(1));
}
export const stringEnumCasesTypeAttributeKind = new TypeAttributeKind<OrderedMap<string, number> | null>(
"stringEnumCases",
true,
(a, b) => {
if (a === null || b === null) {
return null;
}
return a.mergeWith((x, y) => x + y, b);
},
_ => undefined,
m => (m === null ? "no enum" : `${m.size.toString()} enums: ${m.keySeq().first()}, ...`)
);
export abstract class Type {
constructor(readonly typeRef: TypeRef, readonly kind: TypeKind) {}
@ -198,13 +211,6 @@ export class PrimitiveType extends Type {
// @ts-ignore: This is initialized in the Type constructor
readonly kind: PrimitiveTypeKind;
constructor(typeRef: TypeRef, kind: PrimitiveTypeKind, checkKind: boolean = true) {
if (checkKind) {
assert(kind !== "string", "Cannot instantiate a PrimitiveType as string");
}
super(typeRef, kind);
}
get children(): OrderedSet<Type> {
return OrderedSet();
}
@ -234,44 +240,6 @@ export class PrimitiveType extends Type {
}
}
export function stringTypeIdentity(
attributes: TypeAttributes,
enumCases: OrderedMap<string, number> | undefined
): List<any> | undefined {
if (enumCases !== undefined) return undefined;
// mapOptional(ec => ec.keySeq().toSet(), enumCases)
return List(["string", identityAttributes(attributes)]);
}
export class StringType extends PrimitiveType {
constructor(typeRef: TypeRef, readonly enumCases: OrderedMap<string, number> | undefined) {
super(typeRef, "string", false);
}
get identity(): List<any> | undefined {
return stringTypeIdentity(this.getAttributes(), this.enumCases);
}
reconstitute<T extends BaseGraphRewriteBuilder>(builder: TypeReconstituter<T>): void {
builder.getStringType(this.enumCases);
}
protected structuralEqualityStep(
_other: Type,
_conflateNumbers: boolean,
_queue: (a: Type, b: Type) => boolean
): boolean {
return true;
}
get debugPrintKind(): string {
if (this.enumCases === undefined) {
return "string";
}
return `string (${this.enumCases.size} enums)`;
}
}
export function arrayTypeIdentity(attributes: TypeAttributes, itemsRef: TypeRef): List<any> {
return List(["array", identityAttributes(attributes), itemsRef]);
}

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

@ -1,6 +1,6 @@
"use strict";
import { Map, OrderedMap, OrderedSet, Set, List } from "immutable";
import { Map, OrderedMap, OrderedSet, Set, List, is } from "immutable";
import {
PrimitiveTypeKind,
@ -12,18 +12,17 @@ import {
ClassType,
UnionType,
PrimitiveStringTypeKind,
StringType,
ClassProperty,
IntersectionType,
ObjectType,
stringTypeIdentity,
primitiveTypeIdentity,
enumTypeIdentity,
mapTypeIdentify,
arrayTypeIdentity,
classTypeIdentity,
unionTypeIdentity,
intersectionTypeIdentity
intersectionTypeIdentity,
stringEnumCasesTypeAttributeKind
} from "./Type";
import { removeNullFromUnion } from "./TypeUtils";
import { TypeGraph } from "./TypeGraph";
@ -141,7 +140,8 @@ export class TypeBuilder {
}
const tref = forwardingRef !== undefined ? forwardingRef : this.reserveTypeRef();
if (attributes !== undefined) {
this.addAttributes(tref, attributes);
const index = tref.index;
this.typeAttributes[index] = combineTypeAttributes(this.typeAttributes[index], attributes);
}
const t = creator(tref);
this.commitType(tref, t);
@ -158,9 +158,19 @@ export class TypeBuilder {
}
addAttributes(tref: TypeRef, attributes: TypeAttributes): void {
assert(attributes.every((_, k) => !k.inIdentity), "Can't add identity type attributes to an existing type");
const index = tref.index;
this.typeAttributes[index] = combineTypeAttributes(this.typeAttributes[index], attributes);
const existingAttributes = this.typeAttributes[index];
assert(
attributes.every((v, k) => {
if (!k.inIdentity) return true;
const existing = existingAttributes.get(k);
if (existing === undefined) return false;
return is(existing, v);
}),
"Can't add different identity type attributes to an existing type"
);
const nonIdentityAttributes = attributes.filterNot((_, k) => k.inIdentity);
this.typeAttributes[index] = combineTypeAttributes(existingAttributes, nonIdentityAttributes);
}
makeNullable(tref: TypeRef, attributes: TypeAttributes): TypeRef {
@ -223,7 +233,11 @@ export class TypeBuilder {
if (maybeTypeRef !== undefined) {
const result = this.forwardIfNecessary(forwardingRef, maybeTypeRef);
if (attributes !== undefined) {
this.addAttributes(result, attributes);
// We only add the attributes that are not in the identity, since
// we found the type based on its identity, i.e. all the identity
// attributes must be in there already, and we have a check that
// asserts that no identity attributes are added later.
this.addAttributes(result, attributes.filter((_, k) => !k.inIdentity));
}
return result;
}
@ -237,30 +251,44 @@ export class TypeBuilder {
this.registerTypeForIdentity(t.identity, t.typeRef);
}
getPrimitiveType(kind: PrimitiveTypeKind, forwardingRef?: TypeRef): TypeRef {
assert(kind !== "string", "Use getStringType to create strings");
getPrimitiveType(kind: PrimitiveTypeKind, attributes?: TypeAttributes, forwardingRef?: TypeRef): TypeRef {
if (attributes === undefined) {
attributes = emptyTypeAttributes;
}
let enumCases = kind === "string" ? undefined : null;
if (kind === "date") kind = this._stringTypeMapping.date;
if (kind === "time") kind = this._stringTypeMapping.time;
if (kind === "date-time") kind = this._stringTypeMapping.dateTime;
if (kind === "string") {
return this.getStringType(emptyTypeAttributes, undefined, forwardingRef);
return this.getStringType(attributes, enumCases, forwardingRef);
}
return this.getOrAddType(
primitiveTypeIdentity(kind, emptyTypeAttributes),
tr => new PrimitiveType(tr, kind),
undefined,
attributes,
forwardingRef
);
}
getStringType(
attributes: TypeAttributes,
cases: OrderedMap<string, number> | undefined,
cases: OrderedMap<string, number> | null | undefined,
forwardingRef?: TypeRef
): TypeRef {
const existingEnumAttribute = attributes.find((_, k) => k === stringEnumCasesTypeAttributeKind);
assert(
(cases === undefined) !== (existingEnumAttribute === undefined),
"Must instantiate string type with one enum case attribute"
);
if (existingEnumAttribute === undefined) {
attributes = combineTypeAttributes(
attributes,
stringEnumCasesTypeAttributeKind.makeAttributes(defined(cases))
);
}
return this.getOrAddType(
stringTypeIdentity(attributes, cases),
tr => new StringType(tr, cases),
primitiveTypeIdentity("string", attributes),
tr => new PrimitiveType(tr, "string"),
attributes,
forwardingRef
);

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

@ -393,9 +393,8 @@ export function noneToAny(
[noneTypes.toArray()],
debugPrintReconstitution,
(types, builder, forwardingRef) => {
const tref = builder.getPrimitiveType("any", forwardingRef);
const attributes = combineTypeAttributesOfTypes(types);
builder.addAttributes(tref, attributes);
const tref = builder.getPrimitiveType("any", attributes, forwardingRef);
return tref;
}
);

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

@ -1,13 +1,12 @@
"use strict";
import { OrderedSet, Collection, Map, Set } from "immutable";
import { OrderedSet, Collection, Map, Set, OrderedMap } from "immutable";
import { defined, panic, assert, assertNever } from "./Support";
import { TypeAttributes, combineTypeAttributes, emptyTypeAttributes } from "./TypeAttributes";
import {
Type,
PrimitiveType,
StringType,
ArrayType,
EnumType,
ObjectType,
@ -15,7 +14,8 @@ import {
ClassType,
ClassProperty,
SetOperationType,
UnionType
UnionType,
stringEnumCasesTypeAttributeKind
} from "./Type";
export function assertIsObject(t: Type): ObjectType {
@ -191,6 +191,18 @@ export function directlyReachableSingleNamedType(type: Type): Type | undefined {
return definedTypes.first();
}
export function stringEnumCases(t: PrimitiveType): OrderedMap<string, number> | undefined {
assert(t.kind === "string", "Only strings can be considered enums");
const enumCases = stringEnumCasesTypeAttributeKind.tryGetInAttributes(t.getAttributes());
if (enumCases === undefined) {
return panic("All strings must have an enum case attribute");
}
if (enumCases === null) {
return undefined;
}
return enumCases;
}
export type StringTypeMatchers<U> = {
dateType?: (dateType: PrimitiveType) => U;
timeType?: (timeType: PrimitiveType) => U;
@ -205,7 +217,7 @@ export function matchTypeExhaustive<U>(
boolType: (boolType: PrimitiveType) => U,
integerType: (integerType: PrimitiveType) => U,
doubleType: (doubleType: PrimitiveType) => U,
stringType: (stringType: StringType) => U,
stringType: (stringType: PrimitiveType) => U,
arrayType: (arrayType: ArrayType) => U,
classType: (classType: ClassType) => U,
mapType: (mapType: MapType) => U,
@ -247,7 +259,7 @@ export function matchType<U>(
boolType: (boolType: PrimitiveType) => U,
integerType: (integerType: PrimitiveType) => U,
doubleType: (doubleType: PrimitiveType) => U,
stringType: (stringType: StringType) => U,
stringType: (stringType: PrimitiveType) => U,
arrayType: (arrayType: ArrayType) => U,
classType: (classType: ClassType) => U,
mapType: (mapType: MapType) => U,

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

@ -90,7 +90,6 @@ function countProperties(
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[]) => TypeRef
@ -98,19 +97,6 @@ export class UnifyUnionBuilder extends UnionBuilder<TypeBuilder & TypeLookerUp,
super(typeBuilder);
}
protected makeEnum(
enumCases: string[],
counts: { [name: string]: number },
typeAttributes: TypeAttributes,
forwardingRef: TypeRef | undefined
): TypeRef {
if (this._makeEnums) {
return this.typeBuilder.getEnumType(typeAttributes, OrderedSet(enumCases), forwardingRef);
} else {
return this.typeBuilder.getStringType(typeAttributes, OrderedMap(counts), forwardingRef);
}
}
protected makeObject(
objectRefs: TypeRef[],
typeAttributes: TypeAttributes,
@ -149,7 +135,7 @@ export class UnifyUnionBuilder extends UnionBuilder<TypeBuilder & TypeLookerUp,
return tref;
} else {
const [properties, additionalProperties, lostTypeAttributes] = getCliqueProperties(objectTypes, types => {
assert(types.size > 0, "Property has no type");
assert(types.size > 0, "Property has no type");
return this._unifyTypes(types.map(t => t.typeRef).toArray());
});
if (lostTypeAttributes) {
@ -187,17 +173,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[]> {
return new UnifyUnionBuilder(typeBuilder, makeEnums, makeObjectTypes, makeClassesFixed, trefs =>
return new UnifyUnionBuilder(typeBuilder, makeObjectTypes, makeClassesFixed, trefs =>
unifyTypes(
Set(trefs.map(tref => tref.deref()[0])),
emptyTypeAttributes,
typeBuilder,
unionBuilderForUnification(typeBuilder, makeEnums, makeObjectTypes, makeClassesFixed, conflateNumbers),
unionBuilderForUnification(typeBuilder, makeObjectTypes, makeClassesFixed, conflateNumbers),
conflateNumbers
)
);

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

@ -2,7 +2,7 @@
import { Map, Set, OrderedMap, OrderedSet } from "immutable";
import { TypeKind, PrimitiveStringTypeKind, Type, UnionType } from "./Type";
import { TypeKind, PrimitiveStringTypeKind, Type, UnionType, stringEnumCasesTypeAttributeKind } from "./Type";
import { matchTypeExhaustive } from "./TypeUtils";
import {
TypeAttributes,
@ -131,7 +131,8 @@ export class UnionAccumulator<TArray, TObject> implements UnionTypeProvider<TArr
addEnumCases(cases: OrderedMap<string, number>, attributes: TypeAttributes): void {
if (this.have("string")) {
this.addStringType("string", attributes);
const enumAttributes = stringEnumCasesTypeAttributeKind.makeAttributes(cases);
this.addStringType("string", combineTypeAttributes(attributes, enumAttributes));
return;
}
@ -241,14 +242,7 @@ export class TypeRefUnionAccumulator extends UnionAccumulator<TypeRef, TypeRef>
_boolType => this.addBool(attributes),
_integerType => this.addInteger(attributes),
_doubleType => this.addDouble(attributes),
stringType => {
const enumCases = stringType.enumCases;
if (enumCases === undefined) {
this.addStringType("string", attributes);
} else {
this.addEnumCases(enumCases, attributes);
}
},
_stringType => this.addStringType("string", attributes),
arrayType => this.addArray(arrayType.items.typeRef, attributes),
classType => this.addObject(classType.typeRef, attributes),
mapType => this.addObject(mapType.typeRef, attributes),
@ -276,12 +270,15 @@ export class TypeRefUnionAccumulator extends UnionAccumulator<TypeRef, TypeRef>
export abstract class UnionBuilder<TBuilder extends TypeBuilder, TArrayData, TObjectData> {
constructor(protected readonly typeBuilder: TBuilder) {}
protected abstract makeEnum(
protected makeEnum(
cases: string[],
counts: { [name: string]: number },
_counts: { [name: string]: number },
typeAttributes: TypeAttributes,
forwardingRef: TypeRef | undefined
): TypeRef;
): TypeRef {
return this.typeBuilder.getEnumType(typeAttributes, OrderedSet(cases), forwardingRef);
}
protected abstract makeObject(
objects: TObjectData,
typeAttributes: TypeAttributes,
@ -309,11 +306,15 @@ export abstract class UnionBuilder<TBuilder extends TypeBuilder, TArrayData, TOb
case "date":
case "time":
case "date-time":
const t = this.typeBuilder.getPrimitiveType(kind, forwardingRef);
this.typeBuilder.addAttributes(t, typeAttributes);
return t;
return this.typeBuilder.getPrimitiveType(kind, typeAttributes, forwardingRef);
case "string":
return this.typeBuilder.getStringType(typeAttributes, undefined, forwardingRef);
return this.typeBuilder.getStringType(
typeAttributes,
typeAttributes.findKey((_, k) => k === stringEnumCasesTypeAttributeKind) !== undefined
? undefined
: null,
forwardingRef
);
case "enum":
return this.makeEnum(typeProvider.enumCases, typeProvider.enumCaseMap, typeAttributes, forwardingRef);
case "object":

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

@ -211,6 +211,7 @@ export const GoLanguage: Language = {
"github-events.json",
"reddit.json",
"nbl-stats.json",
"recursive.json",
"0cffa.json",
"0e0c2.json",
"127a1.json",