Skip to content

Commit

Permalink
feat(cli): separate tableOptions, add renderPrototype, fix prototype …
Browse files Browse the repository at this point in the history
…config
  • Loading branch information
dk1a committed Mar 4, 2023
1 parent d179c52 commit a7f8d3c
Show file tree
Hide file tree
Showing 13 changed files with 376 additions and 30 deletions.
4 changes: 3 additions & 1 deletion packages/cli/src/config/loadStoreConfig.test-d.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { describe, expectTypeOf } from "vitest";
import { z } from "zod";
import { StoreConfig, StoreUserConfig } from "./loadStoreConfig.js";
import { PrototypeConfig, StoreConfig, StoreUserConfig } from "./loadStoreConfig.js";

describe("loadStoreConfig", () => {
// Typecheck manual interfaces against zod
Expand All @@ -10,5 +10,7 @@ describe("loadStoreConfig", () => {
expectTypeOf<NonNullable<NonNullable<StoreUserConfig["userTypes"]>["enums"]>[string]>().toEqualTypeOf<
NonNullable<NonNullable<z.input<typeof StoreConfig>["userTypes"]>["enums"]>[string]
>();
expectTypeOf<PrototypeConfig>().toEqualTypeOf<z.input<typeof PrototypeConfig>>();
expectTypeOf<PrototypeConfig["tables"][string]>().toEqualTypeOf<z.input<typeof PrototypeConfig>["tables"][string]>();
// TODO If more nested schemas are added, provide separate tests for them
});
80 changes: 78 additions & 2 deletions packages/cli/src/config/loadStoreConfig.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ const TableName = ObjectName;
const KeyName = ValueName;
const ColumnName = ValueName;
const UserEnumName = ObjectName;
const PrototypeName = ObjectName;

// Fields can use SchemaType or one of user defined wrapper types
const FieldData = z.union([z.nativeEnum(SchemaType), UserEnumName]);
Expand Down Expand Up @@ -60,6 +61,16 @@ const TablesRecord = z.record(TableName, z.union([TableDataShorthand, TableDataF
return tables as Record<string, RequiredKeys<typeof tables[string], "route">>;
});

export const PrototypeConfig = z.object({
directory: OrdinaryRoute.default("/prototypes"),
tables: z.record(
TableName,
z.object({
default: z.union([z.string(), z.record(ColumnName, z.string())]).optional(),
})
),
});

const StoreConfigUnrefined = z.object({
baseRoute: BaseRoute.default(""),
storeImportPath: z.string().default("@latticexyz/store/src/"),
Expand All @@ -70,6 +81,7 @@ const StoreConfigUnrefined = z.object({
enums: z.record(UserEnumName, UserEnum).default({}),
})
.default({}),
prototypes: z.record(PrototypeName, PrototypeConfig).default({}),
});
// finally validate global conditions
export const StoreConfig = StoreConfigUnrefined.superRefine(validateStoreConfig);
Expand All @@ -92,6 +104,12 @@ export interface StoreUserConfig {
tables: Record<string, z.input<typeof FieldData> | FullTableConfig>;
/** User-defined types that will be generated and may be used in table schemas instead of `SchemaType` */
userTypes?: UserTypesConfig;
/**
* Configuration for each prototype - a collection of tables that share primary keys.
*
* The key is the prototype name (capitalized). The value is PrototypeConfig.
*/
prototypes?: Record<string, PrototypeConfig>;
}

interface FullTableConfig {
Expand All @@ -118,6 +136,24 @@ interface UserTypesConfig {
enums?: Record<string, string[]>;
}

// note that prototype is an array of objects
export interface PrototypeConfig {
/** Output directory path for the file. Default is "/prototypes" */
directory?: string;
/** Table names used in this prototype, mapped to their options */
tables: Record<
string,
{
/**
* Default value is either a string for structs, or a record of strings for each key
*
* The string is rendered in solidity as-is, unescaped and without adding quotes
*/
default?: string | Record<z.input<typeof ColumnName>, string>;
}
>;
}

export type StoreConfig = z.output<typeof StoreConfig>;

export async function loadStoreConfig(configPath?: string) {
Expand Down Expand Up @@ -155,12 +191,13 @@ function validateStoreConfig(config: z.output<typeof StoreConfigUnrefined>, ctx:
// Global names must be unique
const tableNames = Object.keys(config.tables);
const userTypeNames = Object.keys(config.userTypes.enums);
const globalNames = [...tableNames];
const prototypeNames = Object.keys(config.prototypes);
const globalNames = [...tableNames, ...userTypeNames, ...prototypeNames];
const duplicateGlobalNames = getDuplicates(globalNames);
if (duplicateGlobalNames.length > 0) {
ctx.addIssue({
code: ZodIssueCode.custom,
message: `Table and enum names must be globally unique: ${duplicateGlobalNames.join(", ")}`,
message: `Table, enum, prototype names must be globally unique: ${duplicateGlobalNames.join(", ")}`,
});
}
// User types must exist
Expand All @@ -172,6 +209,41 @@ function validateStoreConfig(config: z.output<typeof StoreConfigUnrefined>, ctx:
validateIfUserType(userTypeNames, fieldType, ctx);
}
}
// Prototypes must use valid tables which use the same primary key types
for (const prototypeName of Object.keys(config.prototypes)) {
const prototype = config.prototypes[prototypeName];
if (Object.keys(prototype.tables).length === 0) {
ctx.addIssue({
code: ZodIssueCode.custom,
message: `Prototype "${prototypeName}" must not be empty`,
});
}

let primaryKeys, firstTableName;
for (const prototypeTableName of Object.keys(prototype.tables)) {
if (!tableNames.includes(prototypeTableName)) {
// check table's existance
ctx.addIssue({
code: ZodIssueCode.custom,
message: `Prototype "${prototypeName}" uses an invalid table "${prototypeTableName}"`,
});
} else {
// check primary keys
const tablePrimaryKeys = config.tables[prototypeTableName].primaryKeys;
if (primaryKeys === undefined) {
primaryKeys = tablePrimaryKeys;
firstTableName = prototypeTableName;
} else if (!arraysShallowEqual(Object.values(primaryKeys), Object.values(tablePrimaryKeys))) {
ctx.addIssue({
code: ZodIssueCode.custom,
message:
`Prototype "${prototypeName}": different types of primary keys` +
` for tables "${prototypeTableName}" and "${firstTableName}"`,
});
}
}
}
}
}

function validateIfUserType(
Expand All @@ -187,4 +259,8 @@ function validateIfUserType(
}
}

function arraysShallowEqual<T>(array1: T[], array2: T[]) {
return array1.length === array2.length && array1.every((value, index) => value === array2[index]);
}

type RequiredKeys<T extends Record<string, unknown>, P extends string> = T & Required<Pick<T, P>>;
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 @@
import { loadStoreConfig, parseStoreConfig } from "./config/loadStoreConfig.js";
import { renderTablesFromConfig } from "./render-solidity/renderTablesFromConfig.js";
import { getAllTableOptions } from "./render-solidity/tableOptions.js";
import { renderTable } from "./render-solidity/renderTable.js";

export type { StoreUserConfig, StoreConfig } from "./config/loadStoreConfig.js";

export { loadStoreConfig, parseStoreConfig, renderTablesFromConfig, renderTable };
export { loadStoreConfig, parseStoreConfig, getAllTableOptions, renderTable };
19 changes: 17 additions & 2 deletions packages/cli/src/render-solidity/common.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { ImportDatum, RenderTableOptions, RenderTableType } from "./types.js";
import { ImportDatum, RenderTableOptions, RenderTableType, StaticRouteData } from "./types.js";

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

export function renderCommonData({ staticRouteData, primaryKeys }: RenderTableOptions) {
export function renderCommonData({
staticRouteData,
primaryKeys,
}: Pick<RenderTableOptions, "staticRouteData" | "primaryKeys">) {
// static route means static tableId as well, and no tableId arguments
const _tableId = staticRouteData ? "" : "_tableId";
const _typedTableId = staticRouteData ? "" : "uint256 _tableId";
Expand Down Expand Up @@ -69,6 +72,18 @@ export function renderImports(imports: ImportDatum[]) {
return renderedImports.join("\n");
}

export function renderTableId(staticRouteData: StaticRouteData) {
const hardcodedTableId = `uint256(keccak256("${staticRouteData.baseRoute + staticRouteData.subRoute}"))`;
const tableIdDefinition = `
uint256 constant _tableId = ${hardcodedTableId};
uint256 constant ${staticRouteData.tableIdName} = _tableId;
`;
return {
hardcodedTableId,
tableIdDefinition,
};
}

function renderValueTypeToBytes32(name: string, { staticByteLength, typeUnwrap, internalTypeId }: RenderTableType) {
const bits = staticByteLength * 8;
const innerText = `${typeUnwrap}(${name})`;
Expand Down
2 changes: 1 addition & 1 deletion packages/cli/src/render-solidity/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,5 +2,5 @@ export * from "./common.js";
export * from "./field.js";
export * from "./record.js";
export * from "./renderTable.js";
export * from "./renderTablesFromConfig.js";
export * from "./tableOptions.js";
export * from "./types.js";
93 changes: 93 additions & 0 deletions packages/cli/src/render-solidity/prototypeOptions.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
import path from "path";
import { StoreConfig } from "../config/loadStoreConfig.js";
import { ImportDatum, RenderPrototypeOptions, RenderTableForPrototype } from "./types.js";
import { TableOptions } from "./tableOptions.js";

export interface PrototypeOptions {
outputDirectory: string;
prototypeName: string;
renderOptions: RenderPrototypeOptions;
}

export function getAllPrototypeOptions(
config: StoreConfig,
allTablesOptions: TableOptions[],
srcDirectory: string
): PrototypeOptions[] {
const options = [];
for (const prototypeName of Object.keys(config.prototypes)) {
const prototypeData = config.prototypes[prototypeName];
const outputDirectory = path.join(srcDirectory, prototypeData.directory);

const tablesOptions = allTablesOptions.filter(({ tableName }) =>
Object.keys(prototypeData.tables).includes(tableName)
);
const primaryKeys = tablesOptions[0].renderOptions.primaryKeys;

// list of any symbols that need to be imported
const imports: ImportDatum[] = [];

const tableConfigs = Object.keys(prototypeData.tables).map((tableName): RenderTableForPrototype => {
const { default: tableDefault } = prototypeData.tables[tableName];
const tableOptions = tablesOptions.find((val) => val.tableName === tableName);
if (tableOptions === undefined) throw new Error(`No render options found for table ${tableName}`);

const {
libraryName: tableLibraryName,
structName,
imports: tableImports,
staticRouteData,
} = tableOptions.renderOptions;

const importTableRelativePath =
"./" + path.relative(outputDirectory, path.join(tableOptions.outputDirectory, tableOptions.tableName)) + ".sol";
imports.push({
symbol: tableLibraryName,
path: importTableRelativePath,
pathFromSrc: tableOptions.outputDirectory,
});
if (structName !== undefined) {
imports.push({
symbol: tableLibraryName,
path: importTableRelativePath,
pathFromSrc: tableOptions.outputDirectory,
});
}
for (const importDatum of tableImports) {
imports.push({
...importDatum,
path: "./" + path.relative(outputDirectory, importDatum.pathFromSrc) + ".sol",
});
}

const fields = tableOptions.renderOptions.fields.map((field) => {
return {
...field,
default: typeof tableDefault === "object" ? tableDefault[field.name] : undefined,
};
});

if (staticRouteData === undefined) throw new Error("Prototypes with table id arguments are not supported");

return {
libraryName: tableLibraryName,
structName,
staticRouteData,
fields,
default: typeof structName !== "undefined" && typeof tableDefault === "string" ? tableDefault : undefined,
};
});

options.push({
outputDirectory,
prototypeName,
renderOptions: {
imports,
libraryName: prototypeName,
primaryKeys,
tables: tableConfigs,
},
});
}
return options;
}
Loading

0 comments on commit a7f8d3c

Please sign in to comment.