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

wip parsed config #1818

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all 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
2 changes: 1 addition & 1 deletion packages/schema-type/src/typescript/dynamicAbiTypes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,6 @@ export type DynamicAbiTypeToPrimitiveType<TDynamicAbiType extends DynamicAbiType
(typeof dynamicAbiTypeToDefaultValue)[TDynamicAbiType]
>;

export function isDynamicAbiType(abiType: string): abiType is DynamicAbiType {
export function isDynamicAbiType(abiType: unknown): abiType is DynamicAbiType {
return dynamicAbiTypes.includes(abiType as DynamicAbiType);
}
4 changes: 4 additions & 0 deletions packages/schema-type/src/typescript/schemaAbiTypes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -211,3 +211,7 @@ export const dynamicAbiTypes = schemaAbiTypes.slice(98) as any as TupleSplit<typ

export type StaticAbiType = (typeof staticAbiTypes)[number];
export type DynamicAbiType = (typeof dynamicAbiTypes)[number];

export function isSchemaAbiType(abiType: unknown): abiType is SchemaAbiType {
return schemaAbiTypes.includes(abiType as SchemaAbiType);
}
2 changes: 1 addition & 1 deletion packages/schema-type/src/typescript/staticAbiTypes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -218,6 +218,6 @@ export const staticAbiTypeToByteLength = {
address: 20,
} as const satisfies Record<StaticAbiType, number>;

export function isStaticAbiType(abiType: string): abiType is StaticAbiType {
export function isStaticAbiType(abiType: unknown): abiType is StaticAbiType {
return staticAbiTypes.includes(abiType as StaticAbiType);
}
1 change: 1 addition & 0 deletions packages/store/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@
"@latticexyz/config": "workspace:*",
"@latticexyz/schema-type": "workspace:*",
"abitype": "0.9.8",
"viem": "1.14.0",
"zod": "^3.21.4"
},
"devDependencies": {
Expand Down
90 changes: 90 additions & 0 deletions packages/store/ts/config/common.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
import { SchemaAbiType, StaticAbiType } from "@latticexyz/schema-type";
import { ExpandTablesConfig, StoreConfig, StoreUserConfig, resolveUserTypes } from "./storeConfig";
import { Hex } from "viem";
import { resourceToHex } from "@latticexyz/common";

// export type ConfigToResolvedTables<
// config extends StoreUserConfig,
// tables extends ExpandTablesConfig<config["tables"]>
// > = {
// [tableName in keyof tables]: {
// tableId: Hex;
// namespace: config["namespace"];
// name: tableName;
// keySchema: tables[tableName]["keySchema"];
// valueSchema: tables[tableName]["valueSchema"];
// };
// };

// TODO: helper to filter user types to StaticAbiType
export type UserTypes = Record<string, { internalType: SchemaAbiType }>;

export type KeySchema<userTypes extends UserTypes | undefined = undefined> = Record<
string,
userTypes extends UserTypes ? StaticAbiType | keyof userTypes : StaticAbiType
>;
export type ValueSchema<userTypes extends UserTypes | undefined = undefined> = Record<
string,
userTypes extends UserTypes ? SchemaAbiType | keyof userTypes : SchemaAbiType
>;

type ConfigToUserTypes<config extends StoreConfig = StoreConfig> = config["userTypes"];
// TODO: fix strong enum types and avoid every schema getting `{ [k: string]: "uint8" }`
// type ConfigToUserTypes<config extends StoreConfig = StoreConfig> = config["userTypes"] & {
// [k in keyof config["enums"]]: { internalType: "uint8" };
// };

export type TableKey<
config extends StoreConfig = StoreConfig,
table extends config["tables"][keyof config["tables"]] = config["tables"][keyof config["tables"]]
> = `${config["namespace"]}_${table["name"]}`;

export type Table<
config extends StoreConfig = StoreConfig,
table extends config["tables"][keyof config["tables"]] = config["tables"][keyof config["tables"]]
> = {
readonly namespace: config["namespace"];
readonly name: table["name"];
readonly tableId: Hex;
readonly keySchema: table["keySchema"] extends KeySchema<ConfigToUserTypes<config>>
? KeySchema & {
readonly [k in keyof table["keySchema"]]: ConfigToUserTypes<config>[table["keySchema"][k]]["internalType"] extends StaticAbiType
? ConfigToUserTypes<config>[table["keySchema"][k]]["internalType"]
: table["keySchema"][k];
}
: KeySchema;
readonly valueSchema: table["valueSchema"] extends ValueSchema<ConfigToUserTypes<config>>
? {
readonly [k in keyof table["valueSchema"]]: ConfigToUserTypes<config>[table["valueSchema"][k]]["internalType"] extends SchemaAbiType
? ConfigToUserTypes<config>[table["valueSchema"][k]]["internalType"]
: table["valueSchema"][k];
}
: ValueSchema;
};

export type Tables<config extends StoreConfig = StoreConfig> = {
readonly [k in keyof config["tables"] as TableKey<config, config["tables"][k]>]: Table<config, config["tables"][k]>;
};

export function configToTables<config extends StoreConfig>(config: config): Tables<config> {
const userTypes = {
...config.userTypes,
...Object.fromEntries(Object.entries(config.enums).map(([key]) => [key, { internalType: "uint8" }] as const)),
};
return Object.fromEntries(
Object.entries(config.tables).map(([tableName, table]) => [
`${config.namespace}_${tableName}` satisfies TableKey<config, config["tables"][keyof config["tables"]]>,
{
namespace: config.namespace,
name: table.name,
tableId: resourceToHex({
type: table.offchainOnly ? "offchainTable" : "table",
namespace: config.namespace,
name: table.name,
}),
keySchema: resolveUserTypes(table.keySchema, userTypes) as any,
valueSchema: resolveUserTypes(table.valueSchema, userTypes) as any,
} satisfies Table<config, config["tables"][keyof config["tables"]]>,
])
) as Tables<config>;
}
26 changes: 26 additions & 0 deletions packages/store/ts/config/experimental/common.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { SchemaAbiType } from "@latticexyz/schema-type";

export type EmptyObject = { readonly [k: string]: never };

export type Prettify<T> = {
[K in keyof T as T[K] extends never ? never : K]: T[K];
} & unknown;

export type Merge<Object1, Object2> = Omit<Object1, keyof Object2> & Object2;

/** @internal */
export function isPlainObject(value: unknown): value is Record<string, unknown> {
return (
typeof value === "object" &&
value !== null &&
value.constructor === Object &&
Object.prototype.toString.call(value) === "[object Object]"
);
}

export type UserTypes = { readonly [k: string]: SchemaAbiType } | undefined;
export type KeyOf<T> = keyof T & string;

export function includes<T>(values: readonly T[], value: unknown): value is T {
return values.includes(value as T);
}
26 changes: 26 additions & 0 deletions packages/store/ts/config/experimental/parseConfig.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { UserTypes } from "./common";
import { ParseTablesInput, ParseTablesOutput, parseTables } from "./parseTables";

export type ParseConfigInput<userTypes extends UserTypes> = {
readonly userTypes?: userTypes;
readonly namespace?: string;
readonly tables: ParseTablesInput<userTypes>;
};

export type ParseConfigOutput<userTypes extends UserTypes, input extends ParseConfigInput<userTypes>> = {
// TODO: ensure that tables of the same name get replaced and are not a union
readonly tables: ParseTablesOutput<
userTypes,
input["namespace"] extends string ? input["namespace"] : "",
input["tables"]
>;
};

export function parseConfig<userTypes extends UserTypes, input extends ParseConfigInput<userTypes>>(
input: input
): ParseConfigOutput<userTypes, input> {
const tables = Object.entries(parseTables(input.userTypes, input.namespace ?? "", input.tables));
return {
tables: Object.fromEntries(tables),
} as ParseConfigOutput<userTypes, input>;
}
21 changes: 21 additions & 0 deletions packages/store/ts/config/experimental/parseKeySchema.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { StaticAbiType, isStaticAbiType } from "@latticexyz/schema-type";

export type KeySchema = { readonly [k: string]: StaticAbiType };

export const defaultKeySchema = { key: "bytes32" } as const satisfies KeySchema;

export type ParseKeySchemaInput = StaticAbiType | KeySchema | undefined;

export type ParseKeySchemaOutput<input extends ParseKeySchemaInput> = input extends undefined
? typeof defaultKeySchema
: input extends StaticAbiType
? { readonly key: input }
: input extends KeySchema
? input
: never;

export function parseKeySchema<input extends ParseKeySchemaInput>(input: input): ParseKeySchemaOutput<input> {
return (
input === undefined ? defaultKeySchema : isStaticAbiType(input) ? { key: input } : input
) as ParseKeySchemaOutput<input>;
}
87 changes: 87 additions & 0 deletions packages/store/ts/config/experimental/parseTable.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
import { SchemaAbiType, isSchemaAbiType } from "@latticexyz/schema-type";
import { KeyOf, UserTypes, includes, isPlainObject } from "./common";
import { ParseKeySchemaInput, ParseKeySchemaOutput, parseKeySchema } from "./parseKeySchema";
import { ParseValueSchemaInput, ParseValueSchemaOutput, parseValueSchema } from "./parseValueSchema";
import { assertExhaustive } from "@latticexyz/common/utils";
import { resourceToHex } from "@latticexyz/common";

/** @internal */
export type TableShapeInput<userTypes extends UserTypes> = {
readonly namespace?: string;
readonly keySchema?: ParseKeySchemaInput;
readonly valueSchema: ParseValueSchemaInput<userTypes>;
readonly offchainOnly?: boolean;
};

export type ParseTableInput<userTypes extends UserTypes> =
| KeyOf<userTypes>
| SchemaAbiType
| TableShapeInput<userTypes>;

export type ParseTableOutput<
userTypes extends UserTypes,
defaultNamespace extends string,
name extends string,
input extends ParseTableInput<userTypes>
> = input extends SchemaAbiType
? ParseTableOutput<userTypes, defaultNamespace, name, { readonly valueSchema: input }>
: input extends TableShapeInput<userTypes>
? {
readonly type: input["offchainOnly"] extends true ? "offchainTable" : "table";
readonly namespace: input["namespace"] extends string ? input["namespace"] : defaultNamespace;
readonly name: name;
readonly tableId: `0x${string}`;
readonly keySchema: ParseKeySchemaOutput<
input["keySchema"] extends ParseKeySchemaInput
? input["keySchema"]
: never extends input["keySchema"]
? undefined
: never
>;
readonly valueSchema: ParseValueSchemaOutput<userTypes, input["valueSchema"]>;
}
: never;

// TODO: is there a better way to check this aside from just looking at the shape/keys of the object?

/** @internal */
export const tableInputShapeKeys = ["namespace", "keySchema", "valueSchema", "offchainOnly"] as const;

/** @internal */
export function isTableShapeInput<userTypes extends UserTypes>(input: unknown): input is TableShapeInput<userTypes> {
if (!isPlainObject(input)) return false;
if (Object.keys(input).some((key) => !includes(tableInputShapeKeys, key))) return false;
return true;
}

export function parseTable<
userTypes extends UserTypes,
defaultNamespace extends string,
name extends string,
input extends ParseTableInput<userTypes>
>(
userTypes: UserTypes,
defaultNamespace: defaultNamespace,
name: name,
input: input
): ParseTableOutput<userTypes, defaultNamespace, name, input> {
const userTypeNames = userTypes != null ? (Object.keys(userTypes) as unknown as readonly KeyOf<userTypes>[]) : null;
return (
isSchemaAbiType(input) || (userTypeNames != null && includes(userTypeNames, input))
? parseTable(userTypes, defaultNamespace, name, { valueSchema: input } as const)
: isTableShapeInput<userTypes>(input)
? {
type: input.offchainOnly === true ? "offchainTable" : "table",
namespace: input.namespace ?? defaultNamespace,
name,
tableId: resourceToHex({
type: input.offchainOnly === true ? "offchainTable" : "table",
namespace: input.namespace ?? defaultNamespace,
name,
}),
keySchema: parseKeySchema(input.keySchema),
valueSchema: parseValueSchema(userTypes, input.valueSchema),
}
: assertExhaustive(input, "invalid table input")
) as ParseTableOutput<userTypes, defaultNamespace, name, input>;
}
31 changes: 31 additions & 0 deletions packages/store/ts/config/experimental/parseTables.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import { KeyOf, UserTypes } from "./common";
import { ParseTableInput, ParseTableOutput, parseTable } from "./parseTable";

export type ParseTablesInput<userTypes extends UserTypes> = { readonly [k: string]: ParseTableInput<userTypes> };

export type ParseTablesOutput<
userTypes extends UserTypes,
defaultNamespace extends string,
input extends ParseTablesInput<userTypes>
> = {
readonly [name in KeyOf<input>]: ParseTableOutput<
userTypes,
input["namespace"] extends string ? input["namespace"] : defaultNamespace,
name,
input[name]
>;
};

export function parseTables<
userTypes extends UserTypes,
defaultNamespace extends string,
input extends ParseTablesInput<userTypes>
>(
userTypes: UserTypes,
defaultNamespace: defaultNamespace,
input: input
): ParseTablesOutput<userTypes, defaultNamespace, input> {
return Object.fromEntries(
Object.entries(input).map(([name, tableInput]) => [name, parseTable(userTypes, defaultNamespace, name, tableInput)])
) as ParseTablesOutput<userTypes, defaultNamespace, input>;
}
45 changes: 45 additions & 0 deletions packages/store/ts/config/experimental/parseValueSchema.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import { SchemaAbiType, isSchemaAbiType } from "@latticexyz/schema-type";
import { KeyOf, UserTypes, includes } from "./common";

export type ValueSchemaInput<userTypes extends UserTypes> = { readonly [k: string]: SchemaAbiType | KeyOf<userTypes> };

export type ParseValueSchemaInput<userTypes extends UserTypes> =
| KeyOf<userTypes>
| SchemaAbiType
| ValueSchemaInput<userTypes>;

export type ParseValueSchemaOutput<
userTypes extends UserTypes,
input extends ParseValueSchemaInput<userTypes>
> = input extends KeyOf<userTypes>
? { readonly value: userTypes[input] }
: input extends SchemaAbiType
? { readonly value: input }
: input extends ValueSchemaInput<userTypes>
? {
readonly [k in KeyOf<input>]: input[k] extends KeyOf<userTypes>
? userTypes[input[k]]
: input[k] extends SchemaAbiType
? input[k]
: never;
}
: never;

export function parseValueSchema<userTypes extends UserTypes, input extends ParseValueSchemaInput<userTypes>>(
userTypes: userTypes,
input: input
): ParseValueSchemaOutput<userTypes, input> {
const userTypeNames = userTypes != null ? (Object.keys(userTypes) as KeyOf<userTypes>[]) : null;
return (
userTypes != null && userTypeNames != null && includes(userTypeNames, input)
? { value: userTypes[input] }
: isSchemaAbiType(input)
? { value: input }
: Object.fromEntries(
Object.entries(input).map(([name, value]) => [
name,
userTypes != null && userTypeNames != null && includes(userTypeNames, value) ? userTypes[value] : value,
])
)
) as ParseValueSchemaOutput<userTypes, input>;
}
1 change: 1 addition & 0 deletions packages/store/ts/config/storeConfig.ts
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,7 @@ const zShorthandSchemaConfig = zFieldData.transform((fieldData) => {
});

export const zSchemaConfig = zFullSchemaConfig.or(zShorthandSchemaConfig);
type schemaconfig = z.input<typeof zSchemaConfig>;

export type ResolvedSchema<
TSchema extends Record<string, string>,
Expand Down
Loading