Skip to content

Commit

Permalink
feat(store): add experimental config resolve helper (#1826)
Browse files Browse the repository at this point in the history
Co-authored-by: Kevin Ingersoll <kingersoll@gmail.com>
  • Loading branch information
alvrs and holic committed Oct 27, 2023
1 parent f6d214e commit b1d4172
Show file tree
Hide file tree
Showing 9 changed files with 308 additions and 2 deletions.
5 changes: 5 additions & 0 deletions .changeset/three-scissors-smile.md
Original file line number Diff line number Diff line change
@@ -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.
1 change: 1 addition & 0 deletions packages/common/src/utils/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
23 changes: 23 additions & 0 deletions packages/common/src/utils/mapObject.test.ts
Original file line number Diff line number Diff line change
@@ -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<T extends Record<string, string>> = { [key in keyof T]: `mapped-${T[key]}` };

const target = mapObject<typeof source, Mapped<typeof source>>(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<typeof target>().toEqualTypeOf<Mapped<typeof source>>();
});
});
11 changes: 11 additions & 0 deletions packages/common/src/utils/mapObject.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
/**
* Map each key of a source object via a given valueMap function
*/
export function mapObject<
Source extends Record<string | number | symbol, unknown>,
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;
}
2 changes: 1 addition & 1 deletion packages/store/mud.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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" },
Expand Down
51 changes: 51 additions & 0 deletions packages/store/ts/config/experimental/resolveConfig.test-d.ts
Original file line number Diff line number Diff line change
@@ -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<typeof config.tables.Shorthand.namespace>().toEqualTypeOf<"the-namespace">();

expectTypeOf<typeof config.tables.Shorthand.name>().toEqualTypeOf<"Shorthand">();

expectTypeOf<typeof config.tables.Shorthand.tableId>().toEqualTypeOf<`0x${string}`>();

expectTypeOf<typeof config.tables.Shorthand.keySchema>().toEqualTypeOf<{
key: {
internalType: "ResourceId";
type: "bytes32";
};
}>();

expectTypeOf<typeof config.tables.Shorthand.valueSchema>().toEqualTypeOf<{
value: {
internalType: "ResourceType";
type: "uint8";
};
}>();
});
49 changes: 49 additions & 0 deletions packages/store/ts/config/experimental/resolveConfig.test.ts
Original file line number Diff line number Diff line change
@@ -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" },
});
});
});
166 changes: 166 additions & 0 deletions packages/store/ts/config/experimental/resolveConfig.ts
Original file line number Diff line number Diff line change
@@ -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<TStoreConfig extends StoreConfig> = {
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<TTableConfig, "keySchema" | "valueSchema"> & {
keySchema: ResolvedKeySchema<TTableConfig["keySchema"], TUserTypes, TEnumNames>;
valueSchema: ResolvedValueSchema<TTableConfig["valueSchema"], TUserTypes, TEnumNames>;
namespace: TNamespace;
name: TName;
tableId: `0x${string}`;
};

export type ResolvedKeySchema<
TKeySchema extends TableConfig["keySchema"],
TUserTypes extends UserTypesConfig["userTypes"],
TEnumNames extends StringForUnion
> = ResolvedSchema<TKeySchema, TUserTypes, TEnumNames>;

export type ResolvedValueSchema<
TValueSchema extends TableConfig["valueSchema"],
TUserTypes extends UserTypesConfig["userTypes"],
TEnumNames extends StringForUnion
> = ResolvedSchema<Exclude<TValueSchema, string>, TUserTypes, TEnumNames>;

export type ResolvedSchema<
TSchema extends Exclude<TableConfig["keySchema"] | TableConfig["valueSchema"], string>,
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<TStoreConfig extends StoreConfig>(
config: TStoreConfig
): ResolvedStoreConfig<TStoreConfig> {
const resolvedTables: Record<string, ReturnType<typeof resolveTable>> = {};

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<typeof resolveTable>;
}

return {
tables: resolvedTables as ResolvedStoreConfig<TStoreConfig>["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<typeof tableConfig, TUserTypes, TEnums[number]> {
const { keySchema, valueSchema, ...rest } = tableConfig;

return {
...rest,
keySchema: resolveKeySchema(keySchema, userTypes, enums),
valueSchema: resolveValueSchema(valueSchema, userTypes, enums) as ResolvedSchema<
Exclude<TTableConfig["valueSchema"], string>,
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<TKeySchema extends undefined ? { key: "bytes32" } : TKeySchema, TUserTypes, TEnums[number]> {
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<TValueSchema, TUserTypes, TEnums[number]> {
const schema = (
typeof valueSchema === "string" ? ({ value: valueSchema } as unknown as TValueSchema) : valueSchema
) as Exclude<TValueSchema, string>;
return resolveSchema(schema, userTypes, enums);
}

function resolveSchema<
TSchema extends Exclude<NonNullable<TableConfig["keySchema"]> | TableConfig["valueSchema"], string>,
TUserTypes extends UserTypesConfig["userTypes"],
TEnums extends StringForUnion[]
>(schema: TSchema, userTypes: TUserTypes, enums: TEnums): ResolvedSchema<TSchema, TUserTypes, TEnums[number]> {
return mapObject<TSchema, ResolvedSchema<TSchema, TUserTypes, TEnums[number]>>(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,
};
});
}
2 changes: 1 addition & 1 deletion packages/world/mud.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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" },
},
Expand Down

0 comments on commit b1d4172

Please sign in to comment.