Skip to content

Commit

Permalink
custom object handling demo
Browse files Browse the repository at this point in the history
  • Loading branch information
justjake committed Jul 21, 2022
1 parent e17529c commit 5a84291
Show file tree
Hide file tree
Showing 3 changed files with 242 additions and 19 deletions.
4 changes: 2 additions & 2 deletions src/simple-type.ts
Original file line number Diff line number Diff line change
Expand Up @@ -273,7 +273,7 @@ export interface SimpleTypeMethod extends SimpleTypeBase {
export interface SimpleTypeGenericArguments extends SimpleTypeBase {
readonly kind: "GENERIC_ARGUMENTS"; // TODO: rename
/** The generic type being instantiated */
readonly target: Extract<SimpleType, { typeParameters?: unknown }>;
readonly target: Extract<SimpleType, { typeParameters?: unknown }> | SimpleTypeCustom;
/** The arguments passed to the generic */
readonly typeArguments: SimpleType[];
/** The concrete type resulting from applying the type parameters to the generic */
Expand Down Expand Up @@ -324,7 +324,7 @@ export interface SimpleTypePromise extends SimpleTypeBase {

export interface SimpleTypeCustom<T = unknown> extends SimpleTypeBase {
readonly kind: "CUSTOM";
readonly data: T;
readonly extra?: T;
}

export type SimpleType =
Expand Down
70 changes: 55 additions & 15 deletions src/transform/to-simple-type.ts
Original file line number Diff line number Diff line change
Expand Up @@ -66,13 +66,25 @@ interface ToSimpleTypePureOptions {
cache?: WeakMap<Type, SimpleType>;
}

interface ToCustomTypeArguments {
type: ts.Type;
checker: ts.TypeChecker;
ts: typeof tsModule;
/** True when `type` is the target of a GENERIC_ARGUMENTS instantiation */
generic: boolean;
}

type ToCustomType = (args: ToCustomTypeArguments) => SimpleType | ((concrete: SimpleType) => SimpleType) | undefined;

interface ToSimpleTypeConfigureTypeConstruction extends ToSimpleTypePureOptions {
/** With these options, the user must provide a cache because options modify how types are built, making repeat calls with the default cache non-deterministic */
cache: WeakMap<Type, SimpleType>;
/** Add methods like .getType(), .getTypeChecker() to each simple type */
addMethods?: boolean;
/** Add { kind: "ALIAS" } wrapper types around simple aliases. Otherwise, remove these wrappers. */
preserveSimpleAliases?: boolean;
/** If defined, called with each type, should return a CUSTOM type or undefined */
toCustomType?: ToCustomType;
}

export type ToSimpleTypeOptions = ToSimpleTypePureOptions | ToSimpleTypeConfigureTypeConstruction;
Expand Down Expand Up @@ -114,6 +126,7 @@ export function toSimpleType(type: Type | Node | SimpleType, checker?: TypeCheck
cache: options.cache || DEFAULT_TYPE_CACHE,
addMethods: "addMethods" in options ? options.addMethods : undefined,
preserveSimpleAliases: "preserveSimpleAliases" in options ? options.preserveSimpleAliases : undefined,
toCustomType: "toCustomType" in options ? options.toCustomType : undefined,
ts: getTypescriptModule()
});
}
Expand Down Expand Up @@ -214,8 +227,8 @@ function toSimpleTypeCached(type: Type, options: ToSimpleTypeInternalOptions): S
* @param type
* @param options
*/
function liftGenericType(type: Type, options: ToSimpleTypeInternalOptions): { generic: (instantiated: SimpleType) => SimpleType; instantiated: Type } | undefined {
const enhance = (instantiated: SimpleType) => withMethods(instantiated, type, options);
function liftGenericType(type: Type, options: ToSimpleTypeInternalOptions): { wrap: (instantiated: SimpleType) => SimpleType; instantiated: Type } | undefined {
const addMethods = (instantiated: SimpleType) => withMethods(instantiated, type, options);
const wrapIfAlias = (instantiated: SimpleType, ignoreTypeParams?: boolean): SimpleType => {
if (isAlias(type, options.ts)) {
const aliasName = type.aliasSymbol!.getName() || "";
Expand Down Expand Up @@ -264,19 +277,34 @@ function liftGenericType(type: Type, options: ToSimpleTypeInternalOptions): { ge

return {
instantiated: type,
generic: instantiated => {
wrap: instantiated => {
const typeArgumentsSimpleType = typeArguments.map(t => toSimpleTypeCached(t, options));

const generic: SimpleTypeGenericArguments = {
const customType = options.toCustomType?.({
...options,
type: type.target,
generic: true
});

const targetSimpleType =
typeof customType === "function"
? toSimpleTypeCached(type.target, options) /// XXX unlimited recursion?
: customType;

let generic: SimpleType = {
kind: "GENERIC_ARGUMENTS",
target: toSimpleTypeCached(type.target, options) as any,
target: targetSimpleType as any,
instantiated,
typeArguments: typeArgumentsSimpleType
};

if (typeof customType === "function") {
generic = customType(generic);
}

// This makes current tests work, but may be actually incorrect.
// vvvvvv
return enhance(wrapIfAlias(generic, true));
return addMethods(wrapIfAlias(generic, true));
}
};
}
Expand All @@ -286,8 +314,8 @@ function liftGenericType(type: Type, options: ToSimpleTypeInternalOptions): { ge
return {
// TODO: better type safety
instantiated: (type as any).target || type,
generic: instantiated => {
return enhance(wrapIfAlias(instantiated));
wrap: instantiated => {
return addMethods(wrapIfAlias(instantiated));
}
};
}
Expand Down Expand Up @@ -330,20 +358,37 @@ function memberWithMethods<T extends SimpleTypeMember>(obj: T, symbol: ts.Symbol
};
}

function toSimpleTypeInternal(type: Type, options: ToSimpleTypeInternalOptions): SimpleType {
function toSimpleTypeInternal(outerType: Type, options: ToSimpleTypeInternalOptions): SimpleType {
const { checker, ts } = options;

let type = outerType;
const symbol: ESSymbol | undefined = type.getSymbol();
const name = symbol != null ? getRealSymbolName(symbol, ts) : undefined;

let simpleType: SimpleType | undefined;
let enhance: (instantiated: SimpleType) => SimpleType = t => withMethods(t, type, options);

const generic = liftGenericType(type, options);
if (generic != null) {
type = generic.instantiated;
const originalEnhance = enhance;
enhance = t => generic.wrap(originalEnhance(t));
}

const enhance = (obj: SimpleType) => withMethods(obj, type, options);
// Custom types
const customType = options.toCustomType?.({
...options,
type,
generic: false
});
if (customType) {
if (typeof customType === "function") {
const originalEnhance = enhance;
enhance = t => withMethods(customType(originalEnhance(t)), outerType, options);
} else {
return enhance(customType);
}
}

// Literal types
if (isLiteral(type, ts)) {
Expand Down Expand Up @@ -678,11 +723,6 @@ function toSimpleTypeInternal(type: Type, options: ToSimpleTypeInternalOptions):
};
}

// Lift generic types and aliases if possible
if (generic != null) {
return generic.generic(enhance(simpleType));
}

return enhance(simpleType);
}

Expand Down
187 changes: 185 additions & 2 deletions test/to-simple-type.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,13 @@ import {
SimpleType,
SimpleTypeAlias,
SimpleTypeArray,
SimpleTypeString,
SimpleTypeCustom,
SimpleTypeGenericArguments,
SimpleTypeInterface,
SimpleTypeMember,
SimpleTypeMemberNamed,
SimpleTypeObject,
SimpleTypeString,
SimpleTypeUnion,
toSimpleType
} from "../src";
Expand Down Expand Up @@ -48,7 +51,8 @@ export type ContentPointer = RecordPointer<'block' | 'collection'>
test("it adds methods when addMethods is set", ctx => {
const { types, typeChecker } = getTestTypes(["SimpleAlias", "SimpleAliasExample", "GenericInterface", "GenericInterfaceExample"], TEST_TYPES);
const simpleType = toSimpleType(types.SimpleAliasExample, typeChecker, {
addMethods: true
addMethods: true,
cache: new WeakMap()
});

const toTs = simpleType.getTypescript?.();
Expand Down Expand Up @@ -371,6 +375,185 @@ test("generic type alias handling", ctx => {
ctx.deepEqual(recordPointerExpected, toSimpleType(types.RecordPointer, typeChecker));
});

const CUSTOM_TYPE_EXAMPLE = `
const sym = Symbol('fool')
/// XXX: We still can't "see" GENERIC_ARGUMENTS of many aliases :(
export type GenericAlias<T, Q> = T & CustomType<Q>
export type RecordId<T> = GenericAlias<string, { table: T }>
export interface CustomType<T> {
[typeof sym]: T
}
export type ExampleType = { id: string, related_id: CustomType<string> }
export type ExampleType2 = { id: string, related_id: RecordId<'block'> }
`;

test("custom type handling, non-generic", ctx => {
const { types, typeChecker } = getTestTypes(["CustomType", "ExampleType"], CUSTOM_TYPE_EXAMPLE);
const expected: SimpleTypeObject = {
kind: "OBJECT",
name: "ExampleType",
members: [
{ name: "id", type: { kind: "STRING" } },
{
name: "related_id",
type: {
kind: "GENERIC_ARGUMENTS",
target: { kind: "CUSTOM", name: "Very cool custom target" },
instantiated: { kind: "OBJECT", name: "CustomType", members: [] },
typeArguments: [{ kind: "STRING" }]
}
}
]
};

const actual = toSimpleType(types.ExampleType, typeChecker, {
cache: new WeakMap(),
toCustomType({ type }) {
if (type === types.CustomType) {
return {
kind: "CUSTOM",
name: "Very cool custom target"
};
}
}
});

ctx.deepEqual(actual, expected);
});

test("custom type handling, generic", ctx => {
const { types, typeChecker } = getTestTypes(["CustomType", "ExampleType"], CUSTOM_TYPE_EXAMPLE);
const expected: SimpleTypeObject = {
kind: "OBJECT",
name: "ExampleType",
members: [
{ name: "id", type: { kind: "STRING" } },
{
name: "related_id",
type: {
kind: "CUSTOM",
name: "Generic custom type",
extra: {
extractedParameter: { kind: "STRING" },
instantiated: { kind: "OBJECT", name: "CustomType", members: [] }
}
}
}
]
};

const actual = toSimpleType(types.ExampleType, typeChecker, {
cache: new WeakMap(),
toCustomType({ type, generic }) {
if (generic && type === types.CustomType) {
return function wrap(simpleType) {
if (simpleType.kind !== "GENERIC_ARGUMENTS") {
ctx.is(simpleType.kind, "GENERIC_ARGUMENTS", "should be a GENERIC_ARGUMENTS");
throw "no";
}
return {
kind: "CUSTOM",
name: "Generic custom type",
extra: {
extractedParameter: simpleType.typeArguments[0],
instantiated: simpleType.instantiated
}
};
};
}
}
});

ctx.deepEqual(actual, expected);
});

test("custom type handling, wrapper with generic anchor", ctx => {
const { types, typeChecker } = getTestTypes(["CustomType", "ExampleType2"], CUSTOM_TYPE_EXAMPLE);
const expected: SimpleTypeObject = {
kind: "OBJECT",
name: "ExampleType2",
members: [
{ name: "id", type: { kind: "STRING" } },
{
name: "related_id",
type: { kind: "CUSTOM", name: "RecordId", extra: { table: "block" } }
}
]
};

const actual = toSimpleType(types.ExampleType2, typeChecker, {
cache: new WeakMap(),
toCustomType({ type, generic }) {
// Anchor - it's easy to find a generic application of an interface,
// so we search for that happening and turn it into a custom MetaData type
// by extracting the generic argument.
if (generic && type === types.CustomType) {
return function wrapGeneric(simpleType) {
if (simpleType.kind !== "GENERIC_ARGUMENTS") {
ctx.is(simpleType.kind, "GENERIC_ARGUMENTS", "should be a GENERIC_ARGUMENTS");
throw "no";
}

return {
kind: "CUSTOM",
name: "MetaData",
extra: simpleTypeToLiteral(simpleType.typeArguments[0])
};
};
} else {
return simpleType => {
// Abstraction - check every type converted to SimpleType for special patterns.
// In this case, we look for RecordId alias, which contains a MetaData anchor.
// Then, we replace the whole abstraction type with just its metadata.
if (simpleType.kind === "ALIAS" && simpleType.name === "RecordId" && simpleType.target.kind === "INTERSECTION") {
const metaDataType = simpleType.target.types.find(t => t.kind === "CUSTOM" && t.name === "MetaData") as SimpleTypeCustom;
if (metaDataType) {
return {
kind: "CUSTOM",
name: "RecordId",
extra: metaDataType.extra
};
}
}
return simpleType;
};
}
}
});

ctx.deepEqual(actual, expected);
});

function simpleTypeToLiteral(simpleType: SimpleType): unknown {
if ("value" in simpleType) {
return simpleType.value;
}

if ("members" in simpleType && simpleType.members) {
const result: any = simpleType.kind === "TUPLE" ? [] : {};
simpleType.members.forEach((member: SimpleTypeMember | SimpleTypeMemberNamed, i) => {
const name = "name" in member ? member.name : i;
result[name] = simpleTypeToLiteral(member.type);
});
return result;
}

if (simpleType.kind === "UNION") {
return simpleType.types.map(simpleTypeToLiteral);
}

if (simpleType.kind === "NULL") {
return null;
}

if (simpleType.kind === "UNDEFINED") {
return undefined;
}

throw new Error(`Cannot convert SimpleType to literal: ${JSON.stringify(simpleType, null, 2)}`);
}

const stringSimpleType: SimpleTypeString = {
kind: "STRING"
};
Expand Down

0 comments on commit 5a84291

Please sign in to comment.