-
Notifications
You must be signed in to change notification settings - Fork 196
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
Changes from 19 commits
Commits
Show all changes
22 commits
Select commit
Hold shift + click to select a range
d02e1cb
feat(store): add experimental resolveConfig util
alvrs 170ceb7
remove dev code
alvrs 45a96bd
add enum support
alvrs 5bcba0b
remove dev config
alvrs 3bf37f4
remove test enum
alvrs 0bc6229
add actual resolver
alvrs 7ae1e57
add type test
alvrs 8a01828
add tests and fixes
alvrs 390a282
add mapObject util type
alvrs 55bb227
clean up
alvrs 6aa8b3a
minor clean up
alvrs 5ecd868
fix test
alvrs c894a6d
add namespace, name and tableId
alvrs 5fe80f4
Create three-scissors-smile.md
alvrs fc2560c
Create dirty-cows-lie.md
alvrs 839f313
update mapObject
alvrs 30d1d13
reverse argument order on mapObject
alvrs 81ebb25
remove nested _resolve key
alvrs 6f659fa
remove internal changeset
alvrs 22ab299
add const to store and world
alvrs 4f60103
Update packages/store/ts/config/experimental/resolveConfig.ts
alvrs 5295bb2
prettier
alvrs File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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; | ||
} |
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,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, | ||
}; | ||
}); | ||
} |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
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 specificas const
to just the namespace without those downstream effects, but yeah, should aim toas const
the whole config.There was a problem hiding this comment.
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 isas const
There was a problem hiding this comment.
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?