From 86b07c8dee7656baabebb5a58dcc83c16085b0d4 Mon Sep 17 00:00:00 2001 From: Kevin Ingersoll Date: Tue, 24 Oct 2023 12:25:10 +0100 Subject: [PATCH] wip parsed config --- .../src/typescript/dynamicAbiTypes.ts | 2 +- .../src/typescript/schemaAbiTypes.ts | 4 + .../src/typescript/staticAbiTypes.ts | 2 +- packages/store/package.json | 1 + packages/store/ts/config/common.ts | 90 +++++++++++++++++++ .../store/ts/config/experimental/common.ts | 26 ++++++ .../ts/config/experimental/parseConfig.ts | 26 ++++++ .../ts/config/experimental/parseKeySchema.ts | 21 +++++ .../ts/config/experimental/parseTable.ts | 87 ++++++++++++++++++ .../ts/config/experimental/parseTables.ts | 31 +++++++ .../config/experimental/parseValueSchema.ts | 45 ++++++++++ packages/store/ts/config/storeConfig.ts | 1 + .../store/ts/register/mudConfig.test-d.ts | 33 ++++++- packages/store/ts/register/mudConfig.ts | 15 +++- pnpm-lock.yaml | 3 + 15 files changed, 378 insertions(+), 9 deletions(-) create mode 100644 packages/store/ts/config/common.ts create mode 100644 packages/store/ts/config/experimental/common.ts create mode 100644 packages/store/ts/config/experimental/parseConfig.ts create mode 100644 packages/store/ts/config/experimental/parseKeySchema.ts create mode 100644 packages/store/ts/config/experimental/parseTable.ts create mode 100644 packages/store/ts/config/experimental/parseTables.ts create mode 100644 packages/store/ts/config/experimental/parseValueSchema.ts diff --git a/packages/schema-type/src/typescript/dynamicAbiTypes.ts b/packages/schema-type/src/typescript/dynamicAbiTypes.ts index 8c4549c2f2..cfb322e87f 100644 --- a/packages/schema-type/src/typescript/dynamicAbiTypes.ts +++ b/packages/schema-type/src/typescript/dynamicAbiTypes.ts @@ -124,6 +124,6 @@ export type DynamicAbiTypeToPrimitiveType; -export function isDynamicAbiType(abiType: string): abiType is DynamicAbiType { +export function isDynamicAbiType(abiType: unknown): abiType is DynamicAbiType { return dynamicAbiTypes.includes(abiType as DynamicAbiType); } diff --git a/packages/schema-type/src/typescript/schemaAbiTypes.ts b/packages/schema-type/src/typescript/schemaAbiTypes.ts index c7f3886e15..58792943c5 100644 --- a/packages/schema-type/src/typescript/schemaAbiTypes.ts +++ b/packages/schema-type/src/typescript/schemaAbiTypes.ts @@ -211,3 +211,7 @@ export const dynamicAbiTypes = schemaAbiTypes.slice(98) as any as TupleSplit; -export function isStaticAbiType(abiType: string): abiType is StaticAbiType { +export function isStaticAbiType(abiType: unknown): abiType is StaticAbiType { return staticAbiTypes.includes(abiType as StaticAbiType); } diff --git a/packages/store/package.json b/packages/store/package.json index f231b25388..58ddb59f98 100644 --- a/packages/store/package.json +++ b/packages/store/package.json @@ -55,6 +55,7 @@ "@latticexyz/config": "workspace:*", "@latticexyz/schema-type": "workspace:*", "abitype": "0.9.8", + "viem": "1.14.0", "zod": "^3.21.4" }, "devDependencies": { diff --git a/packages/store/ts/config/common.ts b/packages/store/ts/config/common.ts new file mode 100644 index 0000000000..1e0fc58c71 --- /dev/null +++ b/packages/store/ts/config/common.ts @@ -0,0 +1,90 @@ +import { SchemaAbiType, StaticAbiType } from "@latticexyz/schema-type"; +import { ExpandTablesConfig, StoreConfig, StoreUserConfig, resolveUserTypes } from "./storeConfig"; +import { Hex } from "viem"; +import { resourceToHex } from "@latticexyz/common"; + +// export type ConfigToResolvedTables< +// config extends StoreUserConfig, +// tables extends ExpandTablesConfig +// > = { +// [tableName in keyof tables]: { +// tableId: Hex; +// namespace: config["namespace"]; +// name: tableName; +// keySchema: tables[tableName]["keySchema"]; +// valueSchema: tables[tableName]["valueSchema"]; +// }; +// }; + +// TODO: helper to filter user types to StaticAbiType +export type UserTypes = Record; + +export type KeySchema = Record< + string, + userTypes extends UserTypes ? StaticAbiType | keyof userTypes : StaticAbiType +>; +export type ValueSchema = Record< + string, + userTypes extends UserTypes ? SchemaAbiType | keyof userTypes : SchemaAbiType +>; + +type ConfigToUserTypes = config["userTypes"]; +// TODO: fix strong enum types and avoid every schema getting `{ [k: string]: "uint8" }` +// type ConfigToUserTypes = config["userTypes"] & { +// [k in keyof config["enums"]]: { internalType: "uint8" }; +// }; + +export type TableKey< + config extends StoreConfig = StoreConfig, + table extends config["tables"][keyof config["tables"]] = config["tables"][keyof config["tables"]] +> = `${config["namespace"]}_${table["name"]}`; + +export type Table< + config extends StoreConfig = StoreConfig, + table extends config["tables"][keyof config["tables"]] = config["tables"][keyof config["tables"]] +> = { + readonly namespace: config["namespace"]; + readonly name: table["name"]; + readonly tableId: Hex; + readonly keySchema: table["keySchema"] extends KeySchema> + ? KeySchema & { + readonly [k in keyof table["keySchema"]]: ConfigToUserTypes[table["keySchema"][k]]["internalType"] extends StaticAbiType + ? ConfigToUserTypes[table["keySchema"][k]]["internalType"] + : table["keySchema"][k]; + } + : KeySchema; + readonly valueSchema: table["valueSchema"] extends ValueSchema> + ? { + readonly [k in keyof table["valueSchema"]]: ConfigToUserTypes[table["valueSchema"][k]]["internalType"] extends SchemaAbiType + ? ConfigToUserTypes[table["valueSchema"][k]]["internalType"] + : table["valueSchema"][k]; + } + : ValueSchema; +}; + +export type Tables = { + readonly [k in keyof config["tables"] as TableKey]: Table; +}; + +export function configToTables(config: config): Tables { + const userTypes = { + ...config.userTypes, + ...Object.fromEntries(Object.entries(config.enums).map(([key]) => [key, { internalType: "uint8" }] as const)), + }; + return Object.fromEntries( + Object.entries(config.tables).map(([tableName, table]) => [ + `${config.namespace}_${tableName}` satisfies TableKey, + { + namespace: config.namespace, + name: table.name, + tableId: resourceToHex({ + type: table.offchainOnly ? "offchainTable" : "table", + namespace: config.namespace, + name: table.name, + }), + keySchema: resolveUserTypes(table.keySchema, userTypes) as any, + valueSchema: resolveUserTypes(table.valueSchema, userTypes) as any, + } satisfies Table, + ]) + ) as Tables; +} diff --git a/packages/store/ts/config/experimental/common.ts b/packages/store/ts/config/experimental/common.ts new file mode 100644 index 0000000000..42541f7cdd --- /dev/null +++ b/packages/store/ts/config/experimental/common.ts @@ -0,0 +1,26 @@ +import { SchemaAbiType } from "@latticexyz/schema-type"; + +export type EmptyObject = { readonly [k: string]: never }; + +export type Prettify = { + [K in keyof T as T[K] extends never ? never : K]: T[K]; +} & unknown; + +export type Merge = Omit & Object2; + +/** @internal */ +export function isPlainObject(value: unknown): value is Record { + return ( + typeof value === "object" && + value !== null && + value.constructor === Object && + Object.prototype.toString.call(value) === "[object Object]" + ); +} + +export type UserTypes = { readonly [k: string]: SchemaAbiType } | undefined; +export type KeyOf = keyof T & string; + +export function includes(values: readonly T[], value: unknown): value is T { + return values.includes(value as T); +} diff --git a/packages/store/ts/config/experimental/parseConfig.ts b/packages/store/ts/config/experimental/parseConfig.ts new file mode 100644 index 0000000000..27217517c5 --- /dev/null +++ b/packages/store/ts/config/experimental/parseConfig.ts @@ -0,0 +1,26 @@ +import { UserTypes } from "./common"; +import { ParseTablesInput, ParseTablesOutput, parseTables } from "./parseTables"; + +export type ParseConfigInput = { + readonly userTypes?: userTypes; + readonly namespace?: string; + readonly tables: ParseTablesInput; +}; + +export type ParseConfigOutput> = { + // TODO: ensure that tables of the same name get replaced and are not a union + readonly tables: ParseTablesOutput< + userTypes, + input["namespace"] extends string ? input["namespace"] : "", + input["tables"] + >; +}; + +export function parseConfig>( + input: input +): ParseConfigOutput { + const tables = Object.entries(parseTables(input.userTypes, input.namespace ?? "", input.tables)); + return { + tables: Object.fromEntries(tables), + } as ParseConfigOutput; +} diff --git a/packages/store/ts/config/experimental/parseKeySchema.ts b/packages/store/ts/config/experimental/parseKeySchema.ts new file mode 100644 index 0000000000..25c6581a6f --- /dev/null +++ b/packages/store/ts/config/experimental/parseKeySchema.ts @@ -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 undefined + ? typeof defaultKeySchema + : input extends StaticAbiType + ? { readonly key: input } + : input extends KeySchema + ? input + : never; + +export function parseKeySchema(input: input): ParseKeySchemaOutput { + return ( + input === undefined ? defaultKeySchema : isStaticAbiType(input) ? { key: input } : input + ) as ParseKeySchemaOutput; +} diff --git a/packages/store/ts/config/experimental/parseTable.ts b/packages/store/ts/config/experimental/parseTable.ts new file mode 100644 index 0000000000..cd3ee37c4f --- /dev/null +++ b/packages/store/ts/config/experimental/parseTable.ts @@ -0,0 +1,87 @@ +import { SchemaAbiType, isSchemaAbiType } from "@latticexyz/schema-type"; +import { KeyOf, UserTypes, includes, isPlainObject } from "./common"; +import { ParseKeySchemaInput, ParseKeySchemaOutput, parseKeySchema } from "./parseKeySchema"; +import { ParseValueSchemaInput, ParseValueSchemaOutput, parseValueSchema } from "./parseValueSchema"; +import { assertExhaustive } from "@latticexyz/common/utils"; +import { resourceToHex } from "@latticexyz/common"; + +/** @internal */ +export type TableShapeInput = { + readonly namespace?: string; + readonly keySchema?: ParseKeySchemaInput; + readonly valueSchema: ParseValueSchemaInput; + readonly offchainOnly?: boolean; +}; + +export type ParseTableInput = + | KeyOf + | SchemaAbiType + | TableShapeInput; + +export type ParseTableOutput< + userTypes extends UserTypes, + defaultNamespace extends string, + name extends string, + input extends ParseTableInput +> = input extends SchemaAbiType + ? ParseTableOutput + : input extends TableShapeInput + ? { + readonly type: input["offchainOnly"] extends true ? "offchainTable" : "table"; + readonly namespace: input["namespace"] extends string ? input["namespace"] : defaultNamespace; + readonly name: name; + readonly tableId: `0x${string}`; + readonly keySchema: ParseKeySchemaOutput< + input["keySchema"] extends ParseKeySchemaInput + ? input["keySchema"] + : never extends input["keySchema"] + ? undefined + : never + >; + readonly valueSchema: ParseValueSchemaOutput; + } + : never; + +// TODO: is there a better way to check this aside from just looking at the shape/keys of the object? + +/** @internal */ +export const tableInputShapeKeys = ["namespace", "keySchema", "valueSchema", "offchainOnly"] as const; + +/** @internal */ +export function isTableShapeInput(input: unknown): input is TableShapeInput { + if (!isPlainObject(input)) return false; + if (Object.keys(input).some((key) => !includes(tableInputShapeKeys, key))) return false; + return true; +} + +export function parseTable< + userTypes extends UserTypes, + defaultNamespace extends string, + name extends string, + input extends ParseTableInput +>( + userTypes: UserTypes, + defaultNamespace: defaultNamespace, + name: name, + input: input +): ParseTableOutput { + const userTypeNames = userTypes != null ? (Object.keys(userTypes) as unknown as readonly KeyOf[]) : null; + return ( + isSchemaAbiType(input) || (userTypeNames != null && includes(userTypeNames, input)) + ? parseTable(userTypes, defaultNamespace, name, { valueSchema: input } as const) + : isTableShapeInput(input) + ? { + type: input.offchainOnly === true ? "offchainTable" : "table", + namespace: input.namespace ?? defaultNamespace, + name, + tableId: resourceToHex({ + type: input.offchainOnly === true ? "offchainTable" : "table", + namespace: input.namespace ?? defaultNamespace, + name, + }), + keySchema: parseKeySchema(input.keySchema), + valueSchema: parseValueSchema(userTypes, input.valueSchema), + } + : assertExhaustive(input, "invalid table input") + ) as ParseTableOutput; +} diff --git a/packages/store/ts/config/experimental/parseTables.ts b/packages/store/ts/config/experimental/parseTables.ts new file mode 100644 index 0000000000..8021aa60ce --- /dev/null +++ b/packages/store/ts/config/experimental/parseTables.ts @@ -0,0 +1,31 @@ +import { KeyOf, UserTypes } from "./common"; +import { ParseTableInput, ParseTableOutput, parseTable } from "./parseTable"; + +export type ParseTablesInput = { readonly [k: string]: ParseTableInput }; + +export type ParseTablesOutput< + userTypes extends UserTypes, + defaultNamespace extends string, + input extends ParseTablesInput +> = { + readonly [name in KeyOf]: ParseTableOutput< + userTypes, + input["namespace"] extends string ? input["namespace"] : defaultNamespace, + name, + input[name] + >; +}; + +export function parseTables< + userTypes extends UserTypes, + defaultNamespace extends string, + input extends ParseTablesInput +>( + userTypes: UserTypes, + defaultNamespace: defaultNamespace, + input: input +): ParseTablesOutput { + return Object.fromEntries( + Object.entries(input).map(([name, tableInput]) => [name, parseTable(userTypes, defaultNamespace, name, tableInput)]) + ) as ParseTablesOutput; +} diff --git a/packages/store/ts/config/experimental/parseValueSchema.ts b/packages/store/ts/config/experimental/parseValueSchema.ts new file mode 100644 index 0000000000..7e235cecb1 --- /dev/null +++ b/packages/store/ts/config/experimental/parseValueSchema.ts @@ -0,0 +1,45 @@ +import { SchemaAbiType, isSchemaAbiType } from "@latticexyz/schema-type"; +import { KeyOf, UserTypes, includes } from "./common"; + +export type ValueSchemaInput = { readonly [k: string]: SchemaAbiType | KeyOf }; + +export type ParseValueSchemaInput = + | KeyOf + | SchemaAbiType + | ValueSchemaInput; + +export type ParseValueSchemaOutput< + userTypes extends UserTypes, + input extends ParseValueSchemaInput +> = input extends KeyOf + ? { readonly value: userTypes[input] } + : input extends SchemaAbiType + ? { readonly value: input } + : input extends ValueSchemaInput + ? { + readonly [k in KeyOf]: input[k] extends KeyOf + ? userTypes[input[k]] + : input[k] extends SchemaAbiType + ? input[k] + : never; + } + : never; + +export function parseValueSchema>( + userTypes: userTypes, + input: input +): ParseValueSchemaOutput { + const userTypeNames = userTypes != null ? (Object.keys(userTypes) as KeyOf[]) : null; + return ( + userTypes != null && userTypeNames != null && includes(userTypeNames, input) + ? { value: userTypes[input] } + : isSchemaAbiType(input) + ? { value: input } + : Object.fromEntries( + Object.entries(input).map(([name, value]) => [ + name, + userTypes != null && userTypeNames != null && includes(userTypeNames, value) ? userTypes[value] : value, + ]) + ) + ) as ParseValueSchemaOutput; +} diff --git a/packages/store/ts/config/storeConfig.ts b/packages/store/ts/config/storeConfig.ts index a9f9071438..de092a5476 100644 --- a/packages/store/ts/config/storeConfig.ts +++ b/packages/store/ts/config/storeConfig.ts @@ -70,6 +70,7 @@ const zShorthandSchemaConfig = zFieldData.transform((fieldData) => { }); export const zSchemaConfig = zFullSchemaConfig.or(zShorthandSchemaConfig); +type schemaconfig = z.input; export type ResolvedSchema< TSchema extends Record, diff --git a/packages/store/ts/register/mudConfig.test-d.ts b/packages/store/ts/register/mudConfig.test-d.ts index 3252b86fa0..0a49157654 100644 --- a/packages/store/ts/register/mudConfig.test-d.ts +++ b/packages/store/ts/register/mudConfig.test-d.ts @@ -1,5 +1,6 @@ import { describe, expectTypeOf } from "vitest"; import { mudConfig } from "."; +import { Hex } from "viem"; describe("mudConfig", () => { // Test possible inference confusion. @@ -7,6 +8,10 @@ describe("mudConfig", () => { expectTypeOf< ReturnType< typeof mudConfig<{ + enums: { + Enum1: ["E1"]; + Enum2: ["E1"]; + }; tables: { Table1: { keySchema: { @@ -22,10 +27,6 @@ describe("mudConfig", () => { }; }; }; - enums: { - Enum1: ["E1"]; - Enum2: ["E1"]; - }; }> > >().toEqualTypeOf<{ @@ -49,6 +50,30 @@ describe("mudConfig", () => { }; }; }; + resolvedTables: { + Table1: { + tableId: Hex; + namespace: ""; + name: "Table1"; + keySchema: { + a: "uint8"; + }; + valueSchema: { + b: "uint8"; + }; + }; + Table2: { + tableId: Hex; + namespace: ""; + name: "Table1"; + keySchema: { + key: "bytes32"; + }; + valueSchema: { + a: "uint32"; + }; + }; + }; namespace: ""; storeImportPath: "@latticexyz/store/src/"; userTypesFilename: "common.sol"; diff --git a/packages/store/ts/register/mudConfig.ts b/packages/store/ts/register/mudConfig.ts index 907ef5fb1c..9d4e4b4939 100644 --- a/packages/store/ts/register/mudConfig.ts +++ b/packages/store/ts/register/mudConfig.ts @@ -2,6 +2,7 @@ import { mudCoreConfig, MUDCoreUserConfig } from "@latticexyz/config"; import { ExtractUserTypes, StringForUnion } from "@latticexyz/common/type-utils"; import { MUDUserConfig } from ".."; import { ExpandMUDUserConfig } from "./typeExtensions"; +import { parseConfig, ParseConfigOutput } from "../config/experimental/parseConfig"; /** mudCoreConfig wrapper to use generics in some options for better type inference */ export function mudConfig< @@ -10,7 +11,15 @@ export function mudConfig< EnumNames extends StringForUnion = never, UserTypeNames extends StringForUnion = never, StaticUserTypes extends ExtractUserTypes = ExtractUserTypes ->(config: MUDUserConfig): ExpandMUDUserConfig { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - return mudCoreConfig(config) as any; +>( + userConfig: MUDUserConfig +): ExpandMUDUserConfig & { + parsedConfig: ParseConfigOutput<{ [enumName in EnumNames]: "uint8" }, ExpandMUDUserConfig>; +} { + const config = mudCoreConfig(userConfig) as MUDUserConfig; + const parsedConfig = parseConfig(config); + return { + ...config, + parsedConfig, + }; } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 8942de00f0..a324a89ae0 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -773,6 +773,9 @@ importers: abitype: specifier: 0.9.8 version: 0.9.8(typescript@5.1.6)(zod@3.21.4) + viem: + specifier: 1.14.0 + version: 1.14.0(typescript@5.1.6)(zod@3.21.4) zod: specifier: ^3.21.4 version: 3.21.4