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

feat(store): add experimental config resolve helper #1826

Merged
merged 22 commits into from
Oct 27, 2023
Merged
Show file tree
Hide file tree
Changes from 19 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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;
}
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`
Copy link
Member

Choose a reason for hiding this comment

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

I remember trying to add as const to our default MUD configs (world config, store config) and it broke downstream types in weird ways. Hopefully we can just add this more specific as const to just the namespace without those downstream effects, but yeah, should aim to as const the whole config.

Copy link
Member Author

Choose a reason for hiding this comment

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

yeah the arrays for e.g. enums currently complain if the whole config is as const

Copy link
Member

@holic holic Oct 27, 2023

Choose a reason for hiding this comment

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

Do we wanna add as const to the world/store namespaces here?

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" },
});
});
});
162 changes: 162 additions & 0 deletions packages/store/ts/config/experimental/resolveConfig.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,162 @@
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];
};
};

export function resolveConfig<TStoreConfig extends StoreConfig>(
alvrs marked this conversation as resolved.
Show resolved Hide resolved
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,
};
});
}
Loading