-
Notifications
You must be signed in to change notification settings - Fork 196
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(store): add experimental config resolve helper (#1826)
Co-authored-by: Kevin Ingersoll <[email protected]>
- Loading branch information
Showing
9 changed files
with
308 additions
and
2 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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>>(); | ||
}); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
51 changes: 51 additions & 0 deletions
51
packages/store/ts/config/experimental/resolveConfig.test-d.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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
49
packages/store/ts/config/experimental/resolveConfig.test.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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" }, | ||
}); | ||
}); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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, | ||
}; | ||
}); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters