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(cli): improve storeArgument, refactor cli #500

Merged
merged 5 commits into from
Mar 16, 2023
Merged
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
4 changes: 2 additions & 2 deletions packages/cli/contracts/src/tables/Table1.sol
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ import { Schema, SchemaLib } from "@latticexyz/store/src/Schema.sol";
import { PackedCounter, PackedCounterLib } from "@latticexyz/store/src/PackedCounter.sol";

// Import user types
import { Enum1, Enum2 } from "./../types.sol";
import { Enum1, Enum2 } from "./../Types.sol";

uint256 constant _tableId = uint256(bytes32(abi.encodePacked(bytes16(""), bytes16("Table1"))));
uint256 constant Table1TableId = _tableId;
Expand Down Expand Up @@ -437,7 +437,7 @@ library Table1 {
bool v5,
Enum1 v6,
Enum2 v7
) internal returns (bytes memory) {
) internal pure returns (bytes memory) {
return abi.encodePacked(v1, v2, v3, v4, v5, v6, v7);
}

Expand Down
2 changes: 1 addition & 1 deletion packages/cli/contracts/test/Tablegen.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ pragma solidity >=0.8.0;
import "forge-std/Test.sol";
import { StoreView } from "@latticexyz/store/src/StoreView.sol";
import { Table1, Table1Data } from "../src/tables/Table1.sol";
import { Enum1, Enum2 } from "../src/types.sol";
import { Enum1, Enum2 } from "../src/Types.sol";

contract TablegenTest is Test, StoreView {
function testTable1SetAndGet() public {
Expand Down
6 changes: 3 additions & 3 deletions packages/cli/src/config/parseStoreConfig.test-d.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
import { describe, expectTypeOf } from "vitest";
import { z } from "zod";
import { StoreConfig, StoreUserConfig } from "./parseStoreConfig.js";
import { StoreConfig, StoreUserConfig, UserTypesConfig } from "./parseStoreConfig.js";

describe("StoreUserConfig", () => {
// Typecheck manual interfaces against zod
expectTypeOf<StoreUserConfig>().toEqualTypeOf<z.input<typeof StoreConfig>>();
// type equality isn't deep for optionals
expectTypeOf<StoreUserConfig["tables"][string]>().toEqualTypeOf<z.input<typeof StoreConfig>["tables"][string]>();
expectTypeOf<NonNullable<NonNullable<StoreUserConfig["userTypes"]>["enums"]>[string]>().toEqualTypeOf<
NonNullable<NonNullable<z.input<typeof StoreConfig>["userTypes"]>["enums"]>[string]
expectTypeOf<NonNullable<UserTypesConfig["enums"]>[string]>().toEqualTypeOf<
NonNullable<NonNullable<UserTypesConfig>["enums"]>[string]
>();
// TODO If more nested schemas are added, provide separate tests for them
});
157 changes: 106 additions & 51 deletions packages/cli/src/config/parseStoreConfig.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { SchemaType } from "@latticexyz/schema-type";
import { RefinementCtx, z, ZodIssueCode } from "zod";
import { ObjectName, OrdinaryRoute, Selector, StaticSchemaType, UserEnum, ValueName } from "./commonSchemas.js";
import { ObjectName, Selector, StaticSchemaType, UserEnum, ValueName } from "./commonSchemas.js";
import { getDuplicates } from "./validation.js";

const TableName = ObjectName;
Expand All @@ -15,18 +15,63 @@ const FieldData = z.union([z.nativeEnum(SchemaType), UserEnumName]);
const PrimaryKey = z.union([StaticSchemaType, UserEnumName]);
const PrimaryKeys = z.record(KeyName, PrimaryKey).default({ key: SchemaType.BYTES32 });

const Schema = z
/************************************************************************
*
* TABLE SCHEMA
*
************************************************************************/

export type FullSchemaConfig = Record<string, z.input<typeof FieldData>>;
export type ShorthandSchemaConfig = z.input<typeof FieldData>;
export type SchemaConfig = FullSchemaConfig | ShorthandSchemaConfig;

const FullSchemaConfig = z
.record(ColumnName, FieldData)
.refine((arg) => Object.keys(arg).length > 0, "Table schema may not be empty");

const TableDataFull = z
const ShorthandSchemaConfig = FieldData.transform((fieldData) => {
return FullSchemaConfig.parse({
value: fieldData,
});
});

export const SchemaConfig = FullSchemaConfig.or(ShorthandSchemaConfig);

/************************************************************************
*
* TABLE
*
************************************************************************/

export interface TableConfig {
/** Output directory path for the file. Default is "tables" */
directory?: string;
/**
* The fileSelector is used with the namespace to register the table and construct its id.
* The table id will be uint256(bytes32(abi.encodePacked(bytes16(namespace), bytes16(fileSelector)))).
* Default is "<tableName>"
* */
fileSelector?: string;
/** Make methods accept `tableId` argument instead of it being a hardcoded constant. Default is false */
tableIdArgument?: boolean;
/** Include methods that accept a manual `IStore` argument. Default is false. */
storeArgument?: boolean;
/** Include a data struct and methods for it. Default is false for 1-column tables; true for multi-column tables. */
dataStruct?: boolean;
/** Table's primary key names mapped to their types. Default is `{ key: SchemaType.BYTES32 }` */
primaryKeys?: Record<string, z.input<typeof PrimaryKey>>;
/** Table's column names mapped to their types. Table name's 1st letter should be lowercase. */
schema: SchemaConfig;
}

const FullTableConfig = z
.object({
directory: OrdinaryRoute.default("/tables"),
directory: z.string().default("tables"),
fileSelector: Selector.optional(),
tableIdArgument: z.boolean().default(false),
storeArgument: z.boolean().default(false),
primaryKeys: PrimaryKeys,
schema: Schema,
schema: SchemaConfig,
dataStruct: z.boolean().optional(),
})
.transform((arg) => {
Expand All @@ -39,15 +84,25 @@ const TableDataFull = z
return arg as RequireKeys<typeof arg, "dataStruct">;
});

const TableDataShorthand = FieldData.transform((fieldData) => {
return TableDataFull.parse({
const ShorthandTableConfig = FieldData.transform((fieldData) => {
return FullTableConfig.parse({
schema: {
value: fieldData,
},
});
});

const TablesRecord = z.record(TableName, z.union([TableDataShorthand, TableDataFull])).transform((tables) => {
export const TableConfig = FullTableConfig.or(ShorthandTableConfig);

/************************************************************************
*
* TABLES
*
************************************************************************/

export type TablesConfig = Record<string, TableConfig | z.input<typeof FieldData>>;

export const TablesConfig = z.record(TableName, TableConfig).transform((tables) => {
// default fileSelector depends on tableName
for (const tableName of Object.keys(tables)) {
const table = tables[tableName];
Expand All @@ -58,19 +113,31 @@ const TablesRecord = z.record(TableName, z.union([TableDataShorthand, TableDataF
return tables as Record<string, RequireKeys<(typeof tables)[string], "fileSelector">>;
});

const StoreConfigUnrefined = z.object({
namespace: Selector.default(""),
storeImportPath: z.string().default("@latticexyz/store/src/"),
tables: TablesRecord,
userTypes: z
.object({
path: OrdinaryRoute.default("/types"),
enums: z.record(UserEnumName, UserEnum).default({}),
})
.default({}),
});
// finally validate global conditions
export const StoreConfig = StoreConfigUnrefined.superRefine(validateStoreConfig);
/************************************************************************
*
* USER TYPES
*
************************************************************************/

export interface UserTypesConfig<Enums extends Record<string, string[]> = Record<string, string[]>> {
/** Path to the file where common types will be generated and imported from. Default is "Types" */
path?: string;
/** Enum names mapped to lists of their member names */
enums?: Enums;
}

export const UserTypesConfig = z
.object({
path: z.string().default("Types"),
enums: z.record(UserEnumName, UserEnum).default({}),
})
.default({});

/************************************************************************
*
* FINAL
*
************************************************************************/

// zod doesn't preserve doc comments
export interface StoreUserConfig {
Expand All @@ -87,45 +154,33 @@ export interface StoreUserConfig {
* - `SchemaType | userType` for a single-value table (aka ECS component).
* - FullTableConfig object for multi-value tables (or for customizable options).
*/
tables: Record<string, z.input<typeof FieldData> | FullTableConfig>;
tables: TablesConfig;
/** User-defined types that will be generated and may be used in table schemas instead of `SchemaType` */
userTypes?: UserTypesConfig;
}

export interface FullTableConfig {
/** Output directory path for the file. Default is "/tables" */
directory?: string;
/**
* The fileSelector is used with the namespace to register the table and construct its id.
* The table id will be uint256(bytes32(abi.encodePacked(bytes16(namespace), bytes16(fileSelector)))).
* Default is "<tableName>"
* */
fileSelector?: string;
/** Make methods accept `tableId` argument instead of it being a hardcoded constant. Default is false */
tableIdArgument?: boolean;
/** Include methods that accept a manual `IStore` argument. Default is false. */
storeArgument?: boolean;
/** Include a data struct and methods for it. Default is false for 1-column tables; true for multi-column tables. */
dataStruct?: boolean;
/** Table's primary key names mapped to their types. Default is `{ key: SchemaType.BYTES32 }` */
primaryKeys?: Record<string, z.input<typeof PrimaryKey>>;
/** Table's column names mapped to their types. Table name's 1st letter should be lowercase. */
schema: Record<string, z.input<typeof FieldData>>;
}
export type StoreConfig = z.output<typeof StoreConfig>;

interface UserTypesConfig<Enums extends Record<string, string[]> = Record<string, string[]>> {
/** Path to the file where common types will be generated and imported from. Default is "/types" */
path?: string;
/** Enum names mapped to lists of their member names */
enums?: Enums;
}
const StoreConfigUnrefined = z.object({
namespace: Selector.default(""),
storeImportPath: z.string().default("@latticexyz/store/src/"),
tables: TablesConfig,
userTypes: UserTypesConfig,
});

export type StoreConfig = z.output<typeof StoreConfig>;
// finally validate global conditions
export const StoreConfig = StoreConfigUnrefined.superRefine(validateStoreConfig);

export function parseStoreConfig(config: StoreUserConfig) {
export function parseStoreConfig(config: unknown) {
Copy link
Member

@holic holic Mar 17, 2023

Choose a reason for hiding this comment

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

Did this have to get reverted?
Nevermind, I don't think this impacted downstream types like I thought it would!

Copy link
Contributor Author

@dk1a dk1a Mar 17, 2023

Choose a reason for hiding this comment

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

this was an accidental revert actually - was adapting these changes from code before your PR and missed this. But good to know all works well

return StoreConfig.parse(config);
}

/************************************************************************
*
* HELPERS
*
************************************************************************/

// Validate conditions that check multiple different config options simultaneously
function validateStoreConfig(config: z.output<typeof StoreConfigUnrefined>, ctx: RefinementCtx) {
// Local table variables must be unique within the table
Expand All @@ -148,7 +203,7 @@ function validateStoreConfig(config: z.output<typeof StoreConfigUnrefined>, ctx:
if (duplicateGlobalNames.length > 0) {
ctx.addIssue({
code: ZodIssueCode.custom,
message: `Table and enum names must be globally unique: ${duplicateGlobalNames.join(", ")}`,
message: `Table, enum names must be globally unique: ${duplicateGlobalNames.join(", ")}`,
});
}
// User types must exist
Expand Down
4 changes: 2 additions & 2 deletions packages/cli/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
export { parseStoreConfig } from "./config/parseStoreConfig.js";
export { loadStoreConfig } from "./config/loadStoreConfig.js";
export { parseStoreConfig } from "./config/parseStoreConfig.js";
export { loadWorldConfig, resolveWorldConfig, parseWorldConfig } from "./config/world/index.js";
export { renderTablesFromConfig } from "./render-solidity/renderTablesFromConfig.js";
export { getTableOptions } from "./render-solidity/tableOptions.js";
export { renderTable } from "./render-solidity/renderTable.js";
export { resolveTableId } from "./config/dynamicResolution.js";

Expand Down
53 changes: 49 additions & 4 deletions packages/cli/src/render-solidity/common.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { ImportDatum, RenderTableOptions, RenderTableType } from "./types.js";
import path from "path";
import { ImportDatum, RenderTableOptions, RenderTableType, StaticResourceData } from "./types.js";

export const renderedSolidityHeader = `// SPDX-License-Identifier: MIT
pragma solidity >=0.8.0;
Expand All @@ -20,8 +21,11 @@ export function renderArguments(args: (string | undefined)[]) {
return internalRenderList(",", filteredArgs, (arg) => arg);
}

export function renderCommonData({ staticResourceData, primaryKeys }: RenderTableOptions) {
// static route means static tableId as well, and no tableId arguments
export function renderCommonData({
staticResourceData,
primaryKeys,
}: Pick<RenderTableOptions, "staticResourceData" | "primaryKeys">) {
// static resource means static tableId as well, and no tableId arguments
const _tableId = staticResourceData ? "" : "_tableId";
const _typedTableId = staticResourceData ? "" : "uint256 _tableId";

Expand All @@ -45,14 +49,23 @@ export function renderCommonData({ staticResourceData, primaryKeys }: RenderTabl
};
}

/** For 2 paths which are relative to a common root, create a relative import path from one to another */
export function solidityRelativeImportPath(fromPath: string, usedInPath: string) {
// 1st "./" must be added because path strips it,
// but solidity expects it unless there's "../" ("./../" is fine).
// 2nd and 3rd "./" forcefully avoid absolute paths (everything is relative to `src`).
return "./" + path.relative("./" + usedInPath, "./" + fromPath);
}

/**
* Aggregates, deduplicates and renders imports for symbols per path.
* Identical symbols from different paths are NOT handled, they should be checked before rendering.
*/
export function renderImports(imports: ImportDatum[]) {
// Aggregate symbols by import path, also deduplicating them
const aggregatedImports = new Map<string, Set<string>>();
for (const { symbol, path } of imports) {
for (const { symbol, fromPath, usedInPath } of imports) {
const path = solidityRelativeImportPath(fromPath, usedInPath);
if (!aggregatedImports.has(path)) {
aggregatedImports.set(path, new Set());
}
Expand All @@ -67,6 +80,38 @@ export function renderImports(imports: ImportDatum[]) {
return renderedImports.join("\n");
}

export function renderWithStore(
storeArgument: boolean,
callback: (
_typedStore: string | undefined,
_store: string,
_commentSuffix: string,
_untypedStore: string | undefined
) => string
) {
let result = "";
result += callback(undefined, "StoreSwitch", "", undefined);

if (storeArgument) {
result += "\n" + callback("IStore _store", "_store", " (using the specified store)", "_store");
}

return result;
}

export function renderTableId(staticResourceData: StaticResourceData) {
const hardcodedTableId = `uint256(bytes32(abi.encodePacked(bytes16("${staticResourceData.namespace}"), bytes16("${staticResourceData.fileSelector}"))))`;

const tableIdDefinition = `
uint256 constant _tableId = ${hardcodedTableId};
uint256 constant ${staticResourceData.tableIdName} = _tableId;
`;
return {
hardcodedTableId,
tableIdDefinition,
};
}

function renderValueTypeToBytes32(name: string, { staticByteLength, typeUnwrap, internalTypeId }: RenderTableType) {
const bits = staticByteLength * 8;
const innerText = `${typeUnwrap}(${name})`;
Expand Down
Loading