From b1d41727d4b1964ad3cd907c1c2126b02172b413 Mon Sep 17 00:00:00 2001 From: alvarius Date: Fri, 27 Oct 2023 15:09:45 +0200 Subject: [PATCH] feat(store): add experimental config resolve helper (#1826) Co-authored-by: Kevin Ingersoll --- .changeset/three-scissors-smile.md | 5 + packages/common/src/utils/index.ts | 1 + packages/common/src/utils/mapObject.test.ts | 23 +++ packages/common/src/utils/mapObject.ts | 11 ++ packages/store/mud.config.ts | 2 +- .../experimental/resolveConfig.test-d.ts | 51 ++++++ .../config/experimental/resolveConfig.test.ts | 49 ++++++ .../ts/config/experimental/resolveConfig.ts | 166 ++++++++++++++++++ packages/world/mud.config.ts | 2 +- 9 files changed, 308 insertions(+), 2 deletions(-) create mode 100644 .changeset/three-scissors-smile.md create mode 100644 packages/common/src/utils/mapObject.test.ts create mode 100644 packages/common/src/utils/mapObject.ts create mode 100644 packages/store/ts/config/experimental/resolveConfig.test-d.ts create mode 100644 packages/store/ts/config/experimental/resolveConfig.test.ts create mode 100644 packages/store/ts/config/experimental/resolveConfig.ts diff --git a/.changeset/three-scissors-smile.md b/.changeset/three-scissors-smile.md new file mode 100644 index 0000000000..6626fe6a83 --- /dev/null +++ b/.changeset/three-scissors-smile.md @@ -0,0 +1,5 @@ +--- +"@latticexyz/common": minor +--- + +Added a `mapObject` helper to map the value of each property of an object to a new value. diff --git a/packages/common/src/utils/index.ts b/packages/common/src/utils/index.ts index 1ccd90fe05..c11ae82e51 100644 --- a/packages/common/src/utils/index.ts +++ b/packages/common/src/utils/index.ts @@ -8,6 +8,7 @@ export * from "./identity"; export * from "./isDefined"; export * from "./isNotNull"; export * from "./iteratorToArray"; +export * from "./mapObject"; export * from "./uniqueBy"; export * from "./wait"; export * from "./waitForIdle"; diff --git a/packages/common/src/utils/mapObject.test.ts b/packages/common/src/utils/mapObject.test.ts new file mode 100644 index 0000000000..9247791364 --- /dev/null +++ b/packages/common/src/utils/mapObject.test.ts @@ -0,0 +1,23 @@ +import { describe, expect, expectTypeOf, it } from "vitest"; +import { mapObject } from "./mapObject"; +import { assertExhaustive } from "./assertExhaustive"; + +describe("mapObject", () => { + it("should map the source to the target", () => { + const source = { + hello: "world", + foo: "bar", + } as const; + + type Mapped> = { [key in keyof T]: `mapped-${T[key]}` }; + + const target = mapObject>(source, (value, key) => { + if (key === "hello") return `mapped-${value}`; + if (key === "foo") return `mapped-${value}`; + assertExhaustive(key); + }); + + expect(target).toEqual({ hello: `mapped-world`, foo: `mapped-bar` }); + expectTypeOf().toEqualTypeOf>(); + }); +}); diff --git a/packages/common/src/utils/mapObject.ts b/packages/common/src/utils/mapObject.ts new file mode 100644 index 0000000000..189f568657 --- /dev/null +++ b/packages/common/src/utils/mapObject.ts @@ -0,0 +1,11 @@ +/** + * Map each key of a source object via a given valueMap function + */ +export function mapObject< + Source extends Record, + Target extends { [key in keyof Source]: unknown } +>(source: Source, valueMap: (value: Source[typeof key], key: keyof Source) => Target[typeof key]): Target { + return Object.fromEntries( + Object.entries(source).map(([key, value]) => [key, valueMap(value as Source[keyof Source], key)]) + ) as Target; +} diff --git a/packages/store/mud.config.ts b/packages/store/mud.config.ts index 7fcfeab1e3..63463c29f7 100644 --- a/packages/store/mud.config.ts +++ b/packages/store/mud.config.ts @@ -2,7 +2,7 @@ import { mudConfig } from "./ts/register"; export default mudConfig({ storeImportPath: "../../", - namespace: "store", + namespace: "store" as const, userTypes: { ResourceId: { filePath: "./src/ResourceId.sol", internalType: "bytes32" }, FieldLayout: { filePath: "./src/FieldLayout.sol", internalType: "bytes32" }, diff --git a/packages/store/ts/config/experimental/resolveConfig.test-d.ts b/packages/store/ts/config/experimental/resolveConfig.test-d.ts new file mode 100644 index 0000000000..1390bbd9fb --- /dev/null +++ b/packages/store/ts/config/experimental/resolveConfig.test-d.ts @@ -0,0 +1,51 @@ +import { describe, expectTypeOf } from "vitest"; +import { mudConfig } from "../../register/mudConfig"; +import { resolveConfig } from "./resolveConfig"; + +const config = resolveConfig( + mudConfig({ + // Seems like we need `as const` here to keep the strong type. + // Note it resolves to the strong `""` type if no namespace is provided. + // TODO: require the entire input config to be `const` + namespace: "the-namespace" as const, + userTypes: { + ResourceId: { + internalType: "bytes32", + filePath: "", + }, + }, + enums: { + ResourceType: ["namespace", "system", "table"], + }, + tables: { + Shorthand: { + keySchema: { + key: "ResourceId", + }, + valueSchema: "ResourceType", + }, + }, + }) +); + +describe("resolveConfig", () => { + expectTypeOf().toEqualTypeOf<"the-namespace">(); + + expectTypeOf().toEqualTypeOf<"Shorthand">(); + + expectTypeOf().toEqualTypeOf<`0x${string}`>(); + + expectTypeOf().toEqualTypeOf<{ + key: { + internalType: "ResourceId"; + type: "bytes32"; + }; + }>(); + + expectTypeOf().toEqualTypeOf<{ + value: { + internalType: "ResourceType"; + type: "uint8"; + }; + }>(); +}); diff --git a/packages/store/ts/config/experimental/resolveConfig.test.ts b/packages/store/ts/config/experimental/resolveConfig.test.ts new file mode 100644 index 0000000000..d6187ca73c --- /dev/null +++ b/packages/store/ts/config/experimental/resolveConfig.test.ts @@ -0,0 +1,49 @@ +import { describe, expect, it } from "vitest"; +import { mudConfig } from "../../register/mudConfig"; +import { resolveConfig } from "./resolveConfig"; +import { MUDCoreContext } from "@latticexyz/config"; +import { resourceToHex } from "@latticexyz/common"; +MUDCoreContext.createContext(); + +const config = resolveConfig( + mudConfig({ + namespace: "the-namespace", + userTypes: { + ResourceId: { + internalType: "bytes32", + filePath: "", + }, + }, + enums: { + ResourceType: ["namespace", "system", "table"], + }, + tables: { + Shorthand: { + keySchema: { + key: "ResourceId", + }, + valueSchema: "ResourceType", + }, + }, + }) +); + +describe("resolveConfig", () => { + it("should resolve userTypes and enums", () => { + expect(config.tables.Shorthand.namespace).toEqual("the-namespace"); + + expect(config.tables.Shorthand.name).toEqual("Shorthand"); + + expect(config.tables.Shorthand.tableId).toEqual( + resourceToHex({ type: "table", namespace: "the-namespace", name: "Shorthand" }) + ); + + expect(config.tables.Shorthand.keySchema).toEqual({ + key: { internalType: "ResourceId", type: "bytes32" }, + }); + + expect(config.tables.Shorthand.valueSchema).toEqual({ + value: { internalType: "ResourceType", type: "uint8" }, + }); + }); +}); diff --git a/packages/store/ts/config/experimental/resolveConfig.ts b/packages/store/ts/config/experimental/resolveConfig.ts new file mode 100644 index 0000000000..b02cb2e713 --- /dev/null +++ b/packages/store/ts/config/experimental/resolveConfig.ts @@ -0,0 +1,166 @@ +import { StringForUnion } from "@latticexyz/common/type-utils"; +import { StoreConfig, TableConfig, UserTypesConfig } from "../storeConfig"; +import { UserType } from "@latticexyz/common/codegen"; +import { mapObject } from "@latticexyz/common/utils"; +import { resourceToHex } from "@latticexyz/common"; + +export type ResolvedStoreConfig = { + tables: { + [TableKey in keyof TStoreConfig["tables"] & string]: ResolvedTableConfig< + TStoreConfig["tables"][TableKey], + TStoreConfig["userTypes"], + keyof TStoreConfig["enums"] & string, + TStoreConfig["namespace"], + TableKey + >; + }; +}; + +export type ResolvedTableConfig< + TTableConfig extends TableConfig, + TUserTypes extends UserTypesConfig["userTypes"], + TEnumNames extends StringForUnion, + TNamespace extends string = string, + TName extends string = string +> = Omit & { + keySchema: ResolvedKeySchema; + valueSchema: ResolvedValueSchema; + namespace: TNamespace; + name: TName; + tableId: `0x${string}`; +}; + +export type ResolvedKeySchema< + TKeySchema extends TableConfig["keySchema"], + TUserTypes extends UserTypesConfig["userTypes"], + TEnumNames extends StringForUnion +> = ResolvedSchema; + +export type ResolvedValueSchema< + TValueSchema extends TableConfig["valueSchema"], + TUserTypes extends UserTypesConfig["userTypes"], + TEnumNames extends StringForUnion +> = ResolvedSchema, TUserTypes, TEnumNames>; + +export type ResolvedSchema< + TSchema extends Exclude, + TUserTypes extends UserTypesConfig["userTypes"], + TEnumNames extends StringForUnion +> = { + [key in keyof TSchema]: { + type: TSchema[key] extends keyof TUserTypes + ? TUserTypes[TSchema[key]] extends UserType + ? // Note: we mistakenly named the plain ABI type "internalType", + // while in Solidity ABIs the plain ABI type is called "type" and + // and the custom type "internalType". We're planning to + // change our version and align with Solidity ABIs going forward. + TUserTypes[TSchema[key]]["internalType"] + : never + : TSchema[key] extends TEnumNames + ? "uint8" + : TSchema[key]; + internalType: TSchema[key]; + }; +}; + +/** + * @internal Internal only + * @deprecated Internal only + */ +export function resolveConfig( + config: TStoreConfig +): ResolvedStoreConfig { + const resolvedTables: Record> = {}; + + for (const key of Object.keys(config.tables)) { + resolvedTables[key] = resolveTable( + config.tables[key], + config.userTypes, + Object.keys(config.enums), + config.namespace, + key + ) as ReturnType; + } + + return { + tables: resolvedTables as ResolvedStoreConfig["tables"], + }; +} + +function resolveTable< + TTableConfig extends TableConfig, + TUserTypes extends UserTypesConfig["userTypes"], + TEnums extends StringForUnion[], + TNamespace extends string, + TName extends string +>( + tableConfig: TTableConfig, + userTypes: TUserTypes, + enums: TEnums, + namespace: TNamespace, + name: TName +): ResolvedTableConfig { + const { keySchema, valueSchema, ...rest } = tableConfig; + + return { + ...rest, + keySchema: resolveKeySchema(keySchema, userTypes, enums), + valueSchema: resolveValueSchema(valueSchema, userTypes, enums) as ResolvedSchema< + Exclude, + TUserTypes, + TEnums[number] + >, + namespace, + name, + tableId: resourceToHex({ type: "table", namespace, name }), + }; +} + +function resolveKeySchema< + TKeySchema extends TableConfig["keySchema"], + TUserTypes extends UserTypesConfig["userTypes"], + TEnums extends StringForUnion[] +>( + keySchema: TKeySchema, + userTypes: TUserTypes, + enums: TEnums +): ResolvedKeySchema { + const schema = ( + keySchema == null ? { key: "bytes32" } : typeof keySchema === "string" ? { key: keySchema } : keySchema + ) as TKeySchema extends undefined ? { key: "bytes32" } : TKeySchema; + return resolveSchema(schema, userTypes, enums); +} + +function resolveValueSchema< + TValueSchema extends TableConfig["valueSchema"], + TUserTypes extends UserTypesConfig["userTypes"], + TEnums extends StringForUnion[] +>( + valueSchema: TValueSchema, + userTypes: TUserTypes, + enums: TEnums +): ResolvedValueSchema { + const schema = ( + typeof valueSchema === "string" ? ({ value: valueSchema } as unknown as TValueSchema) : valueSchema + ) as Exclude; + return resolveSchema(schema, userTypes, enums); +} + +function resolveSchema< + TSchema extends Exclude | TableConfig["valueSchema"], string>, + TUserTypes extends UserTypesConfig["userTypes"], + TEnums extends StringForUnion[] +>(schema: TSchema, userTypes: TUserTypes, enums: TEnums): ResolvedSchema { + return mapObject>(schema, (value, key) => { + const isUserType = userTypes && value in userTypes; + const isEnum = enums.includes(value); + return { + type: (isUserType ? userTypes[value].internalType : isEnum ? ("uint8" as const) : value) as ResolvedSchema< + TSchema, + TUserTypes, + TEnums[number] + >[typeof key]["type"], + internalType: value, + }; + }); +} diff --git a/packages/world/mud.config.ts b/packages/world/mud.config.ts index dbafd51d2a..bd15594a38 100644 --- a/packages/world/mud.config.ts +++ b/packages/world/mud.config.ts @@ -4,7 +4,7 @@ export default mudConfig({ worldImportPath: "../../", worldgenDirectory: "interfaces", worldInterfaceName: "IBaseWorld", - namespace: "world", // NOTE: this namespace is only used for tables, the core system is deployed in the root namespace. + namespace: "world" as const, // NOTE: this namespace is only used for tables, the core system is deployed in the root namespace. userTypes: { ResourceId: { filePath: "@latticexyz/store/src/ResourceId.sol", internalType: "bytes32" }, },