Skip to content

Commit

Permalink
refactor(store, world): separate shorthand config from full config re…
Browse files Browse the repository at this point in the history
…solvers and cleanup (#2464)
  • Loading branch information
alvrs authored Mar 19, 2024
1 parent db30280 commit f6b1473
Show file tree
Hide file tree
Showing 40 changed files with 1,994 additions and 1,940 deletions.
47 changes: 47 additions & 0 deletions packages/store/ts/config/v2/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
# Config conventions

These are the types and functions that should be placed in a file called `x.ts`

```ts
/**
* validateX returns input if the input is valid and expected type otherwise.
* This makes it such that there are fine grained (and custom) type errors on the input of defineX.
*/
type validateX<x> = { [key in keyof x]: x[key] extends Expected ? x[key] : Expected };

/**
* validateX function throws a runtime error if x is not X and has a type assertion
*/
function validateX(x: unknonw): asserts x is X {
//
}

/**
* resolveX expects a valid input type and maps it to the resolved output type
*/
type resolveX<x> = x extends X ? { [key in keyof x]: Resolved } : never;

/**
* defineX function validates the input types and calls resolveX to resolve it.
* Note: the runtime validation happens in `resolveX`.
* This is to take advantage of the type assertion in the function body.
*/
function defineX<const x>(x: validateX<x>): resolveX<x> {
return resolveX(x);
}

/**
* resolveX function does not validate the input type, but validates the runtime types.
* (This is to take advantage of the type assertion in the function body).
* This function is used by defineX and other higher level resolution functions.
*/
function resolveX<const x extends X>(x: x): resolveX<x> {
validateX(x);
//
}
```

There are two files that fall out of this patten: `input.ts` and `output.ts`:

- `input.ts` includes the flattened input types. They are supposed to be broad and not include all constraints. The stronger constraints are implemented in the `validateX` helpers.
- `output.ts` includes the flattened output types. They are supposed to be broad so downstream consumers can use them as input types for working with the config, and strongly typed config outputs will be assignable to them.
12 changes: 12 additions & 0 deletions packages/store/ts/config/v2/codegen.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { CODEGEN_DEFAULTS } from "./defaults";
import { isObject, mergeIfUndefined } from "./generics";

export type resolveCodegen<codegen> = codegen extends {}
? mergeIfUndefined<codegen, typeof CODEGEN_DEFAULTS>
: typeof CODEGEN_DEFAULTS;

export function resolveCodegen<codegen>(codegen: codegen): resolveCodegen<codegen> {
return (
isObject(codegen) ? mergeIfUndefined(codegen, CODEGEN_DEFAULTS) : CODEGEN_DEFAULTS
) as resolveCodegen<codegen>;
}
16 changes: 8 additions & 8 deletions packages/store/ts/config/v2/compat.test.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,15 @@
import { describe, it } from "vitest";
import { resolveStoreConfig } from "./store";
import { defineStore } from "./store";
import { attest } from "@arktype/attest";
import { StoreConfig as StoreConfigV1 } from "../storeConfig";
import { mudConfig } from "../../register";
import { configToV1 } from "./compat";
import { Config } from "./output";
import { storeToV1 } from "./compat";
import { Store } from "./output";

describe("configToV1", () => {
it("should transform the broad v2 output to the broad v1 output", () => {
attest<StoreConfigV1, configToV1<Config>>();
attest<configToV1<Config>, StoreConfigV1>();
attest<StoreConfigV1, storeToV1<Store>>();
attest<storeToV1<Store>, StoreConfigV1>();
});

it("should transform a v2 store config output to the v1 config output", () => {
Expand Down Expand Up @@ -53,7 +53,7 @@ describe("configToV1", () => {
},
}) satisfies StoreConfigV1;

const configV2 = resolveStoreConfig({
const configV2 = defineStore({
enums: {
TerrainType: ["None", "Ocean", "Grassland", "Desert"],
},
Expand Down Expand Up @@ -87,7 +87,7 @@ describe("configToV1", () => {
},
});

attest<typeof configV1>(configToV1(configV2)).equals(configV1);
attest<configToV1<typeof configV2>>(configV1);
attest<typeof configV1>(storeToV1(configV2)).equals(configV1);
attest<storeToV1<typeof configV2>>(configV1);
});
});
42 changes: 21 additions & 21 deletions packages/store/ts/config/v2/compat.ts
Original file line number Diff line number Diff line change
@@ -1,22 +1,22 @@
import { conform } from "@arktype/util";
import { Config, Table } from "./output";
import { Store, Table } from "./output";
import { mapObject } from "@latticexyz/common/utils";

export type configToV1<config> = config extends Config
export type storeToV1<store> = store extends Store
? {
namespace: config["namespace"];
enums: { [key in keyof config["enums"]]: string[] };
namespace: store["namespace"];
enums: { [key in keyof store["enums"]]: string[] };
userTypes: {
[key in keyof config["userTypes"]]: {
internalType: config["userTypes"][key]["type"];
[key in keyof store["userTypes"]]: {
internalType: store["userTypes"][key]["type"];
filePath: string;
};
};
storeImportPath: config["codegen"]["storeImportPath"];
userTypesFilename: config["codegen"]["userTypesFilename"];
codegenDirectory: config["codegen"]["codegenDirectory"];
codegenIndexFilename: config["codegen"]["codegenIndexFilename"];
tables: { [key in keyof config["tables"]]: tableToV1<config["tables"][key]> };
storeImportPath: store["codegen"]["storeImportPath"];
userTypesFilename: store["codegen"]["userTypesFilename"];
codegenDirectory: store["codegen"]["codegenDirectory"];
codegenIndexFilename: store["codegen"]["codegenIndexFilename"];
tables: { [key in keyof store["tables"]]: tableToV1<store["tables"][key]> };
}
: never;

Expand All @@ -31,13 +31,13 @@ export type tableToV1<table extends Table> = {
name: table["name"];
};

export function configToV1<config>(config: conform<config, Config>): configToV1<config> {
const resolvedUserTypes = mapObject(config.userTypes, ({ type, filePath }) => ({
export function storeToV1<store>(store: conform<store, Store>): storeToV1<store> {
const resolvedUserTypes = mapObject(store.userTypes, ({ type, filePath }) => ({
internalType: type,
filePath,
}));

const resolvedTables = mapObject(config.tables, (table) => ({
const resolvedTables = mapObject(store.tables, (table) => ({
directory: table.codegen.directory,
dataStruct: table.codegen.dataStruct,
tableIdArgument: table.codegen.tableIdArgument,
Expand All @@ -49,13 +49,13 @@ export function configToV1<config>(config: conform<config, Config>): configToV1<
}));

return {
namespace: config.namespace,
enums: config.enums,
namespace: store.namespace,
enums: store.enums,
userTypes: resolvedUserTypes,
storeImportPath: config.codegen.storeImportPath,
userTypesFilename: config.codegen.userTypesFilename,
codegenDirectory: config.codegen.codegenDirectory,
codegenIndexFilename: config.codegen.codegenIndexFilename,
storeImportPath: store.codegen.storeImportPath,
userTypesFilename: store.codegen.userTypesFilename,
codegenDirectory: store.codegen.codegenDirectory,
codegenIndexFilename: store.codegen.codegenIndexFilename,
tables: resolvedTables,
} as unknown as configToV1<config>;
} as unknown as storeToV1<store>;
}
1 change: 1 addition & 0 deletions packages/store/ts/config/v2/defaults.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ export const TABLE_CODEGEN_DEFAULTS = {
} as const;

export const TABLE_DEFAULTS = {
namespace: "",
type: "table",
} as const;

Expand Down
29 changes: 29 additions & 0 deletions packages/store/ts/config/v2/enums.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import { Enums } from "./output";
import { AbiTypeScope, extendScope } from "./scope";

function isEnums(enums: unknown): enums is Enums {
return (
typeof enums === "object" &&
enums != null &&
Object.values(enums).every((item) => Array.isArray(item) && item.every((element) => typeof element === "string"))
);
}

export type scopeWithEnums<enums, scope extends AbiTypeScope = AbiTypeScope> = Enums extends enums
? scope
: enums extends Enums
? extendScope<scope, { [key in keyof enums]: "uint8" }>
: scope;

export function scopeWithEnums<enums, scope extends AbiTypeScope = AbiTypeScope>(
enums: enums,
scope: scope = AbiTypeScope as scope,
): scopeWithEnums<enums, scope> {
if (isEnums(enums)) {
const enumScope = Object.fromEntries(Object.keys(enums).map((key) => [key, "uint8" as const]));
return extendScope(scope, enumScope) as scopeWithEnums<enums, scope>;
}
return scope as scopeWithEnums<enums, scope>;
}

export type resolveEnums<enums> = { readonly [key in keyof enums]: Readonly<enums[key]> };
23 changes: 23 additions & 0 deletions packages/store/ts/config/v2/generics.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { merge } from "@arktype/util";

export type get<input, key> = key extends keyof input ? input[key] : undefined;

export function get<input, key extends PropertyKey>(input: input, key: key): get<input, key> {
Expand All @@ -18,3 +20,24 @@ export function hasOwnKey<obj, const key extends PropertyKey>(
export function isObject<input>(input: input): input is input & object {
return input != null && typeof input === "object";
}

export type mergeIfUndefined<base, merged> = merge<
base,
{
[key in keyof merged]: key extends keyof base
? undefined extends base[key]
? merged[key]
: base[key]
: merged[key];
}
>;

export function mergeIfUndefined<base extends object, merged extends object>(
base: base,
merged: merged,
): mergeIfUndefined<base, merged> {
const allKeys = [...new Set([...Object.keys(base), ...Object.keys(merged)])];
return Object.fromEntries(
allKeys.map((key) => [key, base[key as keyof base] ?? merged[key as keyof merged]]),
) as mergeIfUndefined<base, merged>;
}
7 changes: 6 additions & 1 deletion packages/store/ts/config/v2/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,14 @@ export * from "./generics";
export * from "./scope";
export * from "./schema";
export * from "./tableShorthand";
export * from "./tableFull";
export * from "./storeWithShorthands";
export * from "./table";
export * from "./tables";
export * from "./store";
export * from "./input";
export * from "./output";
export * from "./defaults";
export * from "./compat";
export * from "./codegen";
export * from "./enums";
export * from "./userTypes";
43 changes: 43 additions & 0 deletions packages/store/ts/config/v2/input.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import { Hex } from "viem";
import { Codegen, Enums, TableCodegen, UserTypes } from "./output";
import { Scope } from "./scope";

export type SchemaInput = {
readonly [key: string]: string;
};

export type ScopedSchemaInput<scope extends Scope> = {
readonly [key: string]: keyof scope["types"];
};

export type TableInput = {
readonly schema: SchemaInput;
readonly key: readonly string[];
readonly tableId?: Hex;
readonly name: string;
readonly namespace?: string;
readonly type?: "table" | "offchainTable";
readonly codegen?: Partial<TableCodegen>;
};

export type TablesInput = {
readonly [key: string]: TableInput;
};

export type StoreInput = {
readonly namespace?: string;
readonly tables: TablesInput;
readonly userTypes?: UserTypes;
readonly enums?: Enums;
readonly codegen?: Partial<Codegen>;
};

/******** Variations with shorthands ********/

export type TableShorthandInput = SchemaInput | string;

export type TablesWithShorthandsInput = {
[key: string]: TableInput | TableShorthandInput;
};

export type StoreWithShorthandsInput = Omit<StoreInput, "tables"> & { tables: TablesWithShorthandsInput };
10 changes: 5 additions & 5 deletions packages/store/ts/config/v2/output.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ export type KeySchema = {
};
};

export type TableCodegenOptions = {
export type TableCodegen = {
readonly directory: string;
readonly tableIdArgument: boolean;
readonly storeArgument: boolean;
Expand All @@ -29,22 +29,22 @@ export type TableCodegenOptions = {
export type Table = BaseTable & {
readonly keySchema: KeySchema;
readonly valueSchema: Schema;
readonly codegen: TableCodegenOptions;
readonly codegen: TableCodegen;
};

export type CodegenOptions = {
export type Codegen = {
readonly storeImportPath: string;
readonly userTypesFilename: string;
readonly codegenDirectory: string;
readonly codegenIndexFilename: string;
};

export type Config = {
export type Store = {
readonly tables: {
readonly [namespacedTableName: string]: Table;
};
readonly userTypes: UserTypes;
readonly enums: Enums;
readonly namespace: string;
readonly codegen: CodegenOptions;
readonly codegen: Codegen;
};
8 changes: 4 additions & 4 deletions packages/store/ts/config/v2/schema.test.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
import { describe, it } from "vitest";
import { resolveSchema } from "./schema";
import { defineSchema } from "./schema";
import { Schema } from "./output";
import { extendScope, AbiTypeScope } from "./scope";
import { attest } from "@arktype/attest";

describe("resolveSchema", () => {
it("should map user types to their primitive type", () => {
const scope = extendScope(AbiTypeScope, { CustomType: "address" });
const resolved = resolveSchema({ regular: "uint256", user: "CustomType" }, scope);
const resolved = defineSchema({ regular: "uint256", user: "CustomType" }, scope);
const expected = {
regular: {
type: "uint256",
Expand All @@ -29,7 +29,7 @@ describe("resolveSchema", () => {
it("should throw if a type is not part of the scope", () => {
const scope = extendScope(AbiTypeScope, { CustomType: "address" });
attest(() =>
resolveSchema(
defineSchema(
{
regular: "uint256",
// @ts-expect-error Type '"NotACustomType"' is not assignable to type 'AbiType | "CustomType"'.
Expand All @@ -44,7 +44,7 @@ describe("resolveSchema", () => {

it("should extend the output Schema type", () => {
const scope = extendScope(AbiTypeScope, { CustomType: "address" });
const resolved = resolveSchema({ regular: "uint256", user: "CustomType" }, scope);
const resolved = defineSchema({ regular: "uint256", user: "CustomType" }, scope);
attest<true, typeof resolved extends Schema ? true : false>();
});
});
Loading

0 comments on commit f6b1473

Please sign in to comment.