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

WIP config parser #1561

Closed
wants to merge 16 commits into from
5 changes: 2 additions & 3 deletions packages/schema-type/src/typescript/dynamicAbiTypes.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import { Hex } from "viem";
import { DynamicAbiType, SchemaAbiType, dynamicAbiTypes } from "./schemaAbiTypes";
import { DynamicAbiType, dynamicAbiTypes } from "./schemaAbiTypes";
import { LiteralToBroad } from "./utils";
import { isArrayAbiType } from "./arrayAbiTypes";

// Variable-length ABI types, where their lengths are encoded by a PackedCounter within the record

Expand Down Expand Up @@ -124,6 +123,6 @@ export type DynamicAbiTypeToPrimitiveType<TDynamicAbiType extends DynamicAbiType
(typeof dynamicAbiTypeToDefaultValue)[TDynamicAbiType]
>;

export function isDynamicAbiType(abiType: string): abiType is DynamicAbiType {
export function isDynamicAbiType(abiType: unknown): abiType is DynamicAbiType {
return dynamicAbiTypes.includes(abiType as DynamicAbiType);
}
4 changes: 4 additions & 0 deletions packages/schema-type/src/typescript/schemaAbiTypes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -211,3 +211,7 @@ export const dynamicAbiTypes = schemaAbiTypes.slice(98) as any as TupleSplit<typ

export type StaticAbiType = (typeof staticAbiTypes)[number];
export type DynamicAbiType = (typeof dynamicAbiTypes)[number];

export function isSchemaAbiType(abiType: unknown): abiType is SchemaAbiType {
return schemaAbiTypes.includes(abiType as SchemaAbiType);
}
2 changes: 1 addition & 1 deletion packages/schema-type/src/typescript/staticAbiTypes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -218,6 +218,6 @@ export const staticAbiTypeToByteLength = {
address: 20,
} as const satisfies Record<StaticAbiType, number>;

export function isStaticAbiType(abiType: string): abiType is StaticAbiType {
export function isStaticAbiType(abiType: unknown): abiType is StaticAbiType {
return staticAbiTypes.includes(abiType as StaticAbiType);
}
28 changes: 28 additions & 0 deletions packages/store/ts/config/experimental/common.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
export type EmptyObject = { readonly [k: string]: never };

export type Prettify<T> = {
[K in keyof T as T[K] extends never ? never : K]: T[K];
} & unknown;

/**
* Merges two object types into new type
*
* @param Object1 - Object to merge into
* @param Object2 - Object to merge and override keys from {@link Object1}
* @returns New object type with keys from {@link Object1} and {@link Object2}. If a key exists in both {@link Object1} and {@link Object2}, the key from {@link Object2} will be used.
*
* @example
* type Result = Merge<{ foo: string }, { foo: number; bar: string }>
* // ^? type Result = { foo: number; bar: string }
*/
export type Merge<Object1, Object2> = Omit<Object1, keyof Object2> & Object2;

/** @internal */
export function isPlainObject(value: unknown): value is Record<string, unknown> {
return (
typeof value === "object" &&
value !== null &&
value.constructor === Object &&
Object.prototype.toString.call(value) === "[object Object]"
);
}
144 changes: 144 additions & 0 deletions packages/store/ts/config/experimental/parseConfig.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
import { describe, expect, expectTypeOf, it } from "vitest";
import { parseConfig } from "./parseConfig";
import { resourceIdToHex } from "@latticexyz/common";

describe("parseConfig", () => {
it("outputs tables from config", () => {
const output = parseConfig({
tables: {
Exists: "bool",
Position: {
valueSchema: { x: "uint32", y: "uint32" },
},
Messages: {
offchainOnly: true,
valueSchema: {
sender: "address",
message: "string",
},
},
},
} as const);

const expectedOutput = {
tables: {
Exists: {
type: "table",
namespace: "",
name: "Exists",
tableId: resourceIdToHex({ type: "table", namespace: "", name: "Exists" }),
keySchema: {
key: "bytes32",
},
valueSchema: {
value: "bool",
},
},
Position: {
type: "table",
namespace: "",
name: "Position",
tableId: resourceIdToHex({ type: "table", namespace: "", name: "Position" }),
keySchema: {
key: "bytes32",
},
valueSchema: {
x: "uint32",
y: "uint32",
},
},
Messages: {
type: "offchainTable",
namespace: "",
name: "Messages",
tableId: resourceIdToHex({ type: "offchainTable", namespace: "", name: "Messages" }),
keySchema: {
key: "bytes32",
},
valueSchema: {
message: "string",
sender: "address",
},
},
},
} as const;

expect(output).toStrictEqual(expectedOutput);
expectTypeOf(output).toEqualTypeOf(expectedOutput);
expectTypeOf(output).toMatchTypeOf(expectedOutput);
});

it("handles namespaces", () => {
const output = parseConfig({
namespace: "DefaultNamespace",
tables: {
Exists: "bool",
Position: {
namespace: "TableNamespace",
valueSchema: { x: "uint32", y: "uint32" },
},
},
namespaces: {
MyNamespace: {
tables: {
PlayerNames: "string",
Exists: {
// TODO: disable overriding namespace here
namespace: "OverrideNamespace",
valueSchema: {
exists: "bool",
},
},
},
},
},
} as const);

const expectedOutput = {
tables: {
Exists: {
type: "table",
namespace: "OverrideNamespace",
name: "Exists",
tableId: resourceIdToHex({ type: "table", namespace: "OverrideNamespace", name: "Exists" }),
keySchema: {
key: "bytes32",
},
valueSchema: {
exists: "bool",
},
},
Position: {
type: "table",
namespace: "TableNamespace",
name: "Position",
tableId: resourceIdToHex({ type: "table", namespace: "TableNamespace", name: "Position" }),
keySchema: {
key: "bytes32",
},
valueSchema: {
x: "uint32",
y: "uint32",
},
},
PlayerNames: {
type: "table",
namespace: "MyNamespace",
name: "PlayerNames",
tableId: resourceIdToHex({ type: "table", namespace: "MyNamespace", name: "PlayerNames" }),
keySchema: {
key: "bytes32",
},
valueSchema: {
value: "string",
},
},
},
} as const;

// TODO: why is PlayerNames disappearing?
expect(output).toStrictEqual(expectedOutput);
expectTypeOf(output).toEqualTypeOf(expectedOutput);
expectTypeOf(output).toMatchTypeOf(expectedOutput);
});
});
33 changes: 33 additions & 0 deletions packages/store/ts/config/experimental/parseConfig.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import { EmptyObject, Merge, Prettify } from "./common";
import { ParseNamespacesInput, ParseNamespacesOutput, parseNamespaces } from "./parseNamespaces";
import { ParseTablesInput, ParseTablesOutput, parseTables } from "./parseTables";

export type ParseConfigInput = {
readonly namespace?: string;
readonly tables?: ParseTablesInput;
readonly namespaces?: ParseNamespacesInput;
};

export type ParseConfigOutput<
input extends ParseConfigInput,
namespaces extends ParseNamespacesInput = input["namespaces"] extends ParseNamespacesInput
? input["namespaces"]
: EmptyObject
> = {
// TODO: ensure that tables of the same name get replaced and are not a union
readonly tables: Prettify<
ParseTablesOutput<
input["namespace"] extends string ? input["namespace"] : "",
input["tables"] extends ParseTablesInput ? input["tables"] : EmptyObject
> &
ParseNamespacesOutput<namespaces>
>;
};

export function parseConfig<input extends ParseConfigInput>(input: input): Prettify<ParseConfigOutput<input>> {
const tables = Object.entries(parseTables(input.namespace ?? "", input.tables ?? {}));
const namespacedTables = Object.entries(parseNamespaces(input.namespaces ?? {}));
return {
tables: Object.fromEntries([...tables, ...namespacedTables]),
} as ParseConfigOutput<input>;
}
38 changes: 38 additions & 0 deletions packages/store/ts/config/experimental/parseKeySchema.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import { describe, expect, expectTypeOf, it } from "vitest";
import { parseKeySchema } from "./parseKeySchema";

// TODO: add tests for failing cases (dynamic ABI types)

describe("parseKeySchema", () => {
it("outputs a key schema for uint8", () => {
const output = parseKeySchema("uint8");
const expectedOutput = { key: "uint8" } as const;
expect(output).toStrictEqual(output);
expectTypeOf(output).toEqualTypeOf(expectedOutput);
expectTypeOf(output).toMatchTypeOf(expectedOutput);
});

it("outputs a key schema for bool", () => {
const output = parseKeySchema("bool");
const expectedOutput = { key: "bool" } as const;
expect(output).toStrictEqual(output);
expectTypeOf(output).toEqualTypeOf(expectedOutput);
expectTypeOf(output).toMatchTypeOf(expectedOutput);
Comment on lines +19 to +20
Copy link
Member

Choose a reason for hiding this comment

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

what's the difference between toEqualTypeOf and toMatchTypeOf?

});

it("returns a full key schema", () => {
const output = parseKeySchema({ x: "uint32", y: "uint32" } as const);
const expectedOutput = { x: "uint32", y: "uint32" } as const;
expect(output).toStrictEqual(output);
expectTypeOf(output).toEqualTypeOf(expectedOutput);
expectTypeOf(output).toMatchTypeOf(expectedOutput);
});

it("defaults key schema when undefined", () => {
const output = parseKeySchema(undefined);
const expectedOutput = { key: "bytes32" } as const;
expect(output).toStrictEqual(output);
expectTypeOf(output).toEqualTypeOf(expectedOutput);
expectTypeOf(output).toMatchTypeOf(expectedOutput);
});
});
21 changes: 21 additions & 0 deletions packages/store/ts/config/experimental/parseKeySchema.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { StaticAbiType, isStaticAbiType } from "@latticexyz/schema-type";

export type KeySchema = { readonly [k: string]: StaticAbiType };

export const defaultKeySchema = { key: "bytes32" } as const satisfies KeySchema;

export type ParseKeySchemaInput = StaticAbiType | KeySchema | undefined;

export type ParseKeySchemaOutput<input extends ParseKeySchemaInput> = input extends undefined
? typeof defaultKeySchema
: input extends StaticAbiType
? { readonly key: input }
: input extends KeySchema
? input
: never;

export function parseKeySchema<input extends ParseKeySchemaInput>(input: input): ParseKeySchemaOutput<input> {
return (
input === undefined ? defaultKeySchema : isStaticAbiType(input) ? { key: input } : input
) as ParseKeySchemaOutput<input>;
}
28 changes: 28 additions & 0 deletions packages/store/ts/config/experimental/parseNamespace.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { EmptyObject } from "./common";
import { ParseTableInput, ParseTableOutput, parseTable } from "./parseTable";

export type ParseNamespaceInput = {
// TODO: omit namespace from table input
readonly tables?: { readonly [k: string]: ParseTableInput };
};

export type ParseNamespaceOutput<namespace extends string, input extends ParseNamespaceInput> = {
readonly tables: input["tables"] extends ParseTableInput
? {
readonly [name in keyof input["tables"]]: ParseTableOutput<namespace, name & string, input["tables"][name]>;
}
: EmptyObject;
};

export function parseNamespace<namespace extends string, input extends ParseNamespaceInput>(
namespace: namespace,
input: input
): ParseNamespaceOutput<namespace, input> {
return {
tables: Object.fromEntries(
// TODO: remove namespace from tableInput or override with the namespace we have
// though this may not be needed if our types omit it
Object.entries(input.tables ?? {}).map(([name, tableInput]) => [name, parseTable(namespace, name, tableInput)])
),
} as ParseNamespaceOutput<namespace, input>;
}
17 changes: 17 additions & 0 deletions packages/store/ts/config/experimental/parseNamespaces.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { ParseNamespaceInput, ParseNamespaceOutput, parseNamespace } from "./parseNamespace";

export type ParseNamespacesInput = { readonly [k: string]: ParseNamespaceInput };

export type ParseNamespacesOutput<input extends ParseNamespacesInput> = {
readonly [namespace in keyof input]: ParseNamespaceOutput<namespace & string, input[namespace]>;
}[keyof input]["tables"];

// TODO: rename to parseNamespacedTables
export function parseNamespaces<input extends ParseNamespacesInput>(input: input): ParseNamespacesOutput<input> {
return Object.fromEntries(
Object.entries(input).flatMap(([namespace, namespaceInput]) => {
const { tables } = parseNamespace(namespace, namespaceInput);
return Object.entries(tables);
})
) as ParseNamespacesOutput<input>;
}
Loading