diff --git a/.vscode/extensions.json b/.vscode/extensions.json new file mode 100644 index 0000000..2b16bbc --- /dev/null +++ b/.vscode/extensions.json @@ -0,0 +1,3 @@ +{ + "recommendations": ["biomejs.biome", "dbaeumer.vscode-eslint"] +} diff --git a/.vscode/settings.json b/.vscode/settings.json index 44f3c3d..dd1f487 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -44,5 +44,6 @@ "javascript.preferences.quoteStyle": "double", "typescript.format.semicolons": "insert", "typescript.preferences.quoteStyle": "double", - "typescript.enablePromptUseWorkspaceTsdk": true + "typescript.enablePromptUseWorkspaceTsdk": true, + "typescript.tsdk": "node_modules/typescript/lib" } diff --git a/biome.jsonc b/biome.jsonc index 16fd232..400312c 100644 --- a/biome.jsonc +++ b/biome.jsonc @@ -33,7 +33,7 @@ }, "javascript": { "formatter": { - "trailingCommas": "none" + "trailingCommas": "es5" } }, "json": { diff --git a/package.json b/package.json index 306fee2..638818e 100644 --- a/package.json +++ b/package.json @@ -19,8 +19,8 @@ "husky": "^9.1.6", "knip": "^5.29.2", "turbo": "^2.2.1", - "typescript": "^5.4.5", - "vitest": "^2.0.5" + "typescript": "^5.7", + "vitest": "^3.0.0" }, "packageManager": "pnpm@8.15.6", "engines": { diff --git a/packages/app-connector/src/connection_state.ts b/packages/app-connector/src/connection_state.ts index e785564..cad8145 100644 --- a/packages/app-connector/src/connection_state.ts +++ b/packages/app-connector/src/connection_state.ts @@ -4,7 +4,7 @@ export type ClientConnectionInfo = { }; export enum ClientConnectionErrorType { - PERMISSION_DENIED = "PERMISSION_DENIED" + PERMISSION_DENIED = "PERMISSION_DENIED", } export type ClientConnectionError = { diff --git a/packages/eslint-config/eslint.base.config.mjs b/packages/eslint-config/eslint.base.config.mjs index 202fff0..bec5d64 100644 --- a/packages/eslint-config/eslint.base.config.mjs +++ b/packages/eslint-config/eslint.base.config.mjs @@ -12,41 +12,44 @@ export default tseslint.config( { ignores: [ + "**/src/generated/**", "**/node_modules/*", "**/dist/", "**/vitest.config.ts", "**/vite.config.ts", - "**/tailwind.config.ts" - ] // global ignore with single ignore key + "**/vitest.workspace.ts", + "**/tailwind.config.ts", + "**/tsup.config.ts", + ], // global ignore with single ignore key }, { languageOptions: { parserOptions: { - projectService: true - } - } + projectService: true, + }, + }, }, { files: ["**/*.js", "**/*.mjs"], - extends: [tseslint.configs.disableTypeChecked] + extends: [tseslint.configs.disableTypeChecked], }, { plugins: { "chai-friendly": pluginChaiFriendly, "react-hooks": hooksPlugin, turbo: eslintPluginTurbo, - import: eslintPluginImport + import: eslintPluginImport, }, rules: { ...hooksPlugin.configs.recommended.rules, - "turbo/no-undeclared-env-vars": "error" - } + "turbo/no-undeclared-env-vars": "error", + }, }, { rules: { - "import/extensions": ["error", "always"] + "import/extensions": ["error", "always"], }, - files: ["./packages/**"] + files: ["./packages/**"], }, { rules: { @@ -60,8 +63,8 @@ export default tseslint.config( argsIgnorePattern: "^_", varsIgnorePattern: "^_", destructuredArrayIgnorePattern: "^_", - caughtErrorsIgnorePattern: "^_" - } + caughtErrorsIgnorePattern: "^_", + }, ], "no-case-declarations": "off", "react-hooks/rules-of-hooks": "error", @@ -72,7 +75,29 @@ export default tseslint.config( "@typescript-eslint/consistent-type-imports": "error", "@typescript-eslint/no-import-type-side-effects": "error", "no-unexpected-multiline": "off", - "no-restricted-globals": ["error", "origin"] - } + "no-restricted-globals": ["error", "origin"], + "no-restricted-syntax": [ + "error", + { + selector: "BinaryExpression[operator='in']", + message: + "Don't use in operator. Use Object.prototype.hasOwnProperty.call instead.", + }, + { + selector: + 'Property:matches([kind = "get"], [kind = "set"]), MethodDefinition:matches([kind = "get"], [kind = "set"])', + message: "Don't use get and set accessors.", + }, + { + selector: "ForInStatement", + message: "Don't use for-in loop.", + }, + // Ban static `this`: + { + selector: "MethodDefinition[static = true] ThisExpression", + message: "Prefer using the class's name directly.", + }, + ], + }, } ); diff --git a/packages/podspec/package.json b/packages/podspec/package.json index 693f1fb..9580a63 100644 --- a/packages/podspec/package.json +++ b/packages/podspec/package.json @@ -1,6 +1,6 @@ { "name": "@parcnet-js/podspec", - "version": "1.2.0", + "version": "2.0.0-alpha.1", "license": "GPL-3.0-or-later", "main": "dist/index.cjs", "module": "dist/index.js", @@ -30,16 +30,20 @@ }, "scripts": { "lint": "eslint . --max-warnings 0", - "build": "tsup 'src/**/*@(ts|tsx)' --format cjs,esm --clean --sourcemap --no-splitting", - "prepublish": "tsup 'src/**/*@(ts|tsx)' --format cjs,esm --clean --sourcemap --dts --no-splitting", + "build": "tsup", + "prepublish": "tsup --dts", "test": "vitest run --typecheck" }, "files": ["dist", "./README.md", "./LICENSE"], "dependencies": { "@pcd/gpc": "^0.4.0", - "@pcd/pod": "^0.5.0" + "@pcd/pod": "^0.5.0", + "base64-js": "^1.5.1", + "canonicalize": "^2.0.0", + "typia": "^7.6.0" }, "devDependencies": { + "@fast-check/vitest": "^0.1.5", "@parcnet-js/eslint-config": "workspace:*", "@parcnet-js/typescript-config": "workspace:*", "@pcd/proto-pod-gpc-artifacts": "^0.11.0", @@ -49,7 +53,7 @@ "tsup": "^8.2.4", "typescript": "^5.5", "uuid": "^9.0.0", - "vitest": "^2.1.1" + "vitest": "^3.0.5" }, "publishConfig": { "access": "public", diff --git a/packages/podspec/src/audit/podSpec.ts b/packages/podspec/src/audit/podSpec.ts new file mode 100644 index 0000000..6f85af8 --- /dev/null +++ b/packages/podspec/src/audit/podSpec.ts @@ -0,0 +1,24 @@ +import type { PODSpec } from "../builders/pod.js"; +import type { EntryTypes } from "../builders/types/entries.js"; +import type { StatementMap } from "../builders/types/statements.js"; +import { assertPODSpec } from "../generated/podspec.js"; + +export function auditPODSpec( + spec: PODSpec +): void { + assertPODSpec(spec); + + for (const [key, statement] of Object.entries(spec.statements)) { + switch (statement.type) { + case "isMemberOf": + // TODO: check that statements are valid + break; + case "isNotMemberOf": + // TODO: check that statements are valid + break; + default: + // prettier-ignore + statement.type satisfies never; + } + } +} diff --git a/packages/podspec/src/builders/group.ts b/packages/podspec/src/builders/group.ts new file mode 100644 index 0000000..ec6206e --- /dev/null +++ b/packages/podspec/src/builders/group.ts @@ -0,0 +1,426 @@ +import type { PODName } from "@pcd/pod"; +import type { IsSingleLiteralString } from "../shared/types.js"; +import { type PODSpec, PODSpecBuilder } from "./pod.js"; +import type { + EntriesOfType, + EntryKeys, + EntryTypes, + PODValueTupleForNamedEntries, + PODValueType, + PODValueTypeFromTypeName, + VirtualEntries, +} from "./types/entries.js"; +import type { + EntriesWithRangeChecks, + EqualsEntry, + GreaterThan, + GreaterThanEq, + InRange, + IsMemberOf, + IsNotMemberOf, + LessThan, + LessThanEq, + NotEqualsEntry, + NotInRange, + StatementMap, + StatementName, + SupportsRangeChecks, +} from "./types/statements.js"; +import { UntypedPODGroupSpecBuilder } from "./untypedGroup.js"; + +export type NamedPODSpecs = Record>; + +/** + @todo + - [ ] Maybe collapse the POD entries structure into a single object, rather + than nested PODs? Might improve reuse with PODSpecBuilder and make typing + easier. + */ + +export type PODGroupSpec

= { + pods: P; + statements: S; +}; + +export type AllPODEntries

= Evaluate< + UnionToIntersection< + { + [K in keyof P]: { + [E in keyof (P[K]["entries"] & VirtualEntries) as `${K & string}.${E & + string}`]: (P[K]["entries"] & VirtualEntries)[E]; + }; + }[keyof P] extends infer O + ? { [K in keyof O]: O[K] } + : never + > +>; + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +type UnionToIntersection = (U extends any ? (x: U) => void : never) extends ( + x: infer I +) => void + ? I + : never; + +type MustBePODValueType = T extends PODValueType ? T : never; + +type EntryType< + P extends NamedPODSpecs, + K extends keyof AllPODEntries

, +> = MustBePODValueType[K]>; + +type Evaluate = T extends infer O ? { [K in keyof O]: O[K] } : never; + +type AddPOD< + PODs extends NamedPODSpecs, + N extends PODName, + Spec extends PODSpec, +> = Evaluate<{ + [K in keyof PODs | N]: K extends N ? Spec : PODs[K & keyof PODs]; +}>; + +// TODO it's possible to create a PODGroupSpecBuilder with no PODs initially, +// and this causes some issues for typing, because we can't assume that there +// will be any PODs. The create/constructor should require at least one named +// POD, which will ensure there is always one POD. Any attempt to remove the +// final POD should fail. +// Once fixed, we can add some extra type exclusions around statements which +// refer to multiple POD entries, where the second entry cannot be the same as +// the first. +export class PODGroupSpecBuilder< + P extends NamedPODSpecs, + S extends StatementMap, +> { + readonly #innerBuilder: UntypedPODGroupSpecBuilder; + + private constructor(innerBuilder: UntypedPODGroupSpecBuilder) { + this.#innerBuilder = innerBuilder; + } + + // eslint-disable-next-line @typescript-eslint/no-empty-object-type + public static create(): PODGroupSpecBuilder<{}, {}> { + return new PODGroupSpecBuilder(UntypedPODGroupSpecBuilder.create()); + } + + public spec(): PODGroupSpec { + return this.#innerBuilder.spec() as PODGroupSpec; + } + + public pod< + N extends PODName, + Spec extends PODSpec, + NewPods extends AddPOD, + >( + name: IsSingleLiteralString extends true ? N : never, + spec: Spec + ): PODGroupSpecBuilder { + return new PODGroupSpecBuilder(this.#innerBuilder.pod(name, spec)); + } + + public isMemberOf>, C extends string>( + names: [...N], + values: N["length"] extends 1 + ? PODValueTypeFromTypeName>>[] + : PODValueTupleForNamedEntries, N>[], + customStatementName?: C + ): PODGroupSpecBuilder< + P, + S & { + [K in IsSingleLiteralString extends true + ? C + : StatementName]: IsMemberOf, N>; + } + > { + return new PODGroupSpecBuilder( + this.#innerBuilder.isMemberOf(names, values, customStatementName) + ); + } + + public isNotMemberOf>, C extends string>( + names: [...N], + values: N["length"] extends 1 + ? PODValueTypeFromTypeName>>[] + : PODValueTupleForNamedEntries, N>[], + customStatementName?: C + ): PODGroupSpecBuilder< + P, + S & { + [K in IsSingleLiteralString extends true + ? C + : StatementName]: IsNotMemberOf< + AllPODEntries

, + N + >; + } + > { + return new PODGroupSpecBuilder( + this.#innerBuilder.isNotMemberOf(names, values, customStatementName) + ); + } + + public inRange< + N extends keyof EntriesOfType, SupportsRangeChecks> & + string, + C extends string, + >( + name: N, + range: { + min: N extends keyof EntriesWithRangeChecks> + ? AllPODEntries

[N] extends "date" + ? Date + : bigint + : Date | bigint; + max: N extends keyof EntriesWithRangeChecks> + ? AllPODEntries

[N] extends "date" + ? Date + : bigint + : Date | bigint; + }, + customStatementName?: C + ): PODGroupSpecBuilder< + P, + S & { + [K in IsSingleLiteralString extends true + ? C + : StatementName<[N & string], "inRange", S>]: InRange< + AllPODEntries

, + N + >; + } + > { + return new PODGroupSpecBuilder( + this.#innerBuilder.inRange(name, range, customStatementName) + ); + } + + public notInRange< + N extends keyof EntriesOfType, SupportsRangeChecks> & + string, + C extends string, + >( + name: N, + range: { + min: AllPODEntries

[N] extends "date" ? Date : bigint; + max: AllPODEntries

[N] extends "date" ? Date : bigint; + }, + customStatementName?: C + ): PODGroupSpecBuilder< + P, + S & { + [K in IsSingleLiteralString extends true + ? C + : StatementName<[N & string], "notInRange", S>]: NotInRange< + AllPODEntries

, + N + >; + } + > { + return new PODGroupSpecBuilder( + this.#innerBuilder.notInRange(name, range, customStatementName) + ); + } + + public greaterThan< + N1 extends keyof AllPODEntries

& string, + N2 extends keyof EntriesOfType, EntryType> & string, + C extends string, + >( + name1: N1, + name2: N2, + customStatementName?: C + ): PODGroupSpecBuilder< + P, + S & { + [K in IsSingleLiteralString extends true + ? C + : StatementName< + [N1 & string, N2 & string], + "greaterThan", + S + >]: GreaterThan, N1, N2>; + } + > { + return new PODGroupSpecBuilder( + this.#innerBuilder.greaterThan(name1, name2, customStatementName) + ); + } + + public greaterThanEq< + N1 extends keyof AllPODEntries

& string, + N2 extends keyof EntriesOfType, EntryType> & string, + C extends string, + >( + name1: N1, + name2: N2, + customStatementName?: C + ): PODGroupSpecBuilder< + P, + S & { + [K in IsSingleLiteralString extends true + ? C + : StatementName< + [N1 & string, N2 & string], + "greaterThanEq", + S + >]: GreaterThanEq, N1, N2>; + } + > { + return new PODGroupSpecBuilder( + this.#innerBuilder.greaterThanEq(name1, name2, customStatementName) + ); + } + + public lessThan< + N1 extends keyof AllPODEntries

& string, + N2 extends keyof EntriesOfType, EntryType> & string, + C extends string, + >( + name1: N1, + name2: N2, + customStatementName?: C + ): PODGroupSpecBuilder< + P, + S & { + [K in IsSingleLiteralString extends true + ? C + : StatementName<[N1 & string, N2 & string], "lessThan", S>]: LessThan< + AllPODEntries

, + N1, + N2 + >; + } + > { + return new PODGroupSpecBuilder( + this.#innerBuilder.lessThan(name1, name2, customStatementName) + ); + } + + public lessThanEq< + N1 extends keyof AllPODEntries

& string, + N2 extends keyof EntriesOfType, EntryType> & string, + C extends string, + >( + name1: N1, + name2: N2, + customStatementName?: C + ): PODGroupSpecBuilder< + P, + S & { + [K in IsSingleLiteralString extends true + ? C + : StatementName< + [N1 & string, N2 & string], + "lessThanEq", + S + >]: LessThanEq, N1, N2>; + } + > { + return new PODGroupSpecBuilder( + this.#innerBuilder.lessThanEq(name1, name2, customStatementName) + ); + } + + public equalsEntry< + N1 extends keyof AllPODEntries

& string, + N2 extends keyof EntriesOfType, EntryType> & string, + C extends string, + >( + name1: N1, + name2: N2, + customStatementName?: C + ): PODGroupSpecBuilder< + P, + S & { + [K in IsSingleLiteralString extends true + ? C + : StatementName< + [N1 & string, N2 & string], + "equalsEntry", + S + >]: EqualsEntry, N1, N2>; + } + > { + return new PODGroupSpecBuilder( + this.#innerBuilder.equalsEntry(name1, name2, customStatementName) + ); + } + + public notEqualsEntry< + N1 extends keyof AllPODEntries

& string, + N2 extends keyof EntriesOfType, EntryType> & string, + C extends string, + >( + name1: N1, + name2: N2, + customStatementName?: C + ): PODGroupSpecBuilder< + P, + S & { + [K in IsSingleLiteralString extends true + ? C + : StatementName< + [N1 & string, N2 & string], + "notEqualsEntry", + S + >]: NotEqualsEntry, N1, N2>; + } + > { + return new PODGroupSpecBuilder( + this.#innerBuilder.notEqualsEntry(name1, name2, customStatementName) + ); + } +} + +if (import.meta.vitest) { + const { describe, it } = import.meta.vitest; + + describe("PODGroupSpecBuilder", () => { + it("should be able to create a builder", () => { + const pod = PODSpecBuilder.create() + .entry("my_string", "string") + .entry("my_int", "int") + .entry("mystery_name", "string"); + + const _pod2 = PODSpecBuilder.create().entry("something_else", "boolean"); + + const _builder = PODGroupSpecBuilder.create() + .pod("foo", pod.spec()) + .pod("bar", pod.spec()) + .inRange("foo.my_int", { min: 0n, max: 10n }); + }); + }); +} + +type _Entries = AllPODEntries<{ + foo: { + entries: { + my_string: "string"; + my_int: "int"; + mystery_name: "string"; + }; + // eslint-disable-next-line @typescript-eslint/no-empty-object-type + statements: {}; + }; + bar: { + entries: { + something_else: "boolean"; + }; + // eslint-disable-next-line @typescript-eslint/no-empty-object-type + statements: {}; + }; +}>; + +if (import.meta.vitest) { + const { describe, it } = import.meta.vitest; + + describe("PODGroupSpecBuilder", () => { + it("should be able to create a builder", () => { + const pod = PODSpecBuilder.create() + .entry("my_string", "string") + .entry("my_int", "int") + .entry("mystery_name", "string"); + + const _builder = PODGroupSpecBuilder.create().pod("foo", pod.spec()); + }); + }); +} diff --git a/packages/podspec/src/builders/pod.ts b/packages/podspec/src/builders/pod.ts new file mode 100644 index 0000000..8dc42dc --- /dev/null +++ b/packages/podspec/src/builders/pod.ts @@ -0,0 +1,541 @@ +import type { IsJsonSafe } from "../shared/jsonSafe.js"; +import type { IsSingleLiteralString } from "../shared/types.js"; +import type { + EntriesOfType, + EntryKeys, + EntryTypes, + PODValueTupleForNamedEntries, + PODValueType, + PODValueTypeFromTypeName, + VirtualEntries, +} from "./types/entries.js"; +import type { + EntriesWithRangeChecks, + EqualsEntry, + GreaterThan, + GreaterThanEq, + InRange, + IsMemberOf, + IsNotMemberOf, + LessThan, + LessThanEq, + NotEqualsEntry, + NotInRange, + StatementMap, + StatementName, +} from "./types/statements.js"; +import { UntypedPODSpecBuilder } from "./untypedPod.js"; + +/** + @todo + - [x] add lessThan, greaterThan, lessThanEq, greaterThanEq + - [x] add omitEntries + - [x] maybe add pick/omit for statements? + - [x] add signerPublicKey support (done at type level, not run-time) + - [ ] add constraints on signature + - [x] add contentID virtual entry (done at type level, not run-time) + - [ ] refactor types (also delete unused types in types dir) + - [x] rename away from v2 suffix + - [x] validate entry names + - [x] validate isMemberOf/isNotMemberOf parameters + - [ ] handle multiple/incompatible range/inequality checks on the same entry + - [x] switch to using value types rather than PODValues (everywhere? maybe not membership lists) + - [ ] better error messages + - [ ] consider adding a hash to the spec to prevent tampering + - [ ] optional/nullable entries + - [ ] restrict gt/lt/etc checks to numeric types? + */ + +export const virtualEntries: VirtualEntries = { + $contentID: "cryptographic", + //$signature: "string", + $signerPublicKey: "eddsa_pubkey", +}; + +export type PODSpec = { + entries: E; + statements: S; +}; + +// This is a compile-time check that the PODSpec is JSON-safe +true satisfies IsJsonSafe>; + +type NonOverlappingStatements = { + [K in keyof S as S[K] extends // eslint-disable-next-line @typescript-eslint/no-explicit-any + | IsMemberOf + // eslint-disable-next-line @typescript-eslint/no-explicit-any + | IsNotMemberOf + ? Entries[number] extends N[number] + ? K + : never + : // eslint-disable-next-line @typescript-eslint/no-explicit-any + S[K] extends InRange + ? Entry extends N[number] + ? K + : never + : // eslint-disable-next-line @typescript-eslint/no-explicit-any + S[K] extends NotInRange + ? Entry extends N[number] + ? K + : never + : // eslint-disable-next-line @typescript-eslint/no-explicit-any + S[K] extends EqualsEntry + ? [Entry1, Entry2][number] extends N[number] + ? K + : never + : // eslint-disable-next-line @typescript-eslint/no-explicit-any + S[K] extends NotEqualsEntry + ? [Entry1, Entry2][number] extends N[number] + ? K + : never + : never]: S[K]; +}; + +type Concrete = T extends object ? { [K in keyof T]: T[K] } : T; + +type AddEntry< + E extends EntryTypes, + K extends keyof E, + V extends PODValueType, +> = Concrete; + +export class PODSpecBuilder< + E extends EntryTypes, + // eslint-disable-next-line @typescript-eslint/no-empty-object-type + S extends StatementMap = {}, +> { + readonly #innerBuilder: UntypedPODSpecBuilder; + + private constructor(innerBuilder: UntypedPODSpecBuilder) { + this.#innerBuilder = innerBuilder; + } + + // eslint-disable-next-line @typescript-eslint/no-empty-object-type + public static create(): PODSpecBuilder<{}, {}> { + return new PODSpecBuilder(UntypedPODSpecBuilder.create()); + } + + public spec(): PODSpec { + return this.#innerBuilder.spec() as PODSpec; + } + + public toJSON(): string { + return this.#innerBuilder.toJSON(); + } + + public entry< + K extends string, + V extends PODValueType, + NewEntries extends AddEntry, + >( + key: IsSingleLiteralString extends true ? Exclude : never, + type: V + ): PODSpecBuilder { + return new PODSpecBuilder( + this.#innerBuilder.entry(key, type) + ); + } + + public entries( + entries: NewEntries + ): PODSpecBuilder, S> { + return new PODSpecBuilder(this.#innerBuilder.entries(entries)); + } + + /** + * Pick entries by key + */ + public pickEntries< + K extends (keyof E extends never ? string : keyof E) & string, + >( + keys: K[] + ): PODSpecBuilder, Concrete>> { + return new PODSpecBuilder(this.#innerBuilder.pickEntries(keys)); + } + + public omitEntries< + K extends (keyof E extends never ? string : keyof E) & string, + >( + keys: K[] + ): PODSpecBuilder, Concrete>> { + return new PODSpecBuilder(this.#innerBuilder.omitEntries(keys)); + } + + public pickStatements( + keys: K[] + ): PODSpecBuilder>> { + return new PODSpecBuilder(this.#innerBuilder.pickStatements(keys)); + } + + public omitStatements( + keys: K[] + ): PODSpecBuilder>> { + return new PODSpecBuilder(this.#innerBuilder.omitStatements(keys)); + } + + /** + * Add a constraint that the entries must be a member of a list of tuples + * + * The names must be an array of one or more entry names for the POD. + * If there is only one name, then the values must be an array of PODValues + * of the type for that entry. + * + * If there are multiple names, then the values must be an array of one or + * more tuples, where each tuple is an array of PODValues of the type for + * each entry, in the order matching the names array. + * + * @param names - The names of the entries to be constrained + * @param values - The values to be constrained to + * @returns A new PODSpecBuilder with the statement added + */ + public isMemberOf, C extends string>( + names: [...N], + values: N["length"] extends 1 + ? PODValueTypeFromTypeName< + (E & VirtualEntries)[N[0] & keyof (E & VirtualEntries)] + >[] + : PODValueTupleForNamedEntries[], + customStatementName?: C + ): PODSpecBuilder< + E, + S & { + [K in IsSingleLiteralString extends true + ? C + : StatementName]: IsMemberOf; + } + > { + return new PODSpecBuilder( + this.#innerBuilder.isMemberOf(names, values, customStatementName) + ); + } + + /** + * Add a constraint that the entries must not be a member of a list of tuples + * + * The names must be an array of one or more entry names for the POD. + * If there is only one name, then the values must be an array of PODValues + * of the type for that entry. + * + * If there are multiple names, then the values must be an array of one or + * more tuples, where each tuple is an array of PODValues of the type for + * each entry, in the order matching the names array. + * + * @param names - The names of the entries to be constrained + * @param values - The values to be constrained to + * @returns A new PODSpecBuilder with the statement added + */ + public isNotMemberOf, C extends string>( + names: [...N], + values: N["length"] extends 1 + ? PODValueTypeFromTypeName< + (E & VirtualEntries)[N[0] & keyof (E & VirtualEntries)] + >[] + : PODValueTupleForNamedEntries[], + customStatementName?: C + ): PODSpecBuilder< + E, + S & { + [K in IsSingleLiteralString extends true + ? C + : StatementName]: IsNotMemberOf; + } + > { + return new PODSpecBuilder( + this.#innerBuilder.isNotMemberOf(names, values, customStatementName) + ); + } + + /** + * Add a constraint that the entry must be in a range + * + * @param name - The name of the entry to be constrained + * @param range - The range to be constrained to + * @returns A new PODSpecBuilder with the statement added + */ + public inRange< + N extends keyof EntriesWithRangeChecks & string, + C extends string, + >( + name: N, + range: { + min: E[N] extends "date" ? Date : bigint; + max: E[N] extends "date" ? Date : bigint; + }, + customStatementName?: C + ): PODSpecBuilder< + E, + S & { + [K in IsSingleLiteralString extends true + ? C + : StatementName<[N & string], "inRange", S>]: InRange< + E & VirtualEntries, + N + >; + } + > { + return new PODSpecBuilder( + this.#innerBuilder.inRange(name, range, customStatementName) + ); + } + + /** + * Add a constraint that the entry must not be in a range + * + * @param name - The name of the entry to be constrained + * @param range - The range to be constrained to + * @returns A new PODSpecBuilder with the statement added + */ + public notInRange< + N extends keyof EntriesWithRangeChecks & string, + C extends string, + >( + name: N, + range: { + min: E[N] extends "date" ? Date : bigint; + max: E[N] extends "date" ? Date : bigint; + }, + customStatementName?: C + ): PODSpecBuilder< + E, + S & { + [K in IsSingleLiteralString extends true + ? C + : StatementName<[N & string], "notInRange", S>]: NotInRange< + E & VirtualEntries, + N + >; + } + > { + return new PODSpecBuilder( + this.#innerBuilder.notInRange(name, range, customStatementName) + ); + } + + public equalsEntry< + N1 extends keyof (E & VirtualEntries) & string, + N2 extends keyof EntriesOfType< + E & VirtualEntries, + (E & VirtualEntries)[N1] + > & + string, + C extends string, + >( + name1: N1, + name2: Exclude, + customStatementName?: C + ): PODSpecBuilder< + E, + S & { + [K in IsSingleLiteralString extends true + ? C + : StatementName<[N1, N2], "equalsEntry", S>]: EqualsEntry; + } + > { + return new PODSpecBuilder( + this.#innerBuilder.equalsEntry(name1, name2, customStatementName) + ); + } + + public notEqualsEntry< + N1 extends keyof (E & VirtualEntries) & string, + N2 extends keyof EntriesOfType< + E & VirtualEntries, + (E & VirtualEntries)[N1] + > & + string, + C extends string, + >( + name1: N1, + name2: Exclude, + customStatementName?: C + ): PODSpecBuilder< + E, + S & { + [K in IsSingleLiteralString extends true + ? C + : StatementName<[N1, N2], "notEqualsEntry", S>]: NotEqualsEntry< + E, + N1, + N2 + >; + } + > { + return new PODSpecBuilder( + this.#innerBuilder.notEqualsEntry(name1, name2, customStatementName) + ); + } + + public greaterThan< + N1 extends keyof (E & VirtualEntries) & string, + N2 extends keyof EntriesOfType< + E & VirtualEntries, + (E & VirtualEntries)[N1] + > & + string, + C extends string, + >( + name1: N1, + name2: Exclude, + customStatementName?: C + ): PODSpecBuilder< + E, + S & { + [K in IsSingleLiteralString extends true + ? C + : StatementName<[N1, N2], "greaterThan", S>]: GreaterThan; + } + > { + return new PODSpecBuilder( + this.#innerBuilder.greaterThan(name1, name2, customStatementName) + ); + } + + public greaterThanEq< + N1 extends keyof (E & VirtualEntries) & string, + N2 extends keyof EntriesOfType< + E & VirtualEntries, + (E & VirtualEntries)[N1] + > & + string, + C extends string, + >( + name1: N1, + name2: Exclude, + customStatementName?: C + ): PODSpecBuilder< + E, + S & { + [K in IsSingleLiteralString extends true + ? C + : StatementName<[N1, N2], "greaterThanEq", S>]: GreaterThanEq< + E, + N1, + N2 + >; + } + > { + return new PODSpecBuilder( + this.#innerBuilder.greaterThanEq(name1, name2, customStatementName) + ); + } + + public lessThan< + N1 extends keyof (E & VirtualEntries) & string, + N2 extends keyof EntriesOfType< + E & VirtualEntries, + (E & VirtualEntries)[N1] + > & + string, + C extends string, + >( + name1: N1, + name2: Exclude, + customStatementName?: C + ): PODSpecBuilder< + E, + S & { + [K in IsSingleLiteralString extends true + ? C + : StatementName<[N1, N2], "lessThan", S>]: LessThan; + } + > { + return new PODSpecBuilder( + this.#innerBuilder.lessThan(name1, name2, customStatementName) + ); + } + + public lessThanEq< + N1 extends keyof (E & VirtualEntries) & string, + N2 extends keyof EntriesOfType< + E & VirtualEntries, + (E & VirtualEntries)[N1] + > & + string, + C extends string, + >( + name1: N1, + name2: Exclude, + customStatementName?: C + ): PODSpecBuilder< + E, + S & { + [K in IsSingleLiteralString extends true + ? C + : StatementName<[N1, N2], "lessThanEq", S>]: LessThanEq; + } + > { + return new PODSpecBuilder( + this.#innerBuilder.lessThanEq(name1, name2, customStatementName) + ); + } +} + +if (import.meta.vitest) { + const { describe, it, expect } = import.meta.vitest; + + describe("PODSpecBuilder", () => { + it("should be able to create a builder", () => { + const builder = PODSpecBuilder.create(); + expect(builder).toBeDefined(); + + const builderWithEntries = builder + .entry("my_string", "string") + .entry("my_int", "int") + .entry("my_cryptographic", "cryptographic") + .entry("my_bytes", "bytes") + .entry("my_date", "date") + .entry("my_null", "null") + .entry("my_eddsa_pubkey", "eddsa_pubkey") + .entry("my_other_string", "string") + .entry("my_other_int", "int"); + + expect(builderWithEntries.spec().entries).toEqual({ + my_string: "string", + my_int: "int", + my_cryptographic: "cryptographic", + my_bytes: "bytes", + my_date: "date", + my_null: "null", + my_eddsa_pubkey: "eddsa_pubkey", + my_other_string: "string", + my_other_int: "int", + }); + + const builderWithStatements = builderWithEntries + .inRange("my_int", { min: 0n, max: 10n }) + .inRange("my_date", { + min: new Date("2020-01-01"), + max: new Date("2020-01-10"), + }) + .isMemberOf(["my_string"], ["foo", "bar"]) + .isNotMemberOf(["my_string"], ["baz"]) + .equalsEntry("my_string", "my_other_string") + .notEqualsEntry("my_int", "my_other_int") + // TODO At some point, some of these should throw because they cannot + // possibly all be true. + .greaterThan("my_int", "my_other_int") + .greaterThanEq("my_int", "my_other_int") + .lessThan("my_int", "my_other_int") + .lessThanEq("my_int", "my_other_int"); + + expect(Object.keys(builderWithStatements.spec().statements)).toEqual([ + "my_int_inRange", + "my_date_inRange", + "my_string_isMemberOf", + "my_string_isNotMemberOf", + "my_string_my_other_string_equalsEntry", + "my_int_my_other_int_notEqualsEntry", + "my_int_my_other_int_greaterThan", + "my_int_my_other_int_greaterThanEq", + "my_int_my_other_int_lessThan", + "my_int_my_other_int_lessThanEq", + ]); + + expect(() => + builderWithEntries + // @ts-expect-error entry does not exist + .isMemberOf(["non_existent_entry"], ["foo", "bar"]) + ).toThrow(); + }); + }); +} diff --git a/packages/podspec/src/builders/shared.ts b/packages/podspec/src/builders/shared.ts new file mode 100644 index 0000000..52146ed --- /dev/null +++ b/packages/podspec/src/builders/shared.ts @@ -0,0 +1,140 @@ +import { checkPODValue, type PODValue } from "@pcd/pod"; +import type { PODValueType } from "./types/entries.js"; +import { fromByteArray } from "base64-js"; +import type { SupportsRangeChecks } from "./types/statements.js"; +import canonicalize from "canonicalize"; + +/** + * Validates a range check. + * + * @param min - The minimum value + * @param max - The maximum value + * @param allowedMin - The minimum value that is allowed + * @param allowedMax - The maximum value that is allowed + * @throws RangeError if the value is out of range + */ +export function validateRange( + min: bigint | Date, + max: bigint | Date, + allowedMin: bigint | Date, + allowedMax: bigint | Date +): void { + if (min > max) { + throw new RangeError("Min must be less than or equal to max"); + } + if (min < allowedMin || max > allowedMax) { + throw new RangeError("Value out of range"); + } +} + +/** + * Converts a value to a PODValue. + * + * @param nameForError - The name of the value for error messages + * @param type - The type of the value + * @param value - The value to convert + * @throws Error if the value is not a valid PODValue + * @returns The PODValue + */ +export function toPODValue( + nameForError: string, + type: PODValueType, + value: Extract["value"] +): Extract { + return checkPODValue(nameForError, { value, type } as PODValue); +} + +/** + * Freezes an object to make it immutable. + * + * @param obj - The object to freeze. + * @returns The frozen object. + */ +export function deepFreeze(obj: T): T { + if (obj && typeof obj === "object") { + // Get all properties, including non-enumerable ones + const properties = [ + ...Object.getOwnPropertyNames(obj), + ...Object.getOwnPropertySymbols(obj), + ]; + + properties.forEach((prop) => { + const value = obj[prop as keyof T]; + if (value && typeof value === "object" && !Object.isFrozen(value)) { + if (value instanceof Uint8Array) { + return; + } + deepFreeze(value); + } + }); + } + return Object.freeze(obj); +} + +function valueToString(value: PODValue["value"]): string { + if (value === null) { + return "null"; + } + if (value instanceof Uint8Array) { + return fromByteArray(value); + } + if (value instanceof Date) { + return value.toISOString(); + } + return value.toString(); +} + +/** + * Converts POD values to their string representation for storage in a spec. + * Handles both single-entry and multi-entry cases. + */ +export function convertValuesToStringTuples( + names: [...N], + values: N["length"] extends 1 ? PODValue["value"][] : PODValue["value"][][], + entries: Record +): { [K in keyof N]: string }[] { + return names.length === 1 + ? (values as PODValue["value"][]).map((v) => { + const name = names[0]; + const type = entries[name]; + // TODO Maybe catch and rethrow an error with more context + checkPODValue(name, { value: v, type } as PODValue); + return [valueToString(v)] as { [K in keyof N]: string }; + }) + : (values as PODValue["value"][][]).map((tuple, index) => { + if (tuple.length !== names.length) { + throw new Error(`Tuple ${index} length does not match names length`); + } + return tuple.map((v, i) => { + const type = entries[names[i]]; + // TODO Maybe catch and rethrow an error with more context + checkPODValue(names[i], { value: v, type } as PODValue); + return valueToString(v); + }) as { [K in keyof N]: string }; + }); +} + +type DoesNotSupportRangeChecks = Exclude; + +export function supportsRangeChecks( + type: PODValueType +): type is SupportsRangeChecks { + switch (type) { + case "int": + case "boolean": + case "date": + return true; + default: + // Verify the narrowed type matches DoesNotSupportRangeChecks exactly + // prettier-ignore + (type) satisfies DoesNotSupportRangeChecks; + return false; + } +} + +export function canonicalizeJSON(input: unknown): string | undefined { + // Something is screwy with the typings for canonicalize + return (canonicalize as unknown as (input: unknown) => string | undefined)( + input + ); +} diff --git a/packages/podspec/src/builders/types/entries.ts b/packages/podspec/src/builders/types/entries.ts new file mode 100644 index 0000000..8c17ea5 --- /dev/null +++ b/packages/podspec/src/builders/types/entries.ts @@ -0,0 +1,29 @@ +import type { PODName, PODValue } from "@pcd/pod"; + +export type PODValueType = PODValue["type"]; + +export type EntryTypes = Record; + +export type EntryKeys = (keyof E & string)[]; + +export type PODValueTupleForNamedEntries< + E extends EntryTypes, + Names extends EntryKeys, +> = { + [K in keyof Names]: PODValueTypeFromTypeName; +}; + +export type PODValueTypeFromTypeName = Extract< + PODValue, + { type: T } +>["value"]; + +export type EntriesOfType = { + [P in keyof E as E[P] extends T ? P & string : never]: E[P]; +}; + +export type VirtualEntries = { + $contentID: "cryptographic"; + //$signature: "string"; + $signerPublicKey: "eddsa_pubkey"; +}; diff --git a/packages/podspec/src/builders/types/statements.ts b/packages/podspec/src/builders/types/statements.ts new file mode 100644 index 0000000..11a82ea --- /dev/null +++ b/packages/podspec/src/builders/types/statements.ts @@ -0,0 +1,223 @@ +import type { + EntriesOfType, + EntryKeys, + EntryTypes, + PODValueTupleForNamedEntries, + VirtualEntries, +} from "./entries.js"; + +/**************************************************************************** + * Statements + ****************************************************************************/ + +export type MembershipListInput< + E extends EntryTypes, + N extends EntryKeys, +> = PODValueTupleForNamedEntries[]; + +export type MembershipListPersistent< + E extends EntryTypes, + N extends EntryKeys, +> = { [K in keyof N]: string }[]; + +type Concrete = T extends object ? { [K in keyof T]: T[K] } : T; + +export type IsMemberOf< + E extends EntryTypes, + N extends EntryKeys & string[], +> = { + entries: N; + type: "isMemberOf"; + isMemberOf: Concrete>; +}; + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +export type AnyIsMemberOf = IsMemberOf; + +export type IsNotMemberOf< + E extends EntryTypes, + N extends EntryKeys & string[], +> = { + entries: N; + type: "isNotMemberOf"; + isNotMemberOf: Concrete>; +}; + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +export type AnyIsNotMemberOf = IsNotMemberOf; + +// Which entry types support range checks? +export type SupportsRangeChecks = "int" | "boolean" | "date"; +export type EntriesWithRangeChecks = EntriesOfType< + E, + SupportsRangeChecks +>; + +export type RangeInput< + E extends EntryTypes, + N extends keyof EntriesWithRangeChecks & string, +> = { + min: E[N] extends "date" ? Date : bigint; + max: E[N] extends "date" ? Date : bigint; +}; + +export type RangePersistent = { + min: string; + max: string; +}; + +export type InRange< + E extends EntryTypes, + N extends keyof EntriesWithRangeChecks & string, +> = { + entries: [entry: N]; + type: "inRange"; + inRange: RangePersistent; +}; + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +export type AnyInRange = InRange; + +export type NotInRange< + E extends EntryTypes, + N extends keyof EntriesWithRangeChecks & string, +> = { + entries: [entry: N]; + type: "notInRange"; + notInRange: RangePersistent; +}; + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +export type AnyNotInRange = NotInRange; + +export type EqualsEntry< + E extends EntryTypes, + N1 extends keyof (E & VirtualEntries) & string, + N2 extends keyof (E & VirtualEntries) & string, +> = { + entries: [entry: N1, otherEntry: N2]; + type: "equalsEntry"; +}; + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +export type AnyEqualsEntry = EqualsEntry; + +export type NotEqualsEntry< + E extends EntryTypes, + N1 extends keyof (E & VirtualEntries) & string, + N2 extends keyof (E & VirtualEntries) & string, +> = { + entries: [entry: N1, otherEntry: N2]; + type: "notEqualsEntry"; +}; + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +export type AnyNotEqualsEntry = NotEqualsEntry; + +export type GreaterThan< + E extends EntryTypes, + N1 extends keyof (E & VirtualEntries) & string, + N2 extends keyof (E & VirtualEntries) & string, +> = { + entries: [entry: N1, otherEntry: N2]; + type: "greaterThan"; +}; + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +export type AnyGreaterThan = GreaterThan; + +export type GreaterThanEq< + E extends EntryTypes, + N1 extends keyof (E & VirtualEntries) & string, + N2 extends keyof (E & VirtualEntries) & string, +> = { + entries: [entry: N1, otherEntry: N2]; + type: "greaterThanEq"; +}; + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +export type AnyGreaterThanEq = GreaterThanEq; + +export type LessThan< + E extends EntryTypes, + N1 extends keyof (E & VirtualEntries) & string, + N2 extends keyof (E & VirtualEntries) & string, +> = { + entries: [entry: N1, otherEntry: N2]; + type: "lessThan"; +}; + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +export type AnyLessThan = LessThan; + +export type LessThanEq< + E extends EntryTypes, + N1 extends keyof (E & VirtualEntries) & string, + N2 extends keyof (E & VirtualEntries) & string, +> = { + entries: [entry: N1, otherEntry: N2]; + type: "lessThanEq"; +}; + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +export type AnyLessThanEq = LessThanEq; + +export type Statements = + // eslint-disable-next-line @typescript-eslint/no-explicit-any + | IsMemberOf + // eslint-disable-next-line @typescript-eslint/no-explicit-any + | IsNotMemberOf + // eslint-disable-next-line @typescript-eslint/no-explicit-any + | InRange + // eslint-disable-next-line @typescript-eslint/no-explicit-any + | NotInRange + // eslint-disable-next-line @typescript-eslint/no-explicit-any + | EqualsEntry + // eslint-disable-next-line @typescript-eslint/no-explicit-any + | NotEqualsEntry + // eslint-disable-next-line @typescript-eslint/no-explicit-any + | GreaterThan + // eslint-disable-next-line @typescript-eslint/no-explicit-any + | GreaterThanEq + // eslint-disable-next-line @typescript-eslint/no-explicit-any + | LessThan + // eslint-disable-next-line @typescript-eslint/no-explicit-any + | LessThanEq; + +export type StatementMap = Record; + +/**************************************************************************** + * Statement naming + ****************************************************************************/ + +// Utility types for statement naming +type JoinWithUnderscore = T extends readonly [ + infer F extends string, + ...infer R extends string[], +] + ? R["length"] extends 0 + ? F + : `${F}_${JoinWithUnderscore}` + : never; + +type BaseStatementName< + N extends readonly string[], + S extends Statements["type"], +> = `${JoinWithUnderscore}_${S}`; + +type NextAvailableSuffix< + Base extends string, + S extends StatementMap, +> = Base extends keyof S + ? `${Base}_1` extends keyof S + ? `${Base}_2` extends keyof S + ? `${Base}_3` + : `${Base}_2` + : `${Base}_1` + : Base; + +export type StatementName< + N extends readonly string[], + S extends Statements["type"], + Map extends StatementMap, +> = NextAvailableSuffix, Map>; diff --git a/packages/podspec/src/builders/untypedGroup.ts b/packages/podspec/src/builders/untypedGroup.ts new file mode 100644 index 0000000..9c60678 --- /dev/null +++ b/packages/podspec/src/builders/untypedGroup.ts @@ -0,0 +1,805 @@ +import { + type PODName, + type PODValue, + POD_DATE_MAX, + POD_DATE_MIN, + POD_INT_MAX, + POD_INT_MIN, + checkPODName, +} from "@pcd/pod"; +import type { NamedPODSpecs, PODGroupSpec } from "./group.js"; +import { type PODSpec, PODSpecBuilder, virtualEntries } from "./pod.js"; +import { + convertValuesToStringTuples, + deepFreeze, + supportsRangeChecks, + validateRange, +} from "./shared.js"; +import type { EntryTypes, PODValueType } from "./types/entries.js"; +import type { + AnyEqualsEntry, + AnyGreaterThan, + AnyGreaterThanEq, + AnyInRange, + AnyIsMemberOf, + AnyIsNotMemberOf, + AnyLessThan, + AnyLessThanEq, + AnyNotEqualsEntry, + AnyNotInRange, + StatementMap, +} from "./types/statements.js"; + +export class UntypedPODGroupSpecBuilder { + readonly #spec: PODGroupSpec; + + private constructor(spec: PODGroupSpec) { + this.#spec = spec; + } + + public static create() { + return new UntypedPODGroupSpecBuilder({ + pods: {}, + statements: {}, + }); + } + + public spec(): PODGroupSpec { + return deepFreeze(this.#spec); + } + + public toJSON(): string { + return JSON.stringify( + { + ...this.#spec, + }, + null, + 2 + ); + } + + public pod( + name: PODName, + spec: PODSpec + ): UntypedPODGroupSpecBuilder { + if (Object.prototype.hasOwnProperty.call(this.#spec.pods, name)) { + throw new Error(`POD "${name}" already exists`); + } + + // Will throw if the name is not valid. + checkPODName(name); + + return new UntypedPODGroupSpecBuilder({ + ...this.#spec, + pods: { ...this.#spec.pods, [name]: spec }, + }); + } + + public isMemberOf( + names: [...PODName[]], + values: PODValue["value"][] | PODValue["value"][][], + customStatementName?: string + ): UntypedPODGroupSpecBuilder { + // Check for duplicate names + const uniqueNames = new Set(names); + if (uniqueNames.size !== names.length) { + throw new Error("Duplicate entry names are not allowed"); + } + + const allEntries = Object.fromEntries( + Object.entries(this.#spec.pods).flatMap(([podName, podSpec]) => [ + ...Object.entries(podSpec.entries).map( + ([entryName, entryType]): [string, PODValueType] => [ + `${podName}.${entryName}`, + entryType, + ] + ), + ...Object.entries(virtualEntries).map( + ([entryName, entryType]): [string, PODValueType] => [ + `${podName}.${entryName}`, + entryType, + ] + ), + ]) + ); + + for (const name of names) { + if (!Object.prototype.hasOwnProperty.call(allEntries, name)) { + throw new Error(`Entry "${name}" does not exist`); + } + } + + const statement: AnyIsMemberOf = { + entries: names, + type: "isMemberOf", + // eslint-disable-next-line @typescript-eslint/no-explicit-any + isMemberOf: convertValuesToStringTuples(names, values, allEntries), + }; + + const baseName = customStatementName ?? `${names.join("_")}_isMemberOf`; + let statementName: string = baseName; + let suffix = 1; + + while ( + Object.prototype.hasOwnProperty.call(this.#spec.statements, statementName) + ) { + statementName = `${baseName}_${suffix++}`; + } + + return new UntypedPODGroupSpecBuilder({ + ...this.#spec, + statements: { + ...this.#spec.statements, + [statementName]: statement, + }, + }); + } + + public isNotMemberOf( + names: [...PODName[]], + values: PODValue["value"][] | PODValue["value"][][], + customStatementName?: string + ): UntypedPODGroupSpecBuilder { + // Check for duplicate names + const uniqueNames = new Set(names); + if (uniqueNames.size !== names.length) { + throw new Error("Duplicate entry names are not allowed"); + } + + const allEntries = Object.fromEntries( + Object.entries(this.#spec.pods).flatMap(([podName, podSpec]) => [ + ...Object.entries(podSpec.entries).map( + ([entryName, entryType]): [string, PODValueType] => [ + `${podName}.${entryName}`, + entryType, + ] + ), + ...Object.entries(virtualEntries).map( + ([entryName, entryType]): [string, PODValueType] => [ + `${podName}.${entryName}`, + entryType, + ] + ), + ]) + ); + + for (const name of names) { + if (!Object.prototype.hasOwnProperty.call(allEntries, name)) { + throw new Error(`Entry "${name}" does not exist`); + } + } + + const statement: AnyIsNotMemberOf = { + entries: names, + type: "isNotMemberOf", + // eslint-disable-next-line @typescript-eslint/no-explicit-any + isNotMemberOf: convertValuesToStringTuples( + names, + values, + allEntries + ), + }; + + const baseName = customStatementName ?? `${names.join("_")}_isNotMemberOf`; + let statementName: string = baseName; + let suffix = 1; + + while ( + Object.prototype.hasOwnProperty.call(this.#spec.statements, statementName) + ) { + statementName = `${baseName}_${suffix++}`; + } + + return new UntypedPODGroupSpecBuilder({ + ...this.#spec, + statements: { + ...this.#spec.statements, + [statementName]: statement, + }, + }); + } + + public inRange( + name: PODName, + range: { + min: bigint | Date; + max: bigint | Date; + }, + customStatementName?: string + ): UntypedPODGroupSpecBuilder { + // Check that the entry exists + const [podName, entryName] = name.split("."); + if ( + podName === undefined || + entryName === undefined || + !Object.prototype.hasOwnProperty.call(this.#spec.pods, podName) || + !Object.prototype.hasOwnProperty.call( + this.#spec.pods[podName]!.entries, + entryName + ) + ) { + throw new Error(`Entry "${name}" does not exist`); + } + + const entryType = this.#spec.pods[podName]!.entries[entryName]!; + + if (!supportsRangeChecks(entryType)) { + throw new Error(`Entry "${name}" does not support range checks`); + } + + switch (entryType) { + case "int": + validateRange( + range.min as bigint, + range.max as bigint, + POD_INT_MIN, + POD_INT_MAX + ); + break; + case "boolean": + validateRange(range.min as bigint, range.max as bigint, 0n, 1n); + break; + case "date": + validateRange( + range.min as Date, + range.max as Date, + POD_DATE_MIN, + POD_DATE_MAX + ); + break; + default: + const _exhaustiveCheck: never = entryType; + throw new Error(`Unsupported entry type: ${name}`); + } + + const statement: AnyInRange = { + entries: [name], + type: "inRange", + inRange: { + min: + range.min instanceof Date + ? range.min.getTime().toString() + : range.min.toString(), + max: + range.max instanceof Date + ? range.max.getTime().toString() + : range.max.toString(), + }, + }; + + const baseName = customStatementName ?? `${name}_inRange`; + let statementName: string = baseName; + let suffix = 1; + + while ( + Object.prototype.hasOwnProperty.call(this.#spec.statements, statementName) + ) { + statementName = `${baseName}_${suffix++}`; + } + + return new UntypedPODGroupSpecBuilder({ + ...this.#spec, + statements: { + ...this.#spec.statements, + [statementName]: statement, + }, + }); + } + + public notInRange( + name: PODName, + range: { + min: bigint | Date; + max: bigint | Date; + }, + customStatementName?: string + ): UntypedPODGroupSpecBuilder { + // Check that the entry exists + const [podName, entryName] = name.split("."); + if ( + podName === undefined || + entryName === undefined || + !Object.prototype.hasOwnProperty.call(this.#spec.pods, podName) || + !Object.prototype.hasOwnProperty.call( + this.#spec.pods[podName]!.entries, + entryName + ) + ) { + throw new Error(`Entry "${name}" does not exist`); + } + + const entryType = this.#spec.pods[podName]!.entries[entryName]!; + + if (!supportsRangeChecks(entryType)) { + throw new Error(`Entry "${name}" does not support range checks`); + } + + switch (entryType) { + case "int": + validateRange( + range.min as bigint, + range.max as bigint, + POD_INT_MIN, + POD_INT_MAX + ); + break; + case "boolean": + validateRange(range.min as bigint, range.max as bigint, 0n, 1n); + break; + case "date": + validateRange( + range.min as Date, + range.max as Date, + POD_DATE_MIN, + POD_DATE_MAX + ); + break; + default: + const _exhaustiveCheck: never = entryType; + throw new Error(`Unsupported entry type: ${name}`); + } + + const statement: AnyNotInRange = { + entries: [name], + type: "notInRange", + notInRange: { + min: + range.min instanceof Date + ? range.min.getTime().toString() + : range.min.toString(), + max: + range.max instanceof Date + ? range.max.getTime().toString() + : range.max.toString(), + }, + }; + + const baseName = customStatementName ?? `${name}_notInRange`; + let statementName: string = baseName; + let suffix = 1; + + while ( + Object.prototype.hasOwnProperty.call(this.#spec.statements, statementName) + ) { + statementName = `${baseName}_${suffix++}`; + } + + return new UntypedPODGroupSpecBuilder({ + ...this.#spec, + statements: { + ...this.#spec.statements, + [statementName]: statement, + }, + }); + } + + public greaterThan( + name1: PODName, + name2: PODName, + customStatementName?: string + ): UntypedPODGroupSpecBuilder { + // Check that both entries exist + const [pod1, entry1] = name1.split("."); + const [pod2, entry2] = name2.split("."); + if ( + pod1 === undefined || + entry1 === undefined || + !Object.prototype.hasOwnProperty.call(this.#spec.pods, pod1) || + !Object.prototype.hasOwnProperty.call( + this.#spec.pods[pod1]!.entries, + entry1 + ) + ) { + throw new Error(`Entry "${name1}" does not exist`); + } + if ( + pod2 === undefined || + entry2 === undefined || + !Object.prototype.hasOwnProperty.call(this.#spec.pods, pod2) || + !Object.prototype.hasOwnProperty.call( + this.#spec.pods[pod2]!.entries, + entry2 + ) + ) { + throw new Error(`Entry "${name2}" does not exist`); + } + + if (name1 === name2) { + throw new Error("Entry names must be different"); + } + + const type1 = this.#spec.pods[pod1]!.entries[entry1]!; + const type2 = this.#spec.pods[pod2]!.entries[entry2]!; + if (type1 !== type2) { + throw new Error("Entry types must be the same"); + } + + const statement: AnyGreaterThan = { + entries: [name1, name2], + type: "greaterThan", + }; + + const baseName = customStatementName ?? `${name1}_${name2}_greaterThan`; + let statementName: string = baseName; + let suffix = 1; + + while ( + Object.prototype.hasOwnProperty.call(this.#spec.statements, statementName) + ) { + statementName = `${baseName}_${suffix++}`; + } + + return new UntypedPODGroupSpecBuilder({ + ...this.#spec, + statements: { + ...this.#spec.statements, + [statementName]: statement, + }, + }); + } + + public greaterThanEq( + name1: PODName, + name2: PODName, + customStatementName?: string + ): UntypedPODGroupSpecBuilder { + // Check that both entries exist + const [pod1, entry1] = name1.split("."); + const [pod2, entry2] = name2.split("."); + if ( + pod1 === undefined || + entry1 === undefined || + !Object.prototype.hasOwnProperty.call(this.#spec.pods, pod1) || + !Object.prototype.hasOwnProperty.call( + this.#spec.pods[pod1]!.entries, + entry1 + ) + ) { + throw new Error(`Entry "${name1}" does not exist`); + } + if ( + pod2 === undefined || + entry2 === undefined || + !Object.prototype.hasOwnProperty.call(this.#spec.pods, pod2) || + !Object.prototype.hasOwnProperty.call( + this.#spec.pods[pod2]!.entries, + entry2 + ) + ) { + throw new Error(`Entry "${name2}" does not exist`); + } + + if (name1 === name2) { + throw new Error("Entry names must be different"); + } + + const type1 = this.#spec.pods[pod1]!.entries[entry1]!; + const type2 = this.#spec.pods[pod2]!.entries[entry2]!; + if (type1 !== type2) { + throw new Error("Entry types must be the same"); + } + + const statement: AnyGreaterThanEq = { + entries: [name1, name2], + type: "greaterThanEq", + }; + + const baseName = customStatementName ?? `${name1}_${name2}_greaterThanEq`; + let statementName: string = baseName; + let suffix = 1; + + while ( + Object.prototype.hasOwnProperty.call(this.#spec.statements, statementName) + ) { + statementName = `${baseName}_${suffix++}`; + } + + return new UntypedPODGroupSpecBuilder({ + ...this.#spec, + statements: { + ...this.#spec.statements, + [statementName]: statement, + }, + }); + } + + public lessThan( + name1: PODName, + name2: PODName, + customStatementName?: string + ): UntypedPODGroupSpecBuilder { + // Check that both entries exist + const [pod1, entry1] = name1.split("."); + const [pod2, entry2] = name2.split("."); + if ( + pod1 === undefined || + entry1 === undefined || + !Object.prototype.hasOwnProperty.call(this.#spec.pods, pod1) || + !Object.prototype.hasOwnProperty.call( + this.#spec.pods[pod1]!.entries, + entry1 + ) + ) { + throw new Error(`Entry "${name1}" does not exist`); + } + if ( + pod2 === undefined || + entry2 === undefined || + !Object.prototype.hasOwnProperty.call(this.#spec.pods, pod2) || + !Object.prototype.hasOwnProperty.call( + this.#spec.pods[pod2]!.entries, + entry2 + ) + ) { + throw new Error(`Entry "${name2}" does not exist`); + } + + if (name1 === name2) { + throw new Error("Entry names must be different"); + } + + const type1 = this.#spec.pods[pod1]!.entries[entry1]!; + const type2 = this.#spec.pods[pod2]!.entries[entry2]!; + if (type1 !== type2) { + throw new Error("Entry types must be the same"); + } + + const statement: AnyLessThan = { + entries: [name1, name2], + type: "lessThan", + }; + + const baseName = customStatementName ?? `${name1}_${name2}_lessThan`; + let statementName: string = baseName; + let suffix = 1; + + while ( + Object.prototype.hasOwnProperty.call(this.#spec.statements, statementName) + ) { + statementName = `${baseName}_${suffix++}`; + } + + return new UntypedPODGroupSpecBuilder({ + ...this.#spec, + statements: { + ...this.#spec.statements, + [statementName]: statement, + }, + }); + } + + public lessThanEq( + name1: PODName, + name2: PODName, + customStatementName?: string + ): UntypedPODGroupSpecBuilder { + // Check that both entries exist + const [pod1, entry1] = name1.split("."); + const [pod2, entry2] = name2.split("."); + if ( + pod1 === undefined || + entry1 === undefined || + !Object.prototype.hasOwnProperty.call(this.#spec.pods, pod1) || + !Object.prototype.hasOwnProperty.call( + this.#spec.pods[pod1]!.entries, + entry1 + ) + ) { + throw new Error(`Entry "${name1}" does not exist`); + } + if ( + pod2 === undefined || + entry2 === undefined || + !Object.prototype.hasOwnProperty.call(this.#spec.pods, pod2) || + !Object.prototype.hasOwnProperty.call( + this.#spec.pods[pod2]!.entries, + entry2 + ) + ) { + throw new Error(`Entry "${name2}" does not exist`); + } + + if (name1 === name2) { + throw new Error("Entry names must be different"); + } + + const type1 = this.#spec.pods[pod1]!.entries[entry1]!; + const type2 = this.#spec.pods[pod2]!.entries[entry2]!; + if (type1 !== type2) { + throw new Error("Entry types must be the same"); + } + + const statement: AnyLessThanEq = { + entries: [name1, name2], + type: "lessThanEq", + }; + + const baseName = customStatementName ?? `${name1}_${name2}_lessThanEq`; + let statementName: string = baseName; + let suffix = 1; + + while ( + Object.prototype.hasOwnProperty.call(this.#spec.statements, statementName) + ) { + statementName = `${baseName}_${suffix++}`; + } + + return new UntypedPODGroupSpecBuilder({ + ...this.#spec, + statements: { + ...this.#spec.statements, + [statementName]: statement, + }, + }); + } + + public equalsEntry( + name1: PODName, + name2: PODName, + customStatementName?: string + ): UntypedPODGroupSpecBuilder { + // Check that both entries exist + const [pod1, entry1] = name1.split("."); + const [pod2, entry2] = name2.split("."); + if ( + pod1 === undefined || + entry1 === undefined || + !Object.prototype.hasOwnProperty.call(this.#spec.pods, pod1) || + (!Object.prototype.hasOwnProperty.call( + this.#spec.pods[pod1]!.entries, + entry1 + ) && + !Object.prototype.hasOwnProperty.call(virtualEntries, entry1)) + ) { + throw new Error(`Entry "${name1}" does not exist`); + } + if ( + pod2 === undefined || + entry2 === undefined || + !Object.prototype.hasOwnProperty.call(this.#spec.pods, pod2) || + (!Object.prototype.hasOwnProperty.call( + this.#spec.pods[pod2]!.entries, + entry2 + ) && + !Object.prototype.hasOwnProperty.call(virtualEntries, entry2)) + ) { + throw new Error(`Entry "${name2}" does not exist`); + } + + if (name1 === name2) { + throw new Error("Entry names must be different"); + } + + const type1 = this.#spec.pods[pod1]!.entries[entry1]!; + const type2 = this.#spec.pods[pod2]!.entries[entry2]!; + if (type1 !== type2) { + throw new Error("Entry types must be the same"); + } + + const statement: AnyEqualsEntry = { + entries: [name1, name2], + type: "equalsEntry", + }; + + const baseName = customStatementName ?? `${name1}_${name2}_equalsEntry`; + let statementName: string = baseName; + let suffix = 1; + + while ( + Object.prototype.hasOwnProperty.call(this.#spec.statements, statementName) + ) { + statementName = `${baseName}_${suffix++}`; + } + + return new UntypedPODGroupSpecBuilder({ + ...this.#spec, + statements: { + ...this.#spec.statements, + [statementName]: statement, + }, + }); + } + + public notEqualsEntry( + name1: PODName, + name2: PODName, + customStatementName?: string + ): UntypedPODGroupSpecBuilder { + // Check that both entries exist + const [pod1, entry1] = name1.split("."); + const [pod2, entry2] = name2.split("."); + if ( + pod1 === undefined || + entry1 === undefined || + !Object.prototype.hasOwnProperty.call(this.#spec.pods, pod1) || + !Object.prototype.hasOwnProperty.call( + this.#spec.pods[pod1]!.entries, + entry1 + ) + ) { + throw new Error(`Entry "${name1}" does not exist`); + } + if ( + pod2 === undefined || + entry2 === undefined || + !Object.prototype.hasOwnProperty.call(this.#spec.pods, pod2) || + !Object.prototype.hasOwnProperty.call( + this.#spec.pods[pod2]!.entries, + entry2 + ) + ) { + throw new Error(`Entry "${name2}" does not exist`); + } + + if (name1 === name2) { + throw new Error("Entry names must be different"); + } + + const type1 = this.#spec.pods[pod1]!.entries[entry1]!; + const type2 = this.#spec.pods[pod2]!.entries[entry2]!; + if (type1 !== type2) { + throw new Error("Entry types must be the same"); + } + + const statement: AnyNotEqualsEntry = { + entries: [name1, name2], + type: "notEqualsEntry", + }; + + const baseName = customStatementName ?? `${name1}_${name2}_notEqualsEntry`; + let statementName: string = baseName; + let suffix = 1; + + while ( + Object.prototype.hasOwnProperty.call(this.#spec.statements, statementName) + ) { + statementName = `${baseName}_${suffix++}`; + } + + return new UntypedPODGroupSpecBuilder({ + ...this.#spec, + statements: { + ...this.#spec.statements, + [statementName]: statement, + }, + }); + } +} + +if (import.meta.vitest) { + const { describe, it } = import.meta.vitest; + + describe("PODGroupSpecBuilder", () => { + it("should be able to create a builder", () => { + const pod = PODSpecBuilder.create() + .entry("my_string", "string") + .entry("my_int", "int") + .entry("mystery_name", "string"); + + const _pod2 = PODSpecBuilder.create().entry("something_else", "boolean"); + + const _builder = UntypedPODGroupSpecBuilder.create() + .pod("foo", pod.spec()) + .pod("bar", pod.spec()) + .inRange("foo.my_int", { min: 0n, max: 10n }); + }); + }); +} + +if (import.meta.vitest) { + const { describe, it } = import.meta.vitest; + + describe("PODGroupSpecBuilder", () => { + it("should be able to create a builder", () => { + const pod = PODSpecBuilder.create() + .entry("my_string", "string") + .entry("my_int", "int") + .entry("mystery_name", "string"); + + const _builder = UntypedPODGroupSpecBuilder.create().pod( + "foo", + pod.spec() + ); + }); + }); +} diff --git a/packages/podspec/src/builders/untypedPod.ts b/packages/podspec/src/builders/untypedPod.ts new file mode 100644 index 0000000..fecef0f --- /dev/null +++ b/packages/podspec/src/builders/untypedPod.ts @@ -0,0 +1,855 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ +import { + type PODValue, + POD_DATE_MAX, + POD_DATE_MIN, + POD_INT_MAX, + POD_INT_MIN, + checkPODName, +} from "@pcd/pod"; +import type { IsJsonSafe } from "../shared/jsonSafe.js"; +import { virtualEntries } from "./pod.js"; +import { + convertValuesToStringTuples, + deepFreeze, + supportsRangeChecks, + validateRange, +} from "./shared.js"; +import type { EntryKeys, EntryTypes, PODValueType } from "./types/entries.js"; +import type { + EqualsEntry, + GreaterThan, + GreaterThanEq, + InRange, + IsMemberOf, + IsNotMemberOf, + LessThan, + LessThanEq, + NotEqualsEntry, + NotInRange, + StatementMap, +} from "./types/statements.js"; + +export type PODSpec = { + entries: E; + statements: S; +}; + +// This is a compile-time check that the PODSpec is JSON-safe +true satisfies IsJsonSafe>; + +export class UntypedPODSpecBuilder { + readonly #spec: PODSpec; + + public constructor(spec: PODSpec) { + this.#spec = spec; + } + + public static create() { + return new UntypedPODSpecBuilder({ + entries: {}, + statements: {}, + }); + } + + public spec(): PODSpec { + return deepFreeze(this.#spec); + } + + public toJSON(): string { + return JSON.stringify( + { + ...this.#spec, + }, + null, + 2 + ); + } + + public entry(key: string, type: PODValueType): UntypedPODSpecBuilder { + if (Object.prototype.hasOwnProperty.call(this.#spec.entries, key)) { + throw new Error(`Entry "${key}" already exists`); + } + + // Will throw if not a valid POD entry name + checkPODName(key); + + return new UntypedPODSpecBuilder({ + ...this.#spec, + entries: { + ...this.#spec.entries, + [key]: type, + }, + statements: this.#spec.statements, + }); + } + + public entries( + entries: NewEntries + ): UntypedPODSpecBuilder { + for (const entryName of Object.keys(entries)) { + if (Object.prototype.hasOwnProperty.call(this.#spec.entries, entryName)) { + throw new Error(`Entry "${entryName}" already exists`); + } + + // Will throw if not a valid POD entry name + checkPODName(entryName); + } + + return new UntypedPODSpecBuilder({ + ...this.#spec, + entries: { ...this.#spec.entries, ...entries }, + statements: this.#spec.statements, + }); + } + + /** + * Pick entries by key + */ + public pickEntries(keys: string[]): UntypedPODSpecBuilder { + return new UntypedPODSpecBuilder({ + entries: Object.fromEntries( + Object.entries(this.#spec.entries).filter(([key]) => keys.includes(key)) + ) as Pick, + statements: Object.fromEntries( + Object.entries(this.#spec.statements).filter(([_key, statement]) => { + return (statement.entries as EntryKeys).every((entry) => + keys.includes(entry) + ); + }) + ) as StatementMap, + }); + } + + public omitEntries(keys: string[]): UntypedPODSpecBuilder { + return new UntypedPODSpecBuilder({ + ...this.#spec, + entries: Object.fromEntries( + Object.entries(this.#spec.entries).filter( + ([key]) => !keys.includes(key) + ) + ), + statements: Object.fromEntries( + Object.entries(this.#spec.statements).filter(([_key, statement]) => { + return (statement.entries as EntryKeys).every( + (entry) => !keys.includes(entry) + ); + }) + ) as StatementMap, + }); + } + + public pickStatements(keys: string[]): UntypedPODSpecBuilder { + return new UntypedPODSpecBuilder({ + ...this.#spec, + statements: Object.fromEntries( + Object.entries(this.#spec.statements).filter(([key]) => + keys.includes(key) + ) + ) as StatementMap, + }); + } + + public omitStatements(keys: string[]): UntypedPODSpecBuilder { + return new UntypedPODSpecBuilder({ + ...this.#spec, + statements: Object.fromEntries( + Object.entries(this.#spec.statements).filter( + ([key]) => !keys.includes(key) + ) + ) as StatementMap, + }); + } + + /** + * Add a constraint that the entries must be a member of a list of tuples + * + * The names must be an array of one or more entry names for the POD. + * If there is only one name, then the values must be an array of PODValues + * of the type for that entry. + * + * If there are multiple names, then the values must be an array of one or + * more tuples, where each tuple is an array of PODValues of the type for + * each entry, in the order matching the names array. + * + * @param names - The names of the entries to be constrained + * @param values - The values to be constrained to + * @returns A new PODSpecBuilder with the statement added + */ + public isMemberOf( + names: string[], + values: PODValue["value"][] | PODValue["value"][][], + customStatementName?: string + ): UntypedPODSpecBuilder { + // Check for duplicate names + const uniqueNames = new Set(names); + if (uniqueNames.size !== names.length) { + throw new Error("Duplicate entry names are not allowed"); + } + + const allEntries = { + ...this.#spec.entries, + ...virtualEntries, + }; + + for (const name of names) { + if (!Object.prototype.hasOwnProperty.call(allEntries, name)) { + throw new Error(`Entry "${name}" does not exist`); + } + } + + /** + * We want type-safe inputs, but we want JSON-safe data to persist in the + * spec. So, we convert the input values to strings - because we have the + * POD type information, we can convert back to the correct type when we + * read the spec. + * + * For readability, we also have a special-case on inputs, where if there + * is only one entry being matched on, we accept a list in the form of + * value[] instead of [value][] - an array of plain values instead of an + * array of one-element tuples. When persisting, however, we convert these + * to one-element tuples since this makes reading the data out later more + * efficient. + */ + const statement: IsMemberOf = { + entries: names, + type: "isMemberOf", + isMemberOf: convertValuesToStringTuples(names, values, allEntries), + }; + + const baseName = customStatementName ?? `${names.join("_")}_isMemberOf`; + let statementName: string = baseName; + let suffix = 1; + + while ( + Object.prototype.hasOwnProperty.call(this.#spec.statements, statementName) + ) { + statementName = `${baseName}_${suffix++}`; + } + + return new UntypedPODSpecBuilder({ + ...this.#spec, + statements: { + ...this.#spec.statements, + [statementName]: statement, + }, + }); + } + + /** + * Add a constraint that the entries must not be a member of a list of tuples + * + * The names must be an array of one or more entry names for the POD. + * If there is only one name, then the values must be an array of PODValues + * of the type for that entry. + * + * If there are multiple names, then the values must be an array of one or + * more tuples, where each tuple is an array of PODValues of the type for + * each entry, in the order matching the names array. + * + * @param names - The names of the entries to be constrained + * @param values - The values to be constrained to + * @returns A new PODSpecBuilder with the statement added + */ + public isNotMemberOf( + names: string[], + values: PODValue["value"][] | PODValue["value"][][], + customStatementName?: string + ): UntypedPODSpecBuilder { + // Check that all names exist in entries + for (const name of names) { + if (!Object.prototype.hasOwnProperty.call(this.#spec.entries, name)) { + throw new Error(`Entry "${name}" does not exist`); + } + } + + // Check for duplicate names + const uniqueNames = new Set(names); + if (uniqueNames.size !== names.length) { + throw new Error("Duplicate entry names are not allowed"); + } + + const allEntries = { + ...this.#spec.entries, + ...virtualEntries, + }; + + for (const name of names) { + if (!Object.prototype.hasOwnProperty.call(allEntries, name)) { + throw new Error(`Entry "${name}" does not exist`); + } + } + + /** + * We want type-safe inputs, but we want JSON-safe data to persist in the + * spec. So, we convert the input values to strings - because we have the + * POD type information, we can convert back to the correct type when we + * read the spec. + * + * For readability, we also have a special-case on inputs, where if there + * is only one entry being matched on, we accept a list in the form of + * value[] instead of [value][] - an array of plain values instead of an + * array of one-element tuples. When persisting, however, we convert these + * to one-element tuples since this makes reading the data out later more + * efficient. + */ + const statement: IsNotMemberOf = { + entries: names, + type: "isNotMemberOf", + isNotMemberOf: convertValuesToStringTuples( + names, + values, + allEntries + ), + }; + + const baseName = customStatementName ?? `${names.join("_")}_isNotMemberOf`; + let statementName: string = baseName; + let suffix = 1; + + while ( + Object.prototype.hasOwnProperty.call(this.#spec.statements, statementName) + ) { + statementName = `${baseName}_${suffix++}`; + } + + return new UntypedPODSpecBuilder({ + ...this.#spec, + statements: { + ...this.#spec.statements, + [statementName]: statement, + }, + }); + } + + /** + * Add a constraint that the entry must be in a range + * + * @param name - The name of the entry to be constrained + * @param range - The range to be constrained to + * @returns A new PODSpecBuilder with the statement added + */ + public inRange( + name: string, + range: { + min: bigint | Date; + max: bigint | Date; + }, + customStatementName?: string + ): UntypedPODSpecBuilder { + // Check that the entry exists + if ( + !Object.prototype.hasOwnProperty.call(this.#spec.entries, name) && + !Object.prototype.hasOwnProperty.call(virtualEntries, name) + ) { + throw new Error(`Entry "${name}" does not exist`); + } + + const entryType = this.#spec.entries[name]!; + + if (!supportsRangeChecks(entryType)) { + throw new Error(`Entry "${name}" does not support range checks`); + } + + switch (entryType) { + case "int": + validateRange( + range.min as bigint, + range.max as bigint, + POD_INT_MIN, + POD_INT_MAX + ); + break; + case "boolean": + validateRange(range.min as bigint, range.max as bigint, 0n, 1n); + break; + case "date": + validateRange( + range.min as Date, + range.max as Date, + POD_DATE_MIN, + POD_DATE_MAX + ); + break; + default: + const _exhaustiveCheck: never = entryType; + throw new Error(`Unsupported entry type: ${name}`); + } + + const statement: InRange = { + entries: [name], + type: "inRange", + inRange: { + min: + range.min instanceof Date + ? range.min.getTime().toString() + : range.min.toString(), + max: + range.max instanceof Date + ? range.max.getTime().toString() + : range.max.toString(), + }, + }; + + const baseName = customStatementName ?? `${name}_inRange`; + let statementName: string = baseName; + let suffix = 1; + + while ( + Object.prototype.hasOwnProperty.call(this.#spec.statements, statementName) + ) { + statementName = `${baseName}_${suffix++}`; + } + + return new UntypedPODSpecBuilder({ + ...this.#spec, + statements: { + ...this.#spec.statements, + [statementName]: statement, + }, + }); + } + + /** + * Add a constraint that the entry must not be in a range + * + * @param name - The name of the entry to be constrained + * @param range - The range to be constrained to + * @returns A new PODSpecBuilder with the statement added + */ + public notInRange( + name: string, + range: { + min: bigint | Date; + max: bigint | Date; + }, + customStatementName?: string + ): UntypedPODSpecBuilder { + // Check that the entry exists + if ( + !Object.prototype.hasOwnProperty.call(this.#spec.entries, name) && + !Object.prototype.hasOwnProperty.call(virtualEntries, name) + ) { + throw new Error(`Entry "${name}" does not exist`); + } + + const entryType = this.#spec.entries[name]!; + + if (!supportsRangeChecks(entryType)) { + throw new Error(`Entry "${name}" does not support range checks`); + } + + // TODO repetition, consider moving to a utility function + switch (entryType) { + case "int": + validateRange( + range.min as bigint, + range.max as bigint, + POD_INT_MIN, + POD_INT_MAX + ); + break; + case "boolean": + validateRange(range.min as bigint, range.max as bigint, 0n, 1n); + break; + case "date": + validateRange( + range.min as Date, + range.max as Date, + POD_DATE_MIN, + POD_DATE_MAX + ); + break; + default: + const _exhaustiveCheck: never = entryType; + throw new Error(`Unsupported entry type: ${name}`); + } + + const statement: NotInRange = { + entries: [name], + type: "notInRange", + notInRange: { + min: + range.min instanceof Date + ? range.min.getTime().toString() + : range.min.toString(), + max: + range.max instanceof Date + ? range.max.getTime().toString() + : range.max.toString(), + }, + }; + + const baseName = customStatementName ?? `${name}_notInRange`; + let statementName: string = baseName; + let suffix = 1; + + while ( + Object.prototype.hasOwnProperty.call(this.#spec.statements, statementName) + ) { + statementName = `${baseName}_${suffix++}`; + } + + return new UntypedPODSpecBuilder({ + ...this.#spec, + statements: { + ...this.#spec.statements, + [statementName]: statement, + }, + }); + } + + public equalsEntry( + name1: string, + name2: string, + customStatementName?: string + ): UntypedPODSpecBuilder { + // Check that both names exist in entries + if ( + !Object.prototype.hasOwnProperty.call(this.#spec.entries, name1) && + !Object.prototype.hasOwnProperty.call(virtualEntries, name1) + ) { + throw new Error(`Entry "${name1}" does not exist`); + } + if ( + !Object.prototype.hasOwnProperty.call(this.#spec.entries, name2) && + !Object.prototype.hasOwnProperty.call(virtualEntries, name2) + ) { + throw new Error(`Entry "${name2}" does not exist`); + } + if (name1 === name2) { + throw new Error("Entry names must be different"); + } + if (this.#spec.entries[name1] !== this.#spec.entries[name2]) { + throw new Error("Entry types must be the same"); + } + + const statement = { + entries: [name1, name2], + type: "equalsEntry", + } satisfies EqualsEntry; + + const baseName = customStatementName ?? `${name1}_${name2}_equalsEntry`; + let statementName: string = baseName; + let suffix = 1; + + while ( + Object.prototype.hasOwnProperty.call(this.#spec.statements, statementName) + ) { + statementName = `${baseName}_${suffix++}`; + } + + return new UntypedPODSpecBuilder({ + ...this.#spec, + statements: { + ...this.#spec.statements, + [statementName]: statement, + }, + }); + } + + public notEqualsEntry( + name1: string, + name2: string, + customStatementName?: string + ): UntypedPODSpecBuilder { + // Check that both names exist in entries + if ( + !Object.prototype.hasOwnProperty.call(this.#spec.entries, name1) && + !Object.prototype.hasOwnProperty.call(virtualEntries, name1) + ) { + throw new Error(`Entry "${name1}" does not exist`); + } + if ( + !Object.prototype.hasOwnProperty.call(this.#spec.entries, name2) && + !Object.prototype.hasOwnProperty.call(virtualEntries, name2) + ) { + throw new Error(`Entry "${name2}" does not exist`); + } + if (name1 === name2) { + throw new Error("Entry names must be different"); + } + if (this.#spec.entries[name1] !== this.#spec.entries[name2]) { + throw new Error("Entry types must be the same"); + } + + const statement = { + entries: [name1, name2], + type: "notEqualsEntry", + } satisfies NotEqualsEntry; + + const baseName = customStatementName ?? `${name1}_${name2}_notEqualsEntry`; + let statementName: string = baseName; + let suffix = 1; + + while ( + Object.prototype.hasOwnProperty.call(this.#spec.statements, statementName) + ) { + statementName = `${baseName}_${suffix++}`; + } + + return new UntypedPODSpecBuilder({ + ...this.#spec, + statements: { + ...this.#spec.statements, + [statementName]: statement, + }, + }); + } + + public greaterThan( + name1: string, + name2: string, + customStatementName?: string + ): UntypedPODSpecBuilder { + // Check that both names exist in entries + if ( + !Object.prototype.hasOwnProperty.call(this.#spec.entries, name1) && + !Object.prototype.hasOwnProperty.call(virtualEntries, name1) + ) { + throw new Error(`Entry "${name1}" does not exist`); + } + if ( + !Object.prototype.hasOwnProperty.call(this.#spec.entries, name2) && + !Object.prototype.hasOwnProperty.call(virtualEntries, name2) + ) { + throw new Error(`Entry "${name2}" does not exist`); + } + if (name1 === name2) { + throw new Error("Entry names must be different"); + } + if (this.#spec.entries[name1] !== this.#spec.entries[name2]) { + throw new Error("Entry types must be the same"); + } + + const statement = { + entries: [name1, name2], + type: "greaterThan", + } satisfies GreaterThan; + + const baseName = customStatementName ?? `${name1}_${name2}_greaterThan`; + let statementName: string = baseName; + let suffix = 1; + + while ( + Object.prototype.hasOwnProperty.call(this.#spec.statements, statementName) + ) { + statementName = `${baseName}_${suffix++}`; + } + + return new UntypedPODSpecBuilder({ + ...this.#spec, + statements: { + ...this.#spec.statements, + [statementName]: statement, + }, + }); + } + + public greaterThanEq( + name1: string, + name2: string, + customStatementName?: string + ): UntypedPODSpecBuilder { + // Check that both names exist in entries + if ( + !Object.prototype.hasOwnProperty.call(this.#spec.entries, name1) && + !Object.prototype.hasOwnProperty.call(virtualEntries, name1) + ) { + throw new Error(`Entry "${name1}" does not exist`); + } + if ( + !Object.prototype.hasOwnProperty.call(this.#spec.entries, name2) && + !Object.prototype.hasOwnProperty.call(virtualEntries, name2) + ) { + throw new Error(`Entry "${name2}" does not exist`); + } + if (name1 === name2) { + throw new Error("Entry names must be different"); + } + if (this.#spec.entries[name1] !== this.#spec.entries[name2]) { + throw new Error("Entry types must be the same"); + } + + const statement = { + entries: [name1, name2], + type: "greaterThanEq", + } satisfies GreaterThanEq; + + const baseName = customStatementName ?? `${name1}_${name2}_greaterThanEq`; + let statementName: string = baseName; + let suffix = 1; + + while ( + Object.prototype.hasOwnProperty.call(this.#spec.statements, statementName) + ) { + statementName = `${baseName}_${suffix++}`; + } + + return new UntypedPODSpecBuilder({ + ...this.#spec, + statements: { + ...this.#spec.statements, + [statementName]: statement, + }, + }); + } + + public lessThan( + name1: string, + name2: string, + customStatementName?: string + ): UntypedPODSpecBuilder { + // Check that both names exist in entries + if ( + !Object.prototype.hasOwnProperty.call(this.#spec.entries, name2) && + !Object.prototype.hasOwnProperty.call(virtualEntries, name2) + ) { + throw new Error(`Entry "${name2}" does not exist`); + } + if (name1 === name2) { + throw new Error("Entry names must be different"); + } + if (this.#spec.entries[name1] !== this.#spec.entries[name2]) { + throw new Error("Entry types must be the same"); + } + + const statement = { + entries: [name1, name2], + type: "lessThan", + } satisfies LessThan; + + const baseName = customStatementName ?? `${name1}_${name2}_lessThan`; + let statementName: string = baseName; + let suffix = 1; + + while ( + Object.prototype.hasOwnProperty.call(this.#spec.statements, statementName) + ) { + statementName = `${baseName}_${suffix++}`; + } + + return new UntypedPODSpecBuilder({ + ...this.#spec, + statements: { + ...this.#spec.statements, + [statementName]: statement, + }, + }); + } + + public lessThanEq( + name1: string, + name2: string, + customStatementName?: string + ): UntypedPODSpecBuilder { + // Check that both names exist in entries + if ( + !Object.prototype.hasOwnProperty.call(this.#spec.entries, name1) && + !Object.prototype.hasOwnProperty.call(virtualEntries, name1) + ) { + throw new Error(`Entry "${name1}" does not exist`); + } + if ( + !Object.prototype.hasOwnProperty.call(this.#spec.entries, name2) && + !Object.prototype.hasOwnProperty.call(virtualEntries, name2) + ) { + throw new Error(`Entry "${name2}" does not exist`); + } + if (name1 === name2) { + throw new Error("Entry names must be different"); + } + if (this.#spec.entries[name1] !== this.#spec.entries[name2]) { + throw new Error("Entry types must be the same"); + } + + const statement = { + entries: [name1, name2], + type: "lessThanEq", + } satisfies LessThanEq; + + const baseName = customStatementName ?? `${name1}_${name2}_lessThanEq`; + let statementName: string = baseName; + let suffix = 1; + + while ( + Object.prototype.hasOwnProperty.call(this.#spec.statements, statementName) + ) { + statementName = `${baseName}_${suffix++}`; + } + + return new UntypedPODSpecBuilder({ + ...this.#spec, + statements: { + ...this.#spec.statements, + [statementName]: statement, + }, + }); + } +} + +if (import.meta.vitest) { + const { describe, it, expect } = import.meta.vitest; + + describe("PODSpecBuilder", () => { + it("should be able to create a builder", () => { + const builder = UntypedPODSpecBuilder.create(); + expect(builder).toBeDefined(); + + const builderWithEntries = builder + .entry("my_string", "string") + .entry("my_int", "int") + .entry("my_cryptographic", "cryptographic") + .entry("my_bytes", "bytes") + .entry("my_date", "date") + .entry("my_null", "null") + .entry("my_eddsa_pubkey", "eddsa_pubkey") + .entry("my_other_string", "string") + .entry("my_other_int", "int"); + + expect(builderWithEntries.spec().entries).toEqual({ + my_string: "string", + my_int: "int", + my_cryptographic: "cryptographic", + my_bytes: "bytes", + my_date: "date", + my_null: "null", + my_eddsa_pubkey: "eddsa_pubkey", + my_other_string: "string", + my_other_int: "int", + }); + + const builderWithStatements = builderWithEntries + .inRange("my_int", { min: 0n, max: 10n }) + .inRange("my_date", { + min: new Date("2020-01-01"), + max: new Date("2020-01-10"), + }) + .isMemberOf(["my_string"], ["foo", "bar"]) + .isNotMemberOf(["my_string"], ["baz"]) + .equalsEntry("my_string", "my_other_string") + .notEqualsEntry("my_int", "my_other_int") + // TODO At some point, some of these should throw because they cannot + // possibly all be true. + .greaterThan("my_int", "my_other_int") + .greaterThanEq("my_int", "my_other_int") + .lessThan("my_int", "my_other_int") + .lessThanEq("my_int", "my_other_int"); + + expect(Object.keys(builderWithStatements.spec().statements)).toEqual([ + "my_int_inRange", + "my_date_inRange", + "my_string_isMemberOf", + "my_string_isNotMemberOf", + "my_string_my_other_string_equalsEntry", + "my_int_my_other_int_notEqualsEntry", + "my_int_my_other_int_greaterThan", + "my_int_my_other_int_greaterThanEq", + "my_int_my_other_int_lessThan", + "my_int_my_other_int_lessThanEq", + ]); + + builderWithEntries.isMemberOf(["non_existent_entry"], ["foo", "bar"]); + }); + }); +} diff --git a/packages/podspec/src/data.ts b/packages/podspec/src/data.ts deleted file mode 100644 index c9abeed..0000000 --- a/packages/podspec/src/data.ts +++ /dev/null @@ -1,10 +0,0 @@ -import type { POD } from "@pcd/pod"; -import type { PODData } from "./parse/pod.js"; - -export function podToPODData(pod: POD): PODData { - return { - entries: pod.content.asEntries(), - signature: pod.signature, - signerPublicKey: pod.signerPublicKey - }; -} diff --git a/packages/podspec/src/error.ts b/packages/podspec/src/error.ts deleted file mode 100644 index a6fc8be..0000000 --- a/packages/podspec/src/error.ts +++ /dev/null @@ -1,184 +0,0 @@ -import type { PODValue } from "@pcd/pod"; - -/** - * Enum of all the possible issues that can occur when validating a POD - * against a Podspec. - */ -export enum IssueCode { - invalid_type = "invalid_type", - not_in_list = "not_in_list", - excluded_by_list = "excluded_by_list", - not_in_range = "not_in_range", - missing_entry = "missing_entry", - invalid_entry_name = "invalid_entry_name", - invalid_tuple_entry = "invalid_tuple_entry", - not_in_tuple_list = "not_in_tuple_list", - excluded_by_tuple_list = "excluded_by_tuple_list", - signer_not_in_list = "signer_not_in_list", - signer_excluded_by_list = "signer_excluded_by_list", - signature_not_in_list = "signature_not_in_list", - signature_excluded_by_list = "signature_excluded_by_list", - invalid_pod_value = "invalid_pod_value", - unexpected_input_entry = "unexpected_input_entry" -} - -/** - * Base interface for all issues that can occur when validating a POD - * against a Podspec. - */ -export interface PodspecBaseIssue { - message?: string; - path: (string | number)[]; - code: IssueCode; -} - -/** - * Issue that occurs when an input value is of an invalid type. - */ -export interface PodspecInvalidTypeIssue extends PodspecBaseIssue { - code: IssueCode.invalid_type; - expectedType: PODValue["type"] | "PODEntries"; -} - -/** - * Issue that occurs when an input value is not in a list of allowed values. - */ -export interface PodspecNotInListIssue extends PodspecBaseIssue { - code: IssueCode.not_in_list; - value: PODValue; - list: PODValue[]; -} - -/** - * Issue that occurs when an input value is excluded by a list of allowed values. - */ -export interface PodspecExcludedByListIssue extends PodspecBaseIssue { - code: IssueCode.excluded_by_list; - value: PODValue; - list: PODValue[]; -} - -/** - * Issue that occurs when an input value is not in a range of allowed values. - */ -export interface PodspecNotInRangeIssue extends PodspecBaseIssue { - code: IssueCode.not_in_range; - value: bigint; - min: bigint; - max: bigint; -} - -/** - * Issue that occurs when an input value is missing from the PODEntries. - */ -export interface PodspecMissingEntryIssue extends PodspecBaseIssue { - code: IssueCode.missing_entry; - key: string; -} - -/** - * Issue that occurs when an input value has an invalid entry name. - */ -export interface PodspecInvalidEntryNameIssue extends PodspecBaseIssue { - code: IssueCode.invalid_entry_name; - name: string; - description: string; -} - -/** - * Issue that occurs when an invalid entry is specified as part of a tuple, - * e.g. because the specified entry does not exist. - */ -export interface PodspecInvalidTupleEntryIssue extends PodspecBaseIssue { - code: IssueCode.invalid_tuple_entry; - name: string; -} - -/** - * Issue that occurs when an input value is not in a list of allowed values. - */ -export interface PodspecNotInTupleListIssue extends PodspecBaseIssue { - code: IssueCode.not_in_tuple_list; - value: PODValue[]; - list: PODValue[][]; -} - -/** - * Issue that occurs when an input value is excluded by a list of allowed values. - */ -export interface PodspecExcludedByTupleListIssue extends PodspecBaseIssue { - code: IssueCode.excluded_by_tuple_list; - value: PODValue[]; - list: PODValue[][]; -} - -/** - * Issue that occurs when a signer public key is not in a list of allowed values. - */ -export interface PodspecSignerNotInListIssue extends PodspecBaseIssue { - code: IssueCode.signer_not_in_list; - signer: string; - list: string[]; -} - -/** - * Issue that occurs when a signer public key is excluded by a list of allowed values. - */ -export interface PodspecSignerExcludedByListIssue extends PodspecBaseIssue { - code: IssueCode.signer_excluded_by_list; - signer: string; - list: string[]; -} - -/** - * Issue that occurs when a signature is not in a list of allowed values. - */ -export interface PodspecSignatureNotInListIssue extends PodspecBaseIssue { - code: IssueCode.signature_not_in_list; - signature: string; - list: string[]; -} - -/** - * Issue that occurs when a signature is excluded by a list of allowed values. - */ -export interface PodspecSignatureExcludedByListIssue extends PodspecBaseIssue { - code: IssueCode.signature_excluded_by_list; - signature: string; - list: string[]; -} - -/** - * Issue that occurs when a POD value is invalid. - */ -export interface PodspecInvalidPodValueIssue extends PodspecBaseIssue { - code: IssueCode.invalid_pod_value; - value: PODValue; - reason: string; -} - -/** - * Issue that occurs when an unexpected entry is encountered. - * Only relevant for "strict" parsing modes. - */ -export interface PodspecUnexpectedInputEntryIssue extends PodspecBaseIssue { - code: IssueCode.unexpected_input_entry; - name: string; -} - -/** - * Exception class for errors that occur when parsing. - */ -export class PodspecError extends Error { - issues: PodspecBaseIssue[] = []; - - public get errors(): PodspecBaseIssue[] { - return this.issues; - } - - constructor(issues: PodspecBaseIssue[]) { - super(); - this.name = "PodspecError"; - this.issues = issues; - } -} diff --git a/packages/podspec/src/generated/podspec.ts b/packages/podspec/src/generated/podspec.ts new file mode 100644 index 0000000..4616886 --- /dev/null +++ b/packages/podspec/src/generated/podspec.ts @@ -0,0 +1,2449 @@ +import * as __typia_transform__assertGuard from "typia/lib/internal/_assertGuard.js"; +import * as __typia_transform__accessExpressionAsString from "typia/lib/internal/_accessExpressionAsString.js"; +import typia from "typia"; +import type { NamedPODSpecs } from "../builders/group.js"; +import type { PODGroupSpec } from "../builders/group.js"; +import type { PODSpec } from "../builders/pod.js"; +import type { EntryTypes } from "../builders/types/entries.js"; +import type { StatementMap } from "../builders/types/statements.js"; +export const assertPODSpec = (() => { + const _io0 = (input: any): boolean => + "object" === typeof input.entries && + null !== input.entries && + false === Array.isArray(input.entries) && + _io1(input.entries) && + "object" === typeof input.statements && + null !== input.statements && + false === Array.isArray(input.statements) && + _io2(input.statements); + const _io1 = (input: any): boolean => + Object.keys(input).every((key: any) => { + const value = input[key]; + if (undefined === value) return true; + return ( + "string" === value || + "boolean" === value || + "bytes" === value || + "cryptographic" === value || + "int" === value || + "eddsa_pubkey" === value || + "date" === value || + "null" === value + ); + }); + const _io2 = (input: any): boolean => + Object.keys(input).every((key: any) => { + const value = input[key]; + if (undefined === value) return true; + return "object" === typeof value && null !== value && _iu0(value); + }); + const _io3 = (input: any): boolean => + Array.isArray(input.entries) && + input.entries.every((elem: any) => "string" === typeof elem) && + "isMemberOf" === input.type && + Array.isArray(input.isMemberOf) && + input.isMemberOf.every( + (elem: any) => + Array.isArray(elem) && + elem.every((elem: any) => "string" === typeof elem) + ); + const _io4 = (input: any): boolean => + Array.isArray(input.entries) && + input.entries.every((elem: any) => "string" === typeof elem) && + "isNotMemberOf" === input.type && + Array.isArray(input.isNotMemberOf) && + input.isNotMemberOf.every( + (elem: any) => + Array.isArray(elem) && + elem.every((elem: any) => "string" === typeof elem) + ); + const _io5 = (input: any): boolean => + Array.isArray(input.entries) && + input.entries.length === 1 && + "string" === typeof input.entries[0] && + "inRange" === input.type && + "object" === typeof input.inRange && null !== input.inRange && + _io6(input.inRange); + const _io6 = (input: any): boolean => + "string" === typeof input.min && "string" === typeof input.max; + const _io7 = (input: any): boolean => + Array.isArray(input.entries) && + input.entries.length === 1 && + "string" === typeof input.entries[0] && + "notInRange" === input.type && + "object" === typeof input.notInRange && null !== input.notInRange && + _io6(input.notInRange); + const _io8 = (input: any): boolean => + Array.isArray(input.entries) && + input.entries.length === 2 && "string" === typeof input.entries[0] && + "string" === typeof input.entries[1] && + "equalsEntry" === input.type; + const _io9 = (input: any): boolean => + Array.isArray(input.entries) && + input.entries.length === 2 && "string" === typeof input.entries[0] && + "string" === typeof input.entries[1] && + "notEqualsEntry" === input.type; + const _io10 = (input: any): boolean => + Array.isArray(input.entries) && + input.entries.length === 2 && "string" === typeof input.entries[0] && + "string" === typeof input.entries[1] && + "greaterThan" === input.type; + const _io11 = (input: any): boolean => + Array.isArray(input.entries) && + input.entries.length === 2 && "string" === typeof input.entries[0] && + "string" === typeof input.entries[1] && + "greaterThanEq" === input.type; + const _io12 = (input: any): boolean => + Array.isArray(input.entries) && + input.entries.length === 2 && "string" === typeof input.entries[0] && + "string" === typeof input.entries[1] && + "lessThan" === input.type; + const _io13 = (input: any): boolean => + Array.isArray(input.entries) && + input.entries.length === 2 && "string" === typeof input.entries[0] && + "string" === typeof input.entries[1] && + "lessThanEq" === input.type; + const _iu0 = (input: any): any => + (() => { + if ("isMemberOf" === input.type) return _io3(input); + else if ("isNotMemberOf" === input.type) return _io4(input); + else if ("inRange" === input.type) return _io5(input); + else if ("notInRange" === input.type) return _io7(input); + else if ("lessThanEq" === input.type) return _io13(input); + else if ("lessThan" === input.type) return _io12(input); + else if ("greaterThanEq" === input.type) return _io11(input); + else if ("greaterThan" === input.type) return _io10(input); + else if ("notEqualsEntry" === input.type) return _io9(input); + else if ("equalsEntry" === input.type) return _io8(input); + else return false; + })(); + const _ao0 = ( + input: any, + _path: string, + _exceptionable: boolean = true + ): boolean => + (((("object" === typeof input.entries && + null !== input.entries && + false === Array.isArray(input.entries)) || + __typia_transform__assertGuard._assertGuard( + _exceptionable, + { + method: "typia.createAssert", + path: _path + ".entries", + expected: "EntryTypes", + value: input.entries, + }, + _errorFactory + )) && + _ao1(input.entries, _path + ".entries", true && _exceptionable)) || + __typia_transform__assertGuard._assertGuard( + _exceptionable, + { + method: "typia.createAssert", + path: _path + ".entries", + expected: "EntryTypes", + value: input.entries, + }, + _errorFactory + )) && + (((("object" === typeof input.statements && + null !== input.statements && + false === Array.isArray(input.statements)) || + __typia_transform__assertGuard._assertGuard( + _exceptionable, + { + method: "typia.createAssert", + path: _path + ".statements", + expected: "StatementMap", + value: input.statements, + }, + _errorFactory + )) && + _ao2(input.statements, _path + ".statements", true && _exceptionable)) || + __typia_transform__assertGuard._assertGuard( + _exceptionable, + { + method: "typia.createAssert", + path: _path + ".statements", + expected: "StatementMap", + value: input.statements, + }, + _errorFactory + )); + const _ao1 = ( + input: any, + _path: string, + _exceptionable: boolean = true + ): boolean => + false === _exceptionable || + Object.keys(input).every((key: any) => { + const value = input[key]; + if (undefined === value) return true; + return ( + "string" === value || + "boolean" === value || + "bytes" === value || + "cryptographic" === value || + "int" === value || + "eddsa_pubkey" === value || + "date" === value || + "null" === value || + __typia_transform__assertGuard._assertGuard( + _exceptionable, + { + method: "typia.createAssert", + path: + _path + + __typia_transform__accessExpressionAsString._accessExpressionAsString( + key + ), + expected: + '("boolean" | "bytes" | "cryptographic" | "date" | "eddsa_pubkey" | "int" | "null" | "string")', + value: value, + }, + _errorFactory + ) + ); + }); + const _ao2 = ( + input: any, + _path: string, + _exceptionable: boolean = true + ): boolean => + false === _exceptionable || + Object.keys(input).every((key: any) => { + const value = input[key]; + if (undefined === value) return true; + return ( + ((("object" === typeof value && null !== value) || + __typia_transform__assertGuard._assertGuard( + _exceptionable, + { + method: "typia.createAssert", + path: + _path + + __typia_transform__accessExpressionAsString._accessExpressionAsString( + key + ), + expected: + "(EqualsEntry | GreaterThan | GreaterThanEq | InRange | IsMemberOf> | IsNotMemberOf> | LessThan | LessThanEq | NotEqualsEntry | NotInRange)", + value: value, + }, + _errorFactory + )) && + _au0( + value, + _path + + __typia_transform__accessExpressionAsString._accessExpressionAsString( + key + ), + true && _exceptionable + )) || + __typia_transform__assertGuard._assertGuard( + _exceptionable, + { + method: "typia.createAssert", + path: + _path + + __typia_transform__accessExpressionAsString._accessExpressionAsString( + key + ), + expected: + "(EqualsEntry | GreaterThan | GreaterThanEq | InRange | IsMemberOf> | IsNotMemberOf> | LessThan | LessThanEq | NotEqualsEntry | NotInRange)", + value: value, + }, + _errorFactory + ) + ); + }); + const _ao3 = ( + input: any, + _path: string, + _exceptionable: boolean = true + ): boolean => + (((Array.isArray(input.entries) || + __typia_transform__assertGuard._assertGuard( + _exceptionable, + { + method: "typia.createAssert", + path: _path + ".entries", + expected: "Array", + value: input.entries, + }, + _errorFactory + )) && + input.entries.every( + (elem: any, _index7: number) => + "string" === typeof elem || + __typia_transform__assertGuard._assertGuard( + _exceptionable, + { + method: "typia.createAssert", + path: _path + ".entries[" + _index7 + "]", + expected: "string", + value: elem, + }, + _errorFactory + ) + )) || + __typia_transform__assertGuard._assertGuard( + _exceptionable, + { + method: "typia.createAssert", + path: _path + ".entries", + expected: "Array", + value: input.entries, + }, + _errorFactory + )) && + ("isMemberOf" === input.type || + __typia_transform__assertGuard._assertGuard( + _exceptionable, + { + method: "typia.createAssert", + path: _path + ".type", + expected: '"isMemberOf"', + value: input.type, + }, + _errorFactory + )) && + (((Array.isArray(input.isMemberOf) || + __typia_transform__assertGuard._assertGuard( + _exceptionable, + { + method: "typia.createAssert", + path: _path + ".isMemberOf", + expected: "Array>", + value: input.isMemberOf, + }, + _errorFactory + )) && + input.isMemberOf.every( + (elem: any, _index8: number) => + ((Array.isArray(elem) || + __typia_transform__assertGuard._assertGuard( + _exceptionable, + { + method: "typia.createAssert", + path: _path + ".isMemberOf[" + _index8 + "]", + expected: "Array", + value: elem, + }, + _errorFactory + )) && + elem.every( + (elem: any, _index9: number) => + "string" === typeof elem || + __typia_transform__assertGuard._assertGuard( + _exceptionable, + { + method: "typia.createAssert", + path: + _path + ".isMemberOf[" + _index8 + "][" + _index9 + "]", + expected: "string", + value: elem, + }, + _errorFactory + ) + )) || + __typia_transform__assertGuard._assertGuard( + _exceptionable, + { + method: "typia.createAssert", + path: _path + ".isMemberOf[" + _index8 + "]", + expected: "Array", + value: elem, + }, + _errorFactory + ) + )) || + __typia_transform__assertGuard._assertGuard( + _exceptionable, + { + method: "typia.createAssert", + path: _path + ".isMemberOf", + expected: "Array>", + value: input.isMemberOf, + }, + _errorFactory + )); + const _ao4 = ( + input: any, + _path: string, + _exceptionable: boolean = true + ): boolean => + (((Array.isArray(input.entries) || + __typia_transform__assertGuard._assertGuard( + _exceptionable, + { + method: "typia.createAssert", + path: _path + ".entries", + expected: "Array", + value: input.entries, + }, + _errorFactory + )) && + input.entries.every( + (elem: any, _index10: number) => + "string" === typeof elem || + __typia_transform__assertGuard._assertGuard( + _exceptionable, + { + method: "typia.createAssert", + path: _path + ".entries[" + _index10 + "]", + expected: "string", + value: elem, + }, + _errorFactory + ) + )) || + __typia_transform__assertGuard._assertGuard( + _exceptionable, + { + method: "typia.createAssert", + path: _path + ".entries", + expected: "Array", + value: input.entries, + }, + _errorFactory + )) && + ("isNotMemberOf" === input.type || + __typia_transform__assertGuard._assertGuard( + _exceptionable, + { + method: "typia.createAssert", + path: _path + ".type", + expected: '"isNotMemberOf"', + value: input.type, + }, + _errorFactory + )) && + (((Array.isArray(input.isNotMemberOf) || + __typia_transform__assertGuard._assertGuard( + _exceptionable, + { + method: "typia.createAssert", + path: _path + ".isNotMemberOf", + expected: "Array>", + value: input.isNotMemberOf, + }, + _errorFactory + )) && + input.isNotMemberOf.every( + (elem: any, _index11: number) => + ((Array.isArray(elem) || + __typia_transform__assertGuard._assertGuard( + _exceptionable, + { + method: "typia.createAssert", + path: _path + ".isNotMemberOf[" + _index11 + "]", + expected: "Array", + value: elem, + }, + _errorFactory + )) && + elem.every( + (elem: any, _index12: number) => + "string" === typeof elem || + __typia_transform__assertGuard._assertGuard( + _exceptionable, + { + method: "typia.createAssert", + path: + _path + + ".isNotMemberOf[" + + _index11 + + "][" + + _index12 + + "]", + expected: "string", + value: elem, + }, + _errorFactory + ) + )) || + __typia_transform__assertGuard._assertGuard( + _exceptionable, + { + method: "typia.createAssert", + path: _path + ".isNotMemberOf[" + _index11 + "]", + expected: "Array", + value: elem, + }, + _errorFactory + ) + )) || + __typia_transform__assertGuard._assertGuard( + _exceptionable, + { + method: "typia.createAssert", + path: _path + ".isNotMemberOf", + expected: "Array>", + value: input.isNotMemberOf, + }, + _errorFactory + )); + const _ao5 = ( + input: any, + _path: string, + _exceptionable: boolean = true + ): boolean => + (((Array.isArray(input.entries) || + __typia_transform__assertGuard._assertGuard( + _exceptionable, + { + method: "typia.createAssert", + path: _path + ".entries", + expected: "[entry: string]", + value: input.entries, + }, + _errorFactory + )) && + (input.entries.length === 1 || + __typia_transform__assertGuard._assertGuard( + _exceptionable, + { + method: "typia.createAssert", + path: _path + ".entries", + expected: "[string]", + value: input.entries, + }, + _errorFactory + )) && + ("string" === typeof input.entries[0] || + __typia_transform__assertGuard._assertGuard( + _exceptionable, + { + method: "typia.createAssert", + path: _path + ".entries[0]", + expected: "string", + value: input.entries[0], + }, + _errorFactory + ))) || + __typia_transform__assertGuard._assertGuard( + _exceptionable, + { + method: "typia.createAssert", + path: _path + ".entries", + expected: "[entry: string]", + value: input.entries, + }, + _errorFactory + )) && + ("inRange" === input.type || + __typia_transform__assertGuard._assertGuard( + _exceptionable, + { + method: "typia.createAssert", + path: _path + ".type", + expected: '"inRange"', + value: input.type, + }, + _errorFactory + )) && + (((("object" === typeof input.inRange && null !== input.inRange) || + __typia_transform__assertGuard._assertGuard( + _exceptionable, + { + method: "typia.createAssert", + path: _path + ".inRange", + expected: "RangePersistent", + value: input.inRange, + }, + _errorFactory + )) && + _ao6(input.inRange, _path + ".inRange", true && _exceptionable)) || + __typia_transform__assertGuard._assertGuard( + _exceptionable, + { + method: "typia.createAssert", + path: _path + ".inRange", + expected: "RangePersistent", + value: input.inRange, + }, + _errorFactory + )); + const _ao6 = ( + input: any, + _path: string, + _exceptionable: boolean = true + ): boolean => + ("string" === typeof input.min || + __typia_transform__assertGuard._assertGuard( + _exceptionable, + { + method: "typia.createAssert", + path: _path + ".min", + expected: "string", + value: input.min, + }, + _errorFactory + )) && + ("string" === typeof input.max || + __typia_transform__assertGuard._assertGuard( + _exceptionable, + { + method: "typia.createAssert", + path: _path + ".max", + expected: "string", + value: input.max, + }, + _errorFactory + )); + const _ao7 = ( + input: any, + _path: string, + _exceptionable: boolean = true + ): boolean => + (((Array.isArray(input.entries) || + __typia_transform__assertGuard._assertGuard( + _exceptionable, + { + method: "typia.createAssert", + path: _path + ".entries", + expected: "[entry: string].o1", + value: input.entries, + }, + _errorFactory + )) && + (input.entries.length === 1 || + __typia_transform__assertGuard._assertGuard( + _exceptionable, + { + method: "typia.createAssert", + path: _path + ".entries", + expected: "[string]", + value: input.entries, + }, + _errorFactory + )) && + ("string" === typeof input.entries[0] || + __typia_transform__assertGuard._assertGuard( + _exceptionable, + { + method: "typia.createAssert", + path: _path + ".entries[0]", + expected: "string", + value: input.entries[0], + }, + _errorFactory + ))) || + __typia_transform__assertGuard._assertGuard( + _exceptionable, + { + method: "typia.createAssert", + path: _path + ".entries", + expected: "[entry: string].o1", + value: input.entries, + }, + _errorFactory + )) && + ("notInRange" === input.type || + __typia_transform__assertGuard._assertGuard( + _exceptionable, + { + method: "typia.createAssert", + path: _path + ".type", + expected: '"notInRange"', + value: input.type, + }, + _errorFactory + )) && + (((("object" === typeof input.notInRange && null !== input.notInRange) || + __typia_transform__assertGuard._assertGuard( + _exceptionable, + { + method: "typia.createAssert", + path: _path + ".notInRange", + expected: "RangePersistent", + value: input.notInRange, + }, + _errorFactory + )) && + _ao6(input.notInRange, _path + ".notInRange", true && _exceptionable)) || + __typia_transform__assertGuard._assertGuard( + _exceptionable, + { + method: "typia.createAssert", + path: _path + ".notInRange", + expected: "RangePersistent", + value: input.notInRange, + }, + _errorFactory + )); + const _ao8 = ( + input: any, + _path: string, + _exceptionable: boolean = true + ): boolean => + (((Array.isArray(input.entries) || + __typia_transform__assertGuard._assertGuard( + _exceptionable, + { + method: "typia.createAssert", + path: _path + ".entries", + expected: "[entry: string, otherEntry: string]", + value: input.entries, + }, + _errorFactory + )) && + (input.entries.length === 2 || + __typia_transform__assertGuard._assertGuard( + _exceptionable, + { + method: "typia.createAssert", + path: _path + ".entries", + expected: "[string, string]", + value: input.entries, + }, + _errorFactory + )) && + ("string" === typeof input.entries[0] || + __typia_transform__assertGuard._assertGuard( + _exceptionable, + { + method: "typia.createAssert", + path: _path + ".entries[0]", + expected: "string", + value: input.entries[0], + }, + _errorFactory + )) && + ("string" === typeof input.entries[1] || + __typia_transform__assertGuard._assertGuard( + _exceptionable, + { + method: "typia.createAssert", + path: _path + ".entries[1]", + expected: "string", + value: input.entries[1], + }, + _errorFactory + ))) || + __typia_transform__assertGuard._assertGuard( + _exceptionable, + { + method: "typia.createAssert", + path: _path + ".entries", + expected: "[entry: string, otherEntry: string]", + value: input.entries, + }, + _errorFactory + )) && + ("equalsEntry" === input.type || + __typia_transform__assertGuard._assertGuard( + _exceptionable, + { + method: "typia.createAssert", + path: _path + ".type", + expected: '"equalsEntry"', + value: input.type, + }, + _errorFactory + )); + const _ao9 = ( + input: any, + _path: string, + _exceptionable: boolean = true + ): boolean => + (((Array.isArray(input.entries) || + __typia_transform__assertGuard._assertGuard( + _exceptionable, + { + method: "typia.createAssert", + path: _path + ".entries", + expected: "[entry: string, otherEntry: string].o1", + value: input.entries, + }, + _errorFactory + )) && + (input.entries.length === 2 || + __typia_transform__assertGuard._assertGuard( + _exceptionable, + { + method: "typia.createAssert", + path: _path + ".entries", + expected: "[string, string]", + value: input.entries, + }, + _errorFactory + )) && + ("string" === typeof input.entries[0] || + __typia_transform__assertGuard._assertGuard( + _exceptionable, + { + method: "typia.createAssert", + path: _path + ".entries[0]", + expected: "string", + value: input.entries[0], + }, + _errorFactory + )) && + ("string" === typeof input.entries[1] || + __typia_transform__assertGuard._assertGuard( + _exceptionable, + { + method: "typia.createAssert", + path: _path + ".entries[1]", + expected: "string", + value: input.entries[1], + }, + _errorFactory + ))) || + __typia_transform__assertGuard._assertGuard( + _exceptionable, + { + method: "typia.createAssert", + path: _path + ".entries", + expected: "[entry: string, otherEntry: string].o1", + value: input.entries, + }, + _errorFactory + )) && + ("notEqualsEntry" === input.type || + __typia_transform__assertGuard._assertGuard( + _exceptionable, + { + method: "typia.createAssert", + path: _path + ".type", + expected: '"notEqualsEntry"', + value: input.type, + }, + _errorFactory + )); + const _ao10 = ( + input: any, + _path: string, + _exceptionable: boolean = true + ): boolean => + (((Array.isArray(input.entries) || + __typia_transform__assertGuard._assertGuard( + _exceptionable, + { + method: "typia.createAssert", + path: _path + ".entries", + expected: "[entry: string, otherEntry: string].o2", + value: input.entries, + }, + _errorFactory + )) && + (input.entries.length === 2 || + __typia_transform__assertGuard._assertGuard( + _exceptionable, + { + method: "typia.createAssert", + path: _path + ".entries", + expected: "[string, string]", + value: input.entries, + }, + _errorFactory + )) && + ("string" === typeof input.entries[0] || + __typia_transform__assertGuard._assertGuard( + _exceptionable, + { + method: "typia.createAssert", + path: _path + ".entries[0]", + expected: "string", + value: input.entries[0], + }, + _errorFactory + )) && + ("string" === typeof input.entries[1] || + __typia_transform__assertGuard._assertGuard( + _exceptionable, + { + method: "typia.createAssert", + path: _path + ".entries[1]", + expected: "string", + value: input.entries[1], + }, + _errorFactory + ))) || + __typia_transform__assertGuard._assertGuard( + _exceptionable, + { + method: "typia.createAssert", + path: _path + ".entries", + expected: "[entry: string, otherEntry: string].o2", + value: input.entries, + }, + _errorFactory + )) && + ("greaterThan" === input.type || + __typia_transform__assertGuard._assertGuard( + _exceptionable, + { + method: "typia.createAssert", + path: _path + ".type", + expected: '"greaterThan"', + value: input.type, + }, + _errorFactory + )); + const _ao11 = ( + input: any, + _path: string, + _exceptionable: boolean = true + ): boolean => + (((Array.isArray(input.entries) || + __typia_transform__assertGuard._assertGuard( + _exceptionable, + { + method: "typia.createAssert", + path: _path + ".entries", + expected: "[entry: string, otherEntry: string].o3", + value: input.entries, + }, + _errorFactory + )) && + (input.entries.length === 2 || + __typia_transform__assertGuard._assertGuard( + _exceptionable, + { + method: "typia.createAssert", + path: _path + ".entries", + expected: "[string, string]", + value: input.entries, + }, + _errorFactory + )) && + ("string" === typeof input.entries[0] || + __typia_transform__assertGuard._assertGuard( + _exceptionable, + { + method: "typia.createAssert", + path: _path + ".entries[0]", + expected: "string", + value: input.entries[0], + }, + _errorFactory + )) && + ("string" === typeof input.entries[1] || + __typia_transform__assertGuard._assertGuard( + _exceptionable, + { + method: "typia.createAssert", + path: _path + ".entries[1]", + expected: "string", + value: input.entries[1], + }, + _errorFactory + ))) || + __typia_transform__assertGuard._assertGuard( + _exceptionable, + { + method: "typia.createAssert", + path: _path + ".entries", + expected: "[entry: string, otherEntry: string].o3", + value: input.entries, + }, + _errorFactory + )) && + ("greaterThanEq" === input.type || + __typia_transform__assertGuard._assertGuard( + _exceptionable, + { + method: "typia.createAssert", + path: _path + ".type", + expected: '"greaterThanEq"', + value: input.type, + }, + _errorFactory + )); + const _ao12 = ( + input: any, + _path: string, + _exceptionable: boolean = true + ): boolean => + (((Array.isArray(input.entries) || + __typia_transform__assertGuard._assertGuard( + _exceptionable, + { + method: "typia.createAssert", + path: _path + ".entries", + expected: "[entry: string, otherEntry: string].o4", + value: input.entries, + }, + _errorFactory + )) && + (input.entries.length === 2 || + __typia_transform__assertGuard._assertGuard( + _exceptionable, + { + method: "typia.createAssert", + path: _path + ".entries", + expected: "[string, string]", + value: input.entries, + }, + _errorFactory + )) && + ("string" === typeof input.entries[0] || + __typia_transform__assertGuard._assertGuard( + _exceptionable, + { + method: "typia.createAssert", + path: _path + ".entries[0]", + expected: "string", + value: input.entries[0], + }, + _errorFactory + )) && + ("string" === typeof input.entries[1] || + __typia_transform__assertGuard._assertGuard( + _exceptionable, + { + method: "typia.createAssert", + path: _path + ".entries[1]", + expected: "string", + value: input.entries[1], + }, + _errorFactory + ))) || + __typia_transform__assertGuard._assertGuard( + _exceptionable, + { + method: "typia.createAssert", + path: _path + ".entries", + expected: "[entry: string, otherEntry: string].o4", + value: input.entries, + }, + _errorFactory + )) && + ("lessThan" === input.type || + __typia_transform__assertGuard._assertGuard( + _exceptionable, + { + method: "typia.createAssert", + path: _path + ".type", + expected: '"lessThan"', + value: input.type, + }, + _errorFactory + )); + const _ao13 = ( + input: any, + _path: string, + _exceptionable: boolean = true + ): boolean => + (((Array.isArray(input.entries) || + __typia_transform__assertGuard._assertGuard( + _exceptionable, + { + method: "typia.createAssert", + path: _path + ".entries", + expected: "[entry: string, otherEntry: string].o5", + value: input.entries, + }, + _errorFactory + )) && + (input.entries.length === 2 || + __typia_transform__assertGuard._assertGuard( + _exceptionable, + { + method: "typia.createAssert", + path: _path + ".entries", + expected: "[string, string]", + value: input.entries, + }, + _errorFactory + )) && + ("string" === typeof input.entries[0] || + __typia_transform__assertGuard._assertGuard( + _exceptionable, + { + method: "typia.createAssert", + path: _path + ".entries[0]", + expected: "string", + value: input.entries[0], + }, + _errorFactory + )) && + ("string" === typeof input.entries[1] || + __typia_transform__assertGuard._assertGuard( + _exceptionable, + { + method: "typia.createAssert", + path: _path + ".entries[1]", + expected: "string", + value: input.entries[1], + }, + _errorFactory + ))) || + __typia_transform__assertGuard._assertGuard( + _exceptionable, + { + method: "typia.createAssert", + path: _path + ".entries", + expected: "[entry: string, otherEntry: string].o5", + value: input.entries, + }, + _errorFactory + )) && + ("lessThanEq" === input.type || + __typia_transform__assertGuard._assertGuard( + _exceptionable, + { + method: "typia.createAssert", + path: _path + ".type", + expected: '"lessThanEq"', + value: input.type, + }, + _errorFactory + )); + const _au0 = ( + input: any, + _path: string, + _exceptionable: boolean = true + ): any => + (() => { + if ("isMemberOf" === input.type) + return _ao3(input, _path, true && _exceptionable); + else if ("isNotMemberOf" === input.type) + return _ao4(input, _path, true && _exceptionable); + else if ("inRange" === input.type) + return _ao5(input, _path, true && _exceptionable); + else if ("notInRange" === input.type) + return _ao7(input, _path, true && _exceptionable); + else if ("lessThanEq" === input.type) + return _ao13(input, _path, true && _exceptionable); + else if ("lessThan" === input.type) + return _ao12(input, _path, true && _exceptionable); + else if ("greaterThanEq" === input.type) + return _ao11(input, _path, true && _exceptionable); + else if ("greaterThan" === input.type) + return _ao10(input, _path, true && _exceptionable); + else if ("notEqualsEntry" === input.type) + return _ao9(input, _path, true && _exceptionable); + else if ("equalsEntry" === input.type) + return _ao8(input, _path, true && _exceptionable); + else + return __typia_transform__assertGuard._assertGuard( + _exceptionable, + { + method: "typia.createAssert", + path: _path, + expected: + "(IsMemberOf> | IsNotMemberOf> | InRange | NotInRange | LessThanEq | LessThan | GreaterThanEq | GreaterThan | NotEqualsEntry | EqualsEntry)", + value: input, + }, + _errorFactory + ); + })(); + const __is = (input: any): input is PODSpec => + "object" === typeof input && null !== input && _io0(input); + let _errorFactory: any; + return ( + input: any, + errorFactory?: (p: import("typia").TypeGuardError.IProps) => Error + ): PODSpec => { + if (false === __is(input)) { + _errorFactory = errorFactory; + ((input: any, _path: string, _exceptionable: boolean = true) => + ((("object" === typeof input && null !== input) || + __typia_transform__assertGuard._assertGuard( + true, + { + method: "typia.createAssert", + path: _path + "", + expected: "PODSpec", + value: input, + }, + _errorFactory + )) && + _ao0(input, _path + "", true)) || + __typia_transform__assertGuard._assertGuard( + true, + { + method: "typia.createAssert", + path: _path + "", + expected: "PODSpec", + value: input, + }, + _errorFactory + ))(input, "$input", true); + } + return input; + }; +})(); +export const assertPODGroupSpec = (() => { + const _io0 = (input: any): boolean => + "object" === typeof input.pods && + null !== input.pods && + false === Array.isArray(input.pods) && + _io1(input.pods) && + "object" === typeof input.statements && + null !== input.statements && + false === Array.isArray(input.statements) && + _io4(input.statements); + const _io1 = (input: any): boolean => + Object.keys(input).every((key: any) => { + const value = input[key]; + if (undefined === value) return true; + return "object" === typeof value && null !== value && _io2(value); + }); + const _io2 = (input: any): boolean => + "object" === typeof input.entries && + null !== input.entries && + false === Array.isArray(input.entries) && + _io3(input.entries) && + "object" === typeof input.statements && + null !== input.statements && + false === Array.isArray(input.statements) && + _io4(input.statements); + const _io3 = (input: any): boolean => + Object.keys(input).every((key: any) => { + const value = input[key]; + if (undefined === value) return true; + return ( + "string" === value || + "boolean" === value || + "bytes" === value || + "cryptographic" === value || + "int" === value || + "eddsa_pubkey" === value || + "date" === value || + "null" === value + ); + }); + const _io4 = (input: any): boolean => + Object.keys(input).every((key: any) => { + const value = input[key]; + if (undefined === value) return true; + return "object" === typeof value && null !== value && _iu0(value); + }); + const _io5 = (input: any): boolean => + Array.isArray(input.entries) && + input.entries.every((elem: any) => "string" === typeof elem) && + "isMemberOf" === input.type && + Array.isArray(input.isMemberOf) && + input.isMemberOf.every( + (elem: any) => + Array.isArray(elem) && + elem.every((elem: any) => "string" === typeof elem) + ); + const _io6 = (input: any): boolean => + Array.isArray(input.entries) && + input.entries.every((elem: any) => "string" === typeof elem) && + "isNotMemberOf" === input.type && + Array.isArray(input.isNotMemberOf) && + input.isNotMemberOf.every( + (elem: any) => + Array.isArray(elem) && + elem.every((elem: any) => "string" === typeof elem) + ); + const _io7 = (input: any): boolean => + Array.isArray(input.entries) && + input.entries.length === 1 && + "string" === typeof input.entries[0] && + "inRange" === input.type && + "object" === typeof input.inRange && null !== input.inRange && + _io8(input.inRange); + const _io8 = (input: any): boolean => + "string" === typeof input.min && "string" === typeof input.max; + const _io9 = (input: any): boolean => + Array.isArray(input.entries) && + input.entries.length === 1 && + "string" === typeof input.entries[0] && + "notInRange" === input.type && + "object" === typeof input.notInRange && null !== input.notInRange && + _io8(input.notInRange); + const _io10 = (input: any): boolean => + Array.isArray(input.entries) && + input.entries.length === 2 && "string" === typeof input.entries[0] && + "string" === typeof input.entries[1] && + "equalsEntry" === input.type; + const _io11 = (input: any): boolean => + Array.isArray(input.entries) && + input.entries.length === 2 && "string" === typeof input.entries[0] && + "string" === typeof input.entries[1] && + "notEqualsEntry" === input.type; + const _io12 = (input: any): boolean => + Array.isArray(input.entries) && + input.entries.length === 2 && "string" === typeof input.entries[0] && + "string" === typeof input.entries[1] && + "greaterThan" === input.type; + const _io13 = (input: any): boolean => + Array.isArray(input.entries) && + input.entries.length === 2 && "string" === typeof input.entries[0] && + "string" === typeof input.entries[1] && + "greaterThanEq" === input.type; + const _io14 = (input: any): boolean => + Array.isArray(input.entries) && + input.entries.length === 2 && "string" === typeof input.entries[0] && + "string" === typeof input.entries[1] && + "lessThan" === input.type; + const _io15 = (input: any): boolean => + Array.isArray(input.entries) && + input.entries.length === 2 && "string" === typeof input.entries[0] && + "string" === typeof input.entries[1] && + "lessThanEq" === input.type; + const _iu0 = (input: any): any => + (() => { + if ("isMemberOf" === input.type) return _io5(input); + else if ("isNotMemberOf" === input.type) return _io6(input); + else if ("inRange" === input.type) return _io7(input); + else if ("notInRange" === input.type) return _io9(input); + else if ("lessThanEq" === input.type) return _io15(input); + else if ("lessThan" === input.type) return _io14(input); + else if ("greaterThanEq" === input.type) return _io13(input); + else if ("greaterThan" === input.type) return _io12(input); + else if ("notEqualsEntry" === input.type) return _io11(input); + else if ("equalsEntry" === input.type) return _io10(input); + else return false; + })(); + const _ao0 = ( + input: any, + _path: string, + _exceptionable: boolean = true + ): boolean => + (((("object" === typeof input.pods && + null !== input.pods && + false === Array.isArray(input.pods)) || + __typia_transform__assertGuard._assertGuard( + _exceptionable, + { + method: "typia.createAssert", + path: _path + ".pods", + expected: "NamedPODSpecs", + value: input.pods, + }, + _errorFactory + )) && + _ao1(input.pods, _path + ".pods", true && _exceptionable)) || + __typia_transform__assertGuard._assertGuard( + _exceptionable, + { + method: "typia.createAssert", + path: _path + ".pods", + expected: "NamedPODSpecs", + value: input.pods, + }, + _errorFactory + )) && + (((("object" === typeof input.statements && + null !== input.statements && + false === Array.isArray(input.statements)) || + __typia_transform__assertGuard._assertGuard( + _exceptionable, + { + method: "typia.createAssert", + path: _path + ".statements", + expected: "StatementMap", + value: input.statements, + }, + _errorFactory + )) && + _ao4(input.statements, _path + ".statements", true && _exceptionable)) || + __typia_transform__assertGuard._assertGuard( + _exceptionable, + { + method: "typia.createAssert", + path: _path + ".statements", + expected: "StatementMap", + value: input.statements, + }, + _errorFactory + )); + const _ao1 = ( + input: any, + _path: string, + _exceptionable: boolean = true + ): boolean => + false === _exceptionable || + Object.keys(input).every((key: any) => { + const value = input[key]; + if (undefined === value) return true; + return ( + ((("object" === typeof value && null !== value) || + __typia_transform__assertGuard._assertGuard( + _exceptionable, + { + method: "typia.createAssert", + path: + _path + + __typia_transform__accessExpressionAsString._accessExpressionAsString( + key + ), + expected: "PODSpec", + value: value, + }, + _errorFactory + )) && + _ao2( + value, + _path + + __typia_transform__accessExpressionAsString._accessExpressionAsString( + key + ), + true && _exceptionable + )) || + __typia_transform__assertGuard._assertGuard( + _exceptionable, + { + method: "typia.createAssert", + path: + _path + + __typia_transform__accessExpressionAsString._accessExpressionAsString( + key + ), + expected: "PODSpec", + value: value, + }, + _errorFactory + ) + ); + }); + const _ao2 = ( + input: any, + _path: string, + _exceptionable: boolean = true + ): boolean => + (((("object" === typeof input.entries && + null !== input.entries && + false === Array.isArray(input.entries)) || + __typia_transform__assertGuard._assertGuard( + _exceptionable, + { + method: "typia.createAssert", + path: _path + ".entries", + expected: "EntryTypes", + value: input.entries, + }, + _errorFactory + )) && + _ao3(input.entries, _path + ".entries", true && _exceptionable)) || + __typia_transform__assertGuard._assertGuard( + _exceptionable, + { + method: "typia.createAssert", + path: _path + ".entries", + expected: "EntryTypes", + value: input.entries, + }, + _errorFactory + )) && + (((("object" === typeof input.statements && + null !== input.statements && + false === Array.isArray(input.statements)) || + __typia_transform__assertGuard._assertGuard( + _exceptionable, + { + method: "typia.createAssert", + path: _path + ".statements", + expected: "StatementMap", + value: input.statements, + }, + _errorFactory + )) && + _ao4(input.statements, _path + ".statements", true && _exceptionable)) || + __typia_transform__assertGuard._assertGuard( + _exceptionable, + { + method: "typia.createAssert", + path: _path + ".statements", + expected: "StatementMap", + value: input.statements, + }, + _errorFactory + )); + const _ao3 = ( + input: any, + _path: string, + _exceptionable: boolean = true + ): boolean => + false === _exceptionable || + Object.keys(input).every((key: any) => { + const value = input[key]; + if (undefined === value) return true; + return ( + "string" === value || + "boolean" === value || + "bytes" === value || + "cryptographic" === value || + "int" === value || + "eddsa_pubkey" === value || + "date" === value || + "null" === value || + __typia_transform__assertGuard._assertGuard( + _exceptionable, + { + method: "typia.createAssert", + path: + _path + + __typia_transform__accessExpressionAsString._accessExpressionAsString( + key + ), + expected: + '("boolean" | "bytes" | "cryptographic" | "date" | "eddsa_pubkey" | "int" | "null" | "string")', + value: value, + }, + _errorFactory + ) + ); + }); + const _ao4 = ( + input: any, + _path: string, + _exceptionable: boolean = true + ): boolean => + false === _exceptionable || + Object.keys(input).every((key: any) => { + const value = input[key]; + if (undefined === value) return true; + return ( + ((("object" === typeof value && null !== value) || + __typia_transform__assertGuard._assertGuard( + _exceptionable, + { + method: "typia.createAssert", + path: + _path + + __typia_transform__accessExpressionAsString._accessExpressionAsString( + key + ), + expected: + "(EqualsEntry | GreaterThan | GreaterThanEq | InRange | IsMemberOf> | IsNotMemberOf> | LessThan | LessThanEq | NotEqualsEntry | NotInRange)", + value: value, + }, + _errorFactory + )) && + _au0( + value, + _path + + __typia_transform__accessExpressionAsString._accessExpressionAsString( + key + ), + true && _exceptionable + )) || + __typia_transform__assertGuard._assertGuard( + _exceptionable, + { + method: "typia.createAssert", + path: + _path + + __typia_transform__accessExpressionAsString._accessExpressionAsString( + key + ), + expected: + "(EqualsEntry | GreaterThan | GreaterThanEq | InRange | IsMemberOf> | IsNotMemberOf> | LessThan | LessThanEq | NotEqualsEntry | NotInRange)", + value: value, + }, + _errorFactory + ) + ); + }); + const _ao5 = ( + input: any, + _path: string, + _exceptionable: boolean = true + ): boolean => + (((Array.isArray(input.entries) || + __typia_transform__assertGuard._assertGuard( + _exceptionable, + { + method: "typia.createAssert", + path: _path + ".entries", + expected: "Array", + value: input.entries, + }, + _errorFactory + )) && + input.entries.every( + (elem: any, _index7: number) => + "string" === typeof elem || + __typia_transform__assertGuard._assertGuard( + _exceptionable, + { + method: "typia.createAssert", + path: _path + ".entries[" + _index7 + "]", + expected: "string", + value: elem, + }, + _errorFactory + ) + )) || + __typia_transform__assertGuard._assertGuard( + _exceptionable, + { + method: "typia.createAssert", + path: _path + ".entries", + expected: "Array", + value: input.entries, + }, + _errorFactory + )) && + ("isMemberOf" === input.type || + __typia_transform__assertGuard._assertGuard( + _exceptionable, + { + method: "typia.createAssert", + path: _path + ".type", + expected: '"isMemberOf"', + value: input.type, + }, + _errorFactory + )) && + (((Array.isArray(input.isMemberOf) || + __typia_transform__assertGuard._assertGuard( + _exceptionable, + { + method: "typia.createAssert", + path: _path + ".isMemberOf", + expected: "Array>", + value: input.isMemberOf, + }, + _errorFactory + )) && + input.isMemberOf.every( + (elem: any, _index8: number) => + ((Array.isArray(elem) || + __typia_transform__assertGuard._assertGuard( + _exceptionable, + { + method: "typia.createAssert", + path: _path + ".isMemberOf[" + _index8 + "]", + expected: "Array", + value: elem, + }, + _errorFactory + )) && + elem.every( + (elem: any, _index9: number) => + "string" === typeof elem || + __typia_transform__assertGuard._assertGuard( + _exceptionable, + { + method: "typia.createAssert", + path: + _path + ".isMemberOf[" + _index8 + "][" + _index9 + "]", + expected: "string", + value: elem, + }, + _errorFactory + ) + )) || + __typia_transform__assertGuard._assertGuard( + _exceptionable, + { + method: "typia.createAssert", + path: _path + ".isMemberOf[" + _index8 + "]", + expected: "Array", + value: elem, + }, + _errorFactory + ) + )) || + __typia_transform__assertGuard._assertGuard( + _exceptionable, + { + method: "typia.createAssert", + path: _path + ".isMemberOf", + expected: "Array>", + value: input.isMemberOf, + }, + _errorFactory + )); + const _ao6 = ( + input: any, + _path: string, + _exceptionable: boolean = true + ): boolean => + (((Array.isArray(input.entries) || + __typia_transform__assertGuard._assertGuard( + _exceptionable, + { + method: "typia.createAssert", + path: _path + ".entries", + expected: "Array", + value: input.entries, + }, + _errorFactory + )) && + input.entries.every( + (elem: any, _index10: number) => + "string" === typeof elem || + __typia_transform__assertGuard._assertGuard( + _exceptionable, + { + method: "typia.createAssert", + path: _path + ".entries[" + _index10 + "]", + expected: "string", + value: elem, + }, + _errorFactory + ) + )) || + __typia_transform__assertGuard._assertGuard( + _exceptionable, + { + method: "typia.createAssert", + path: _path + ".entries", + expected: "Array", + value: input.entries, + }, + _errorFactory + )) && + ("isNotMemberOf" === input.type || + __typia_transform__assertGuard._assertGuard( + _exceptionable, + { + method: "typia.createAssert", + path: _path + ".type", + expected: '"isNotMemberOf"', + value: input.type, + }, + _errorFactory + )) && + (((Array.isArray(input.isNotMemberOf) || + __typia_transform__assertGuard._assertGuard( + _exceptionable, + { + method: "typia.createAssert", + path: _path + ".isNotMemberOf", + expected: "Array>", + value: input.isNotMemberOf, + }, + _errorFactory + )) && + input.isNotMemberOf.every( + (elem: any, _index11: number) => + ((Array.isArray(elem) || + __typia_transform__assertGuard._assertGuard( + _exceptionable, + { + method: "typia.createAssert", + path: _path + ".isNotMemberOf[" + _index11 + "]", + expected: "Array", + value: elem, + }, + _errorFactory + )) && + elem.every( + (elem: any, _index12: number) => + "string" === typeof elem || + __typia_transform__assertGuard._assertGuard( + _exceptionable, + { + method: "typia.createAssert", + path: + _path + + ".isNotMemberOf[" + + _index11 + + "][" + + _index12 + + "]", + expected: "string", + value: elem, + }, + _errorFactory + ) + )) || + __typia_transform__assertGuard._assertGuard( + _exceptionable, + { + method: "typia.createAssert", + path: _path + ".isNotMemberOf[" + _index11 + "]", + expected: "Array", + value: elem, + }, + _errorFactory + ) + )) || + __typia_transform__assertGuard._assertGuard( + _exceptionable, + { + method: "typia.createAssert", + path: _path + ".isNotMemberOf", + expected: "Array>", + value: input.isNotMemberOf, + }, + _errorFactory + )); + const _ao7 = ( + input: any, + _path: string, + _exceptionable: boolean = true + ): boolean => + (((Array.isArray(input.entries) || + __typia_transform__assertGuard._assertGuard( + _exceptionable, + { + method: "typia.createAssert", + path: _path + ".entries", + expected: "[entry: string]", + value: input.entries, + }, + _errorFactory + )) && + (input.entries.length === 1 || + __typia_transform__assertGuard._assertGuard( + _exceptionable, + { + method: "typia.createAssert", + path: _path + ".entries", + expected: "[string]", + value: input.entries, + }, + _errorFactory + )) && + ("string" === typeof input.entries[0] || + __typia_transform__assertGuard._assertGuard( + _exceptionable, + { + method: "typia.createAssert", + path: _path + ".entries[0]", + expected: "string", + value: input.entries[0], + }, + _errorFactory + ))) || + __typia_transform__assertGuard._assertGuard( + _exceptionable, + { + method: "typia.createAssert", + path: _path + ".entries", + expected: "[entry: string]", + value: input.entries, + }, + _errorFactory + )) && + ("inRange" === input.type || + __typia_transform__assertGuard._assertGuard( + _exceptionable, + { + method: "typia.createAssert", + path: _path + ".type", + expected: '"inRange"', + value: input.type, + }, + _errorFactory + )) && + (((("object" === typeof input.inRange && null !== input.inRange) || + __typia_transform__assertGuard._assertGuard( + _exceptionable, + { + method: "typia.createAssert", + path: _path + ".inRange", + expected: "RangePersistent", + value: input.inRange, + }, + _errorFactory + )) && + _ao8(input.inRange, _path + ".inRange", true && _exceptionable)) || + __typia_transform__assertGuard._assertGuard( + _exceptionable, + { + method: "typia.createAssert", + path: _path + ".inRange", + expected: "RangePersistent", + value: input.inRange, + }, + _errorFactory + )); + const _ao8 = ( + input: any, + _path: string, + _exceptionable: boolean = true + ): boolean => + ("string" === typeof input.min || + __typia_transform__assertGuard._assertGuard( + _exceptionable, + { + method: "typia.createAssert", + path: _path + ".min", + expected: "string", + value: input.min, + }, + _errorFactory + )) && + ("string" === typeof input.max || + __typia_transform__assertGuard._assertGuard( + _exceptionable, + { + method: "typia.createAssert", + path: _path + ".max", + expected: "string", + value: input.max, + }, + _errorFactory + )); + const _ao9 = ( + input: any, + _path: string, + _exceptionable: boolean = true + ): boolean => + (((Array.isArray(input.entries) || + __typia_transform__assertGuard._assertGuard( + _exceptionable, + { + method: "typia.createAssert", + path: _path + ".entries", + expected: "[entry: string].o1", + value: input.entries, + }, + _errorFactory + )) && + (input.entries.length === 1 || + __typia_transform__assertGuard._assertGuard( + _exceptionable, + { + method: "typia.createAssert", + path: _path + ".entries", + expected: "[string]", + value: input.entries, + }, + _errorFactory + )) && + ("string" === typeof input.entries[0] || + __typia_transform__assertGuard._assertGuard( + _exceptionable, + { + method: "typia.createAssert", + path: _path + ".entries[0]", + expected: "string", + value: input.entries[0], + }, + _errorFactory + ))) || + __typia_transform__assertGuard._assertGuard( + _exceptionable, + { + method: "typia.createAssert", + path: _path + ".entries", + expected: "[entry: string].o1", + value: input.entries, + }, + _errorFactory + )) && + ("notInRange" === input.type || + __typia_transform__assertGuard._assertGuard( + _exceptionable, + { + method: "typia.createAssert", + path: _path + ".type", + expected: '"notInRange"', + value: input.type, + }, + _errorFactory + )) && + (((("object" === typeof input.notInRange && null !== input.notInRange) || + __typia_transform__assertGuard._assertGuard( + _exceptionable, + { + method: "typia.createAssert", + path: _path + ".notInRange", + expected: "RangePersistent", + value: input.notInRange, + }, + _errorFactory + )) && + _ao8(input.notInRange, _path + ".notInRange", true && _exceptionable)) || + __typia_transform__assertGuard._assertGuard( + _exceptionable, + { + method: "typia.createAssert", + path: _path + ".notInRange", + expected: "RangePersistent", + value: input.notInRange, + }, + _errorFactory + )); + const _ao10 = ( + input: any, + _path: string, + _exceptionable: boolean = true + ): boolean => + (((Array.isArray(input.entries) || + __typia_transform__assertGuard._assertGuard( + _exceptionable, + { + method: "typia.createAssert", + path: _path + ".entries", + expected: "[entry: string, otherEntry: string]", + value: input.entries, + }, + _errorFactory + )) && + (input.entries.length === 2 || + __typia_transform__assertGuard._assertGuard( + _exceptionable, + { + method: "typia.createAssert", + path: _path + ".entries", + expected: "[string, string]", + value: input.entries, + }, + _errorFactory + )) && + ("string" === typeof input.entries[0] || + __typia_transform__assertGuard._assertGuard( + _exceptionable, + { + method: "typia.createAssert", + path: _path + ".entries[0]", + expected: "string", + value: input.entries[0], + }, + _errorFactory + )) && + ("string" === typeof input.entries[1] || + __typia_transform__assertGuard._assertGuard( + _exceptionable, + { + method: "typia.createAssert", + path: _path + ".entries[1]", + expected: "string", + value: input.entries[1], + }, + _errorFactory + ))) || + __typia_transform__assertGuard._assertGuard( + _exceptionable, + { + method: "typia.createAssert", + path: _path + ".entries", + expected: "[entry: string, otherEntry: string]", + value: input.entries, + }, + _errorFactory + )) && + ("equalsEntry" === input.type || + __typia_transform__assertGuard._assertGuard( + _exceptionable, + { + method: "typia.createAssert", + path: _path + ".type", + expected: '"equalsEntry"', + value: input.type, + }, + _errorFactory + )); + const _ao11 = ( + input: any, + _path: string, + _exceptionable: boolean = true + ): boolean => + (((Array.isArray(input.entries) || + __typia_transform__assertGuard._assertGuard( + _exceptionable, + { + method: "typia.createAssert", + path: _path + ".entries", + expected: "[entry: string, otherEntry: string].o1", + value: input.entries, + }, + _errorFactory + )) && + (input.entries.length === 2 || + __typia_transform__assertGuard._assertGuard( + _exceptionable, + { + method: "typia.createAssert", + path: _path + ".entries", + expected: "[string, string]", + value: input.entries, + }, + _errorFactory + )) && + ("string" === typeof input.entries[0] || + __typia_transform__assertGuard._assertGuard( + _exceptionable, + { + method: "typia.createAssert", + path: _path + ".entries[0]", + expected: "string", + value: input.entries[0], + }, + _errorFactory + )) && + ("string" === typeof input.entries[1] || + __typia_transform__assertGuard._assertGuard( + _exceptionable, + { + method: "typia.createAssert", + path: _path + ".entries[1]", + expected: "string", + value: input.entries[1], + }, + _errorFactory + ))) || + __typia_transform__assertGuard._assertGuard( + _exceptionable, + { + method: "typia.createAssert", + path: _path + ".entries", + expected: "[entry: string, otherEntry: string].o1", + value: input.entries, + }, + _errorFactory + )) && + ("notEqualsEntry" === input.type || + __typia_transform__assertGuard._assertGuard( + _exceptionable, + { + method: "typia.createAssert", + path: _path + ".type", + expected: '"notEqualsEntry"', + value: input.type, + }, + _errorFactory + )); + const _ao12 = ( + input: any, + _path: string, + _exceptionable: boolean = true + ): boolean => + (((Array.isArray(input.entries) || + __typia_transform__assertGuard._assertGuard( + _exceptionable, + { + method: "typia.createAssert", + path: _path + ".entries", + expected: "[entry: string, otherEntry: string].o2", + value: input.entries, + }, + _errorFactory + )) && + (input.entries.length === 2 || + __typia_transform__assertGuard._assertGuard( + _exceptionable, + { + method: "typia.createAssert", + path: _path + ".entries", + expected: "[string, string]", + value: input.entries, + }, + _errorFactory + )) && + ("string" === typeof input.entries[0] || + __typia_transform__assertGuard._assertGuard( + _exceptionable, + { + method: "typia.createAssert", + path: _path + ".entries[0]", + expected: "string", + value: input.entries[0], + }, + _errorFactory + )) && + ("string" === typeof input.entries[1] || + __typia_transform__assertGuard._assertGuard( + _exceptionable, + { + method: "typia.createAssert", + path: _path + ".entries[1]", + expected: "string", + value: input.entries[1], + }, + _errorFactory + ))) || + __typia_transform__assertGuard._assertGuard( + _exceptionable, + { + method: "typia.createAssert", + path: _path + ".entries", + expected: "[entry: string, otherEntry: string].o2", + value: input.entries, + }, + _errorFactory + )) && + ("greaterThan" === input.type || + __typia_transform__assertGuard._assertGuard( + _exceptionable, + { + method: "typia.createAssert", + path: _path + ".type", + expected: '"greaterThan"', + value: input.type, + }, + _errorFactory + )); + const _ao13 = ( + input: any, + _path: string, + _exceptionable: boolean = true + ): boolean => + (((Array.isArray(input.entries) || + __typia_transform__assertGuard._assertGuard( + _exceptionable, + { + method: "typia.createAssert", + path: _path + ".entries", + expected: "[entry: string, otherEntry: string].o3", + value: input.entries, + }, + _errorFactory + )) && + (input.entries.length === 2 || + __typia_transform__assertGuard._assertGuard( + _exceptionable, + { + method: "typia.createAssert", + path: _path + ".entries", + expected: "[string, string]", + value: input.entries, + }, + _errorFactory + )) && + ("string" === typeof input.entries[0] || + __typia_transform__assertGuard._assertGuard( + _exceptionable, + { + method: "typia.createAssert", + path: _path + ".entries[0]", + expected: "string", + value: input.entries[0], + }, + _errorFactory + )) && + ("string" === typeof input.entries[1] || + __typia_transform__assertGuard._assertGuard( + _exceptionable, + { + method: "typia.createAssert", + path: _path + ".entries[1]", + expected: "string", + value: input.entries[1], + }, + _errorFactory + ))) || + __typia_transform__assertGuard._assertGuard( + _exceptionable, + { + method: "typia.createAssert", + path: _path + ".entries", + expected: "[entry: string, otherEntry: string].o3", + value: input.entries, + }, + _errorFactory + )) && + ("greaterThanEq" === input.type || + __typia_transform__assertGuard._assertGuard( + _exceptionable, + { + method: "typia.createAssert", + path: _path + ".type", + expected: '"greaterThanEq"', + value: input.type, + }, + _errorFactory + )); + const _ao14 = ( + input: any, + _path: string, + _exceptionable: boolean = true + ): boolean => + (((Array.isArray(input.entries) || + __typia_transform__assertGuard._assertGuard( + _exceptionable, + { + method: "typia.createAssert", + path: _path + ".entries", + expected: "[entry: string, otherEntry: string].o4", + value: input.entries, + }, + _errorFactory + )) && + (input.entries.length === 2 || + __typia_transform__assertGuard._assertGuard( + _exceptionable, + { + method: "typia.createAssert", + path: _path + ".entries", + expected: "[string, string]", + value: input.entries, + }, + _errorFactory + )) && + ("string" === typeof input.entries[0] || + __typia_transform__assertGuard._assertGuard( + _exceptionable, + { + method: "typia.createAssert", + path: _path + ".entries[0]", + expected: "string", + value: input.entries[0], + }, + _errorFactory + )) && + ("string" === typeof input.entries[1] || + __typia_transform__assertGuard._assertGuard( + _exceptionable, + { + method: "typia.createAssert", + path: _path + ".entries[1]", + expected: "string", + value: input.entries[1], + }, + _errorFactory + ))) || + __typia_transform__assertGuard._assertGuard( + _exceptionable, + { + method: "typia.createAssert", + path: _path + ".entries", + expected: "[entry: string, otherEntry: string].o4", + value: input.entries, + }, + _errorFactory + )) && + ("lessThan" === input.type || + __typia_transform__assertGuard._assertGuard( + _exceptionable, + { + method: "typia.createAssert", + path: _path + ".type", + expected: '"lessThan"', + value: input.type, + }, + _errorFactory + )); + const _ao15 = ( + input: any, + _path: string, + _exceptionable: boolean = true + ): boolean => + (((Array.isArray(input.entries) || + __typia_transform__assertGuard._assertGuard( + _exceptionable, + { + method: "typia.createAssert", + path: _path + ".entries", + expected: "[entry: string, otherEntry: string].o5", + value: input.entries, + }, + _errorFactory + )) && + (input.entries.length === 2 || + __typia_transform__assertGuard._assertGuard( + _exceptionable, + { + method: "typia.createAssert", + path: _path + ".entries", + expected: "[string, string]", + value: input.entries, + }, + _errorFactory + )) && + ("string" === typeof input.entries[0] || + __typia_transform__assertGuard._assertGuard( + _exceptionable, + { + method: "typia.createAssert", + path: _path + ".entries[0]", + expected: "string", + value: input.entries[0], + }, + _errorFactory + )) && + ("string" === typeof input.entries[1] || + __typia_transform__assertGuard._assertGuard( + _exceptionable, + { + method: "typia.createAssert", + path: _path + ".entries[1]", + expected: "string", + value: input.entries[1], + }, + _errorFactory + ))) || + __typia_transform__assertGuard._assertGuard( + _exceptionable, + { + method: "typia.createAssert", + path: _path + ".entries", + expected: "[entry: string, otherEntry: string].o5", + value: input.entries, + }, + _errorFactory + )) && + ("lessThanEq" === input.type || + __typia_transform__assertGuard._assertGuard( + _exceptionable, + { + method: "typia.createAssert", + path: _path + ".type", + expected: '"lessThanEq"', + value: input.type, + }, + _errorFactory + )); + const _au0 = ( + input: any, + _path: string, + _exceptionable: boolean = true + ): any => + (() => { + if ("isMemberOf" === input.type) + return _ao5(input, _path, true && _exceptionable); + else if ("isNotMemberOf" === input.type) + return _ao6(input, _path, true && _exceptionable); + else if ("inRange" === input.type) + return _ao7(input, _path, true && _exceptionable); + else if ("notInRange" === input.type) + return _ao9(input, _path, true && _exceptionable); + else if ("lessThanEq" === input.type) + return _ao15(input, _path, true && _exceptionable); + else if ("lessThan" === input.type) + return _ao14(input, _path, true && _exceptionable); + else if ("greaterThanEq" === input.type) + return _ao13(input, _path, true && _exceptionable); + else if ("greaterThan" === input.type) + return _ao12(input, _path, true && _exceptionable); + else if ("notEqualsEntry" === input.type) + return _ao11(input, _path, true && _exceptionable); + else if ("equalsEntry" === input.type) + return _ao10(input, _path, true && _exceptionable); + else + return __typia_transform__assertGuard._assertGuard( + _exceptionable, + { + method: "typia.createAssert", + path: _path, + expected: + "(IsMemberOf> | IsNotMemberOf> | InRange | NotInRange | LessThanEq | LessThan | GreaterThanEq | GreaterThan | NotEqualsEntry | EqualsEntry)", + value: input, + }, + _errorFactory + ); + })(); + const __is = ( + input: any + ): input is PODGroupSpec => + "object" === typeof input && null !== input && _io0(input); + let _errorFactory: any; + return ( + input: any, + errorFactory?: (p: import("typia").TypeGuardError.IProps) => Error + ): PODGroupSpec => { + if (false === __is(input)) { + _errorFactory = errorFactory; + ((input: any, _path: string, _exceptionable: boolean = true) => + ((("object" === typeof input && null !== input) || + __typia_transform__assertGuard._assertGuard( + true, + { + method: "typia.createAssert", + path: _path + "", + expected: "PODGroupSpec", + value: input, + }, + _errorFactory + )) && + _ao0(input, _path + "", true)) || + __typia_transform__assertGuard._assertGuard( + true, + { + method: "typia.createAssert", + path: _path + "", + expected: "PODGroupSpec", + value: input, + }, + _errorFactory + ))(input, "$input", true); + } + return input; + }; +})(); diff --git a/packages/podspec/src/gpc/proof_request.ts b/packages/podspec/src/gpc/proof_request.ts index 646f933..29facff 100644 --- a/packages/podspec/src/gpc/proof_request.ts +++ b/packages/podspec/src/gpc/proof_request.ts @@ -1,258 +1,258 @@ -import type { - GPCProofConfig, - GPCProofEntryConfig, - GPCProofObjectConfig, - GPCProofTupleConfig, - IdentityProtocol, - PODEntryIdentifier, - PODMembershipLists -} from "@pcd/gpc"; -import type { POD, PODName, PODValue } from "@pcd/pod"; -import { PodSpec } from "../parse/pod.js"; -import { $e } from "../pod_value_utils.js"; -import type { EntriesSchema } from "../schemas/entries.js"; -import type { PODSchema } from "../schemas/pod.js"; +// import type { +// GPCProofConfig, +// GPCProofEntryConfig, +// GPCProofObjectConfig, +// GPCProofTupleConfig, +// IdentityProtocol, +// PODEntryIdentifier, +// PODMembershipLists +// } from "@pcd/gpc"; +// import type { POD, PODName, PODValue } from "@pcd/pod"; +// import { PodSpec } from "../parse/pod.js"; +// import { $e } from "../pod_value_utils.js"; +// import type { EntriesSchema } from "../schemas/entries.js"; +// import type { PODSchema } from "../schemas/pod.js"; -// eslint-disable-next-line @typescript-eslint/no-explicit-any -type NamedPODs = Record>; +// // eslint-disable-next-line @typescript-eslint/no-explicit-any +// type NamedPODs = Record>; -export interface ProofConfigOwner { - entry: Extract; - protocol: IdentityProtocol; -} +// export interface ProofConfigOwner { +// entry: Extract; +// protocol: IdentityProtocol; +// } -export interface ProofConfigPODSchema { - pod: PODSchema; - revealed?: Partial<{ - [K in Extract]: boolean; - }>; - owner?: ProofConfigOwner; -} +// export interface ProofConfigPODSchema { +// pod: PODSchema; +// revealed?: Partial<{ +// [K in Extract]: boolean; +// }>; +// owner?: ProofConfigOwner; +// } -/** - * A ProofRequest contains the data necessary to verify that a given GPC proof - * matches our expectations of it. - */ -export type ProofRequest = { - proofConfig: GPCProofConfig; - membershipLists: PODMembershipLists; - externalNullifier?: PODValue; - watermark?: PODValue; -}; +// /** +// * A ProofRequest contains the data necessary to verify that a given GPC proof +// * matches our expectations of it. +// */ +// export type ProofRequest = { +// proofConfig: GPCProofConfig; +// membershipLists: PODMembershipLists; +// externalNullifier?: PODValue; +// watermark?: PODValue; +// }; -/** - * A PodspecProofRequest allows us to generate a {@link ProofRequest} from a - * set of Podspecs defining the allowable PODs. - */ -export interface PodspecProofRequestSchema

{ - pods: P; - externalNullifier?: PODValue; - watermark?: PODValue; -} +// /** +// * A PodspecProofRequest allows us to generate a {@link ProofRequest} from a +// * set of Podspecs defining the allowable PODs. +// */ +// export interface PodspecProofRequestSchema

{ +// pods: P; +// externalNullifier?: PODValue; +// watermark?: PODValue; +// } -/** - * A ProofRequestSpec allows us to generate a {@link ProofRequest} from a - * set of Podspecs defining the allowable PODs. - */ -export class ProofRequestSpec< - P extends PodspecProofRequestSchema, - T extends NamedPODs -> { - /** - * Private constructor, see {@link create}. - * @param schema The schema of the PODs that are allowed in this proof. - */ - private constructor(public readonly schema: PodspecProofRequestSchema) {} +// /** +// * A ProofRequestSpec allows us to generate a {@link ProofRequest} from a +// * set of Podspecs defining the allowable PODs. +// */ +// export class ProofRequestSpec< +// P extends PodspecProofRequestSchema, +// T extends NamedPODs +// > { +// /** +// * Private constructor, see {@link create}. +// * @param schema The schema of the PODs that are allowed in this proof. +// */ +// private constructor(public readonly schema: PodspecProofRequestSchema) {} - /** - * Create a new ProofRequestSpec. - * @param schema The schema of the PODs that are allowed in this proof. - * @returns A new ProofRequestSpec. - */ - public static create< - P extends PodspecProofRequestSchema, - T extends NamedPODs - >(schema: PodspecProofRequestSchema): ProofRequestSpec { - return new ProofRequestSpec(schema); - } +// /** +// * Create a new ProofRequestSpec. +// * @param schema The schema of the PODs that are allowed in this proof. +// * @returns A new ProofRequestSpec. +// */ +// public static create< +// P extends PodspecProofRequestSchema, +// T extends NamedPODs +// >(schema: PodspecProofRequestSchema): ProofRequestSpec { +// return new ProofRequestSpec(schema); +// } - /** - * Get the {@link ProofRequest} that this ProofRequestSpec defines. - * @returns A {@link ProofRequest}. - */ - public getProofRequest(): ProofRequest { - return makeProofRequest(this.schema); - } +// /** +// * Get the {@link ProofRequest} that this ProofRequestSpec defines. +// * @returns A {@link ProofRequest}. +// */ +// public getProofRequest(): ProofRequest { +// return makeProofRequest(this.schema); +// } - /** - * A ProofRequest defines a {@link GPCProofConfig} and part of the - * {@link GPCProofInputs} - specifically the watermark, external nullifier, - * and membership lists. However, a GPC proof also requires PODs as inputs. - * Since we know from our schema which PODs would be acceptable inputs, we - * can take an array of PODs and return a mapping of the require POD names - * to the PODs from the array which would be suitable as inputs in each slot - * respectively. - * - * @param pods The PODs to query. - * @returns A record of the PODs that are allowed in this proof. - */ - public queryForInputs(pods: POD[]): Record { - const result: Record = {}; - for (const [podName, proofConfigPODSchema] of Object.entries( - this.schema.pods as Record> - )) { - const podSchema = proofConfigPODSchema.pod; - result[podName] = PodSpec.create(podSchema).query(pods).matches; - } - return result as Record; - } -} +// /** +// * A ProofRequest defines a {@link GPCProofConfig} and part of the +// * {@link GPCProofInputs} - specifically the watermark, external nullifier, +// * and membership lists. However, a GPC proof also requires PODs as inputs. +// * Since we know from our schema which PODs would be acceptable inputs, we +// * can take an array of PODs and return a mapping of the require POD names +// * to the PODs from the array which would be suitable as inputs in each slot +// * respectively. +// * +// * @param pods The PODs to query. +// * @returns A record of the PODs that are allowed in this proof. +// */ +// public queryForInputs(pods: POD[]): Record { +// const result: Record = {}; +// for (const [podName, proofConfigPODSchema] of Object.entries( +// this.schema.pods as Record> +// )) { +// const podSchema = proofConfigPODSchema.pod; +// result[podName] = PodSpec.create(podSchema).query(pods).matches; +// } +// return result as Record; +// } +// } -/** - * Export for convenience. - */ -export const proofRequest =

( - schema: PodspecProofRequestSchema

-) => ProofRequestSpec.create(schema); +// /** +// * Export for convenience. +// */ +// export const proofRequest =

( +// schema: PodspecProofRequestSchema

+// ) => ProofRequestSpec.create(schema); -/** - * Generates a {@link ProofRequest}. - * - * @param request The PodspecProofRequest to derive the ProofRequest from. - * @returns A ProofRequest. - */ -function makeProofRequest

( - request: PodspecProofRequestSchema

-): ProofRequest { - const pods: Record = {}; - const membershipLists: PODMembershipLists = {}; - const tuples: Record = {}; +// /** +// * Generates a {@link ProofRequest}. +// * +// * @param request The PodspecProofRequest to derive the ProofRequest from. +// * @returns A ProofRequest. +// */ +// function makeProofRequest

( +// request: PodspecProofRequestSchema

+// ): ProofRequest { +// const pods: Record = {}; +// const membershipLists: PODMembershipLists = {}; +// const tuples: Record = {}; - for (const [podName, proofConfigPODSchema] of Object.entries( - request.pods as Record> - )) { - const podConfig: GPCProofObjectConfig = { entries: {} }; - const podSchema = proofConfigPODSchema.pod; - const owner = proofConfigPODSchema.owner; +// for (const [podName, proofConfigPODSchema] of Object.entries( +// request.pods as Record> +// )) { +// const podConfig: GPCProofObjectConfig = { entries: {} }; +// const podSchema = proofConfigPODSchema.pod; +// const owner = proofConfigPODSchema.owner; - for (const [entryName, schema] of Object.entries(podSchema.entries)) { - const entrySchema = - schema.type === "optional" ? schema.innerType : schema; +// for (const [entryName, schema] of Object.entries(podSchema.entries)) { +// const entrySchema = +// schema.type === "optional" ? schema.innerType : schema; - const isRevealed = proofConfigPODSchema.revealed?.[entryName] ?? false; - const isMemberOf = - entrySchema.type === "null" ? undefined : entrySchema.isMemberOf; - const isNotMemberOf = - entrySchema.type === "null" ? undefined : entrySchema.isNotMemberOf; - const inRange = - (entrySchema.type === "cryptographic" || entrySchema.type === "int") && - entrySchema.inRange; - const isOwnerID = - (entrySchema.type === "cryptographic" || - entrySchema.type === "eddsa_pubkey") && - owner?.entry === entryName; +// const isRevealed = proofConfigPODSchema.revealed?.[entryName] ?? false; +// const isMemberOf = +// entrySchema.type === "null" ? undefined : entrySchema.isMemberOf; +// const isNotMemberOf = +// entrySchema.type === "null" ? undefined : entrySchema.isNotMemberOf; +// const inRange = +// (entrySchema.type === "cryptographic" || entrySchema.type === "int") && +// entrySchema.inRange; +// const isOwnerID = +// (entrySchema.type === "cryptographic" || +// entrySchema.type === "eddsa_pubkey") && +// owner?.entry === entryName; - if ( - !isRevealed && - !isMemberOf && - !isNotMemberOf && - !inRange && - !isOwnerID - ) { - continue; - } +// if ( +// !isRevealed && +// !isMemberOf && +// !isNotMemberOf && +// !inRange && +// !isOwnerID +// ) { +// continue; +// } - const entryConfig: GPCProofEntryConfig = { - isRevealed, - ...(isMemberOf - ? { isMemberOf: `allowlist_${podName}_${entryName}` } - : {}), - ...(isNotMemberOf - ? { isNotMemberOf: `blocklist_${podName}_${entryName}` } - : {}), - ...(inRange ? { inRange: entrySchema.inRange } : {}), - ...(isOwnerID ? { isOwnerID: owner.protocol } : {}) - }; - podConfig.entries[entryName] = entryConfig; +// const entryConfig: GPCProofEntryConfig = { +// isRevealed, +// ...(isMemberOf +// ? { isMemberOf: `allowlist_${podName}_${entryName}` } +// : {}), +// ...(isNotMemberOf +// ? { isNotMemberOf: `blocklist_${podName}_${entryName}` } +// : {}), +// ...(inRange ? { inRange: entrySchema.inRange } : {}), +// ...(isOwnerID ? { isOwnerID: owner.protocol } : {}) +// }; +// podConfig.entries[entryName] = entryConfig; - if (entrySchema.type !== "null" && entrySchema.isMemberOf) { - membershipLists[`allowlist_${podName}_${entryName}`] = - entrySchema.isMemberOf; - } - if (entrySchema.type !== "null" && entrySchema.isNotMemberOf) { - membershipLists[`blocklist_${podName}_${entryName}`] = - entrySchema.isNotMemberOf; - } - } +// if (entrySchema.type !== "null" && entrySchema.isMemberOf) { +// membershipLists[`allowlist_${podName}_${entryName}`] = +// entrySchema.isMemberOf; +// } +// if (entrySchema.type !== "null" && entrySchema.isNotMemberOf) { +// membershipLists[`blocklist_${podName}_${entryName}`] = +// entrySchema.isNotMemberOf; +// } +// } - for (const entriesTupleSchema of podSchema.tuples ?? []) { - const tupleName = `tuple_${podName}_entries_${entriesTupleSchema.entries - .map((entryName) => entryName.replace("$", "_")) - .join("_")}`; - tuples[tupleName] = { - entries: entriesTupleSchema.entries.map( - (entryName) => `${podName}.${entryName}` satisfies PODEntryIdentifier - ), - isMemberOf: entriesTupleSchema.isMemberOf - ? `allowlist_${tupleName}` - : undefined, - isNotMemberOf: entriesTupleSchema.isNotMemberOf - ? `blocklist_${tupleName}` - : undefined - } satisfies GPCProofTupleConfig; - if (entriesTupleSchema.isMemberOf) { - membershipLists[`allowlist_${tupleName}`] = - entriesTupleSchema.isMemberOf; - } - if (entriesTupleSchema.isNotMemberOf) { - membershipLists[`blocklist_${tupleName}`] = - entriesTupleSchema.isNotMemberOf; - } - // Tuples may contain entries which are not revealed or subject to any - // membership rules or constraints, in which case we need to add them to - // the proof config so that they are included. - for (const entry of entriesTupleSchema.entries) { - if (entry === "$signerPublicKey") { - continue; - } - if (!(entry in podConfig.entries)) { - podConfig.entries[entry] = { - isRevealed: false - }; - } - } - } +// for (const entriesTupleSchema of podSchema.tuples ?? []) { +// const tupleName = `tuple_${podName}_entries_${entriesTupleSchema.entries +// .map((entryName) => entryName.replace("$", "_")) +// .join("_")}`; +// tuples[tupleName] = { +// entries: entriesTupleSchema.entries.map( +// (entryName) => `${podName}.${entryName}` satisfies PODEntryIdentifier +// ), +// isMemberOf: entriesTupleSchema.isMemberOf +// ? `allowlist_${tupleName}` +// : undefined, +// isNotMemberOf: entriesTupleSchema.isNotMemberOf +// ? `blocklist_${tupleName}` +// : undefined +// } satisfies GPCProofTupleConfig; +// if (entriesTupleSchema.isMemberOf) { +// membershipLists[`allowlist_${tupleName}`] = +// entriesTupleSchema.isMemberOf; +// } +// if (entriesTupleSchema.isNotMemberOf) { +// membershipLists[`blocklist_${tupleName}`] = +// entriesTupleSchema.isNotMemberOf; +// } +// // Tuples may contain entries which are not revealed or subject to any +// // membership rules or constraints, in which case we need to add them to +// // the proof config so that they are included. +// for (const entry of entriesTupleSchema.entries) { +// if (entry === "$signerPublicKey") { +// continue; +// } +// if (!(entry in podConfig.entries)) { +// podConfig.entries[entry] = { +// isRevealed: false +// }; +// } +// } +// } - if (podSchema.signerPublicKey) { - podConfig.signerPublicKey = { - isRevealed: podSchema.signerPublicKey.isRevealed ?? false - }; - if (podSchema.signerPublicKey.isMemberOf) { - // Double underscore to avoid collision with entry names. - membershipLists[`allowlist_${podName}__signerPublicKey`] = $e( - podSchema.signerPublicKey.isMemberOf - ); - podConfig.signerPublicKey.isMemberOf = `allowlist_${podName}__signerPublicKey`; - } - if (podSchema.signerPublicKey.isNotMemberOf) { - // Double underscore to avoid collision with entry names. - membershipLists[`blocklist_${podName}__signerPublicKey`] = $e( - podSchema.signerPublicKey.isNotMemberOf - ); - podConfig.signerPublicKey.isNotMemberOf = `blocklist_${podName}__signerPublicKey`; - } - } +// if (podSchema.signerPublicKey) { +// podConfig.signerPublicKey = { +// isRevealed: podSchema.signerPublicKey.isRevealed ?? false +// }; +// if (podSchema.signerPublicKey.isMemberOf) { +// // Double underscore to avoid collision with entry names. +// membershipLists[`allowlist_${podName}__signerPublicKey`] = $e( +// podSchema.signerPublicKey.isMemberOf +// ); +// podConfig.signerPublicKey.isMemberOf = `allowlist_${podName}__signerPublicKey`; +// } +// if (podSchema.signerPublicKey.isNotMemberOf) { +// // Double underscore to avoid collision with entry names. +// membershipLists[`blocklist_${podName}__signerPublicKey`] = $e( +// podSchema.signerPublicKey.isNotMemberOf +// ); +// podConfig.signerPublicKey.isNotMemberOf = `blocklist_${podName}__signerPublicKey`; +// } +// } - pods[podName] = podConfig; - } +// pods[podName] = podConfig; +// } - return { - proofConfig: { - pods, - tuples - }, - membershipLists, - watermark: request.watermark, - externalNullifier: request.externalNullifier - } satisfies ProofRequest; -} +// return { +// proofConfig: { +// pods, +// tuples +// }, +// membershipLists, +// watermark: request.watermark, +// externalNullifier: request.externalNullifier +// } satisfies ProofRequest; +// } diff --git a/packages/podspec/src/index.ts b/packages/podspec/src/index.ts index 187f4d4..2c63b74 100644 --- a/packages/podspec/src/index.ts +++ b/packages/podspec/src/index.ts @@ -1,40 +1,4 @@ -import { podToPODData } from "./data.js"; -import type { - PodspecProofRequestSchema, - ProofConfigPODSchema, - ProofRequest, - ProofRequestSpec -} from "./gpc/proof_request.js"; -import { proofRequest } from "./gpc/proof_request.js"; -import type { EntriesSpec } from "./parse/entries.js"; -import { entries } from "./parse/entries.js"; -import type { PODData, PodSpec } from "./parse/pod.js"; -import { pod } from "./parse/pod.js"; -import type { EntriesSchema } from "./schemas/entries.js"; -import type { PODSchema } from "./schemas/pod.js"; -import type { - EntriesOutputType, - InferEntriesType, - InferJavaScriptEntriesType, - InferPodType -} from "./type_inference.js"; +import { type PODGroupSpec, PODGroupSpecBuilder } from "./builders/group.js"; +import { type PODSpec, PODSpecBuilder } from "./builders/pod.js"; -export { - entries, - pod, - proofRequest, - podToPODData, - type EntriesOutputType, - type EntriesSchema, - type EntriesSpec, - type InferJavaScriptEntriesType, - type InferEntriesType, - type InferPodType, - type ProofConfigPODSchema, - type PODSchema, - type PodSpec, - type PODData, - type PodspecProofRequestSchema as PodspecProofRequest, - type ProofRequest, - type ProofRequestSpec -}; +export { type PODSpec, PODSpecBuilder, type PODGroupSpec, PODGroupSpecBuilder }; diff --git a/packages/podspec/src/parse/entries.ts b/packages/podspec/src/parse/entries.ts deleted file mode 100644 index cfb1f0f..0000000 --- a/packages/podspec/src/parse/entries.ts +++ /dev/null @@ -1,330 +0,0 @@ -import type { PODEntries, PODValue } from "@pcd/pod"; -import { checkPODName } from "@pcd/pod/podChecks"; -import type { - PodspecBaseIssue, - PodspecInvalidEntryNameIssue, - PodspecInvalidTypeIssue, - PodspecMissingEntryIssue, - PodspecUnexpectedInputEntryIssue -} from "../error.js"; -import { IssueCode, PodspecError } from "../error.js"; -import { booleanCoercer, checkPODBooleanValue } from "../schemas/boolean.js"; -import { bytesCoercer, checkPODBytesValue } from "../schemas/bytes.js"; -import { - checkPODCryptographicValue, - cryptographicCoercer -} from "../schemas/cryptographic.js"; -import { checkPODDateValue, dateCoercer } from "../schemas/dates.js"; -import { - checkPODEdDSAPublicKeyValue, - eddsaPublicKeyCoercer -} from "../schemas/eddsa_pubkey.js"; -import type { EntriesSchema, EntriesTupleSchema } from "../schemas/entries.js"; -import type { EntrySchema } from "../schemas/entry.js"; -import { checkPODIntValue, intCoercer } from "../schemas/int.js"; -import { checkPODNullValue, nullCoercer } from "../schemas/null.js"; -import { checkPODStringValue, stringCoercer } from "../schemas/string.js"; -import type { EntriesOutputType } from "../type_inference.js"; -import { deepFreeze } from "../utils.js"; -import { parseEntry } from "./entry.js"; -import type { ParseResult } from "./parse_utils.js"; -import { FAILURE, SUCCESS, safeCheckTuple } from "./parse_utils.js"; - -const COERCERS: Record unknown> = { - string: stringCoercer, - int: intCoercer, - eddsa_pubkey: eddsaPublicKeyCoercer, - cryptographic: cryptographicCoercer, - boolean: booleanCoercer, - bytes: bytesCoercer, - date: dateCoercer, - null: nullCoercer -}; - -const TYPE_VALIDATORS = { - string: checkPODStringValue, - int: checkPODIntValue, - eddsa_pubkey: checkPODEdDSAPublicKeyValue, - cryptographic: checkPODCryptographicValue, - boolean: checkPODBooleanValue, - bytes: checkPODBytesValue, - date: checkPODDateValue, - null: checkPODNullValue -}; - -/** - * Options controlling how parsing of entries is performed. - */ -export interface EntriesParseOptions { - // Exit as soon as the first issue is encountered, useful when you just want - // to validate if some data is correct - exitEarly?: boolean; - // Reject entries in the input which are not in the schema - strict?: boolean; - // Allow certain JavaScript types as inputs, where conversion to PODValue is - // straightforward - coerce?: boolean; - // Tuples to check against the entries provided. - tuples?: EntriesTupleSchema[]; -} - -const VALID_ENTRY_SCHEMA_TYPES = [ - "int", - "string", - "cryptographic", - "eddsa_pubkey", - "boolean", - "bytes", - "date", - "null", - "optional" -] as const; - -type ValidEntrySchemaType = (typeof VALID_ENTRY_SCHEMA_TYPES)[number]; - -// Type assertion to ensure ValidEntrySchemaType matches EntrySchema["type"] -type AssertEntrySchemaType = ValidEntrySchemaType extends EntrySchema["type"] - ? EntrySchema["type"] extends ValidEntrySchemaType - ? true - : false - : false; - -// This will cause a compile-time error if the types don't match -const _: AssertEntrySchemaType = true; - -// Runtime check function -function isValidEntryType(type: string): type is EntrySchema["type"] { - return (VALID_ENTRY_SCHEMA_TYPES as readonly string[]).includes(type); -} - -/** - * A specification for a set of entries. - */ -export class EntriesSpec { - /** - * The schema for this set of entries. - * This is public so that it can be used to create new schemas, but the - * object is frozen and so cannot be mutated. - */ - public readonly schema: E; - /** - * The constructor is private - see {@link create} for public construction. - * - * @param schema The schema to use for this set of entries. - */ - private constructor(schema: E) { - for (const [name, entry] of Object.entries(schema)) { - const entryType = - entry.type === "optional" ? entry.innerType.type : entry.type; - if (!isValidEntryType(entryType)) { - throw new Error( - `Entry ${name} contains invalid entry type: ${entryType as string}` - ); - } - } - this.schema = deepFreeze(structuredClone(schema)); - } - - /** - * Parse entries without throwing an exception. - * - * @param input A record of string keys to PODValues, strings, bigints or numbers. - * @param options Options controlling how parsing of entries is performed. - * @param path The path leading to this object. - * @returns A ParseResult containing either a valid result or list of issues. - */ - public safeParse( - input: Record< - string, - PODValue | string | bigint | number | boolean | Uint8Array | null | Date - >, - options: EntriesParseOptions = DEFAULT_ENTRIES_PARSE_OPTIONS, - path: string[] = [] - ): ParseResult> { - return safeParseEntries(this.schema, input, options, path); - } - - /** - * As {@link safeParse} but will throw an exception if errors are encountered. - */ - public parse( - input: Record< - string, - PODValue | string | bigint | number | boolean | Uint8Array | null | Date - >, - options: EntriesParseOptions = DEFAULT_ENTRIES_PARSE_OPTIONS, - path: string[] = [] - ): EntriesOutputType { - const result = this.safeParse(input, options, path); - if (result.isValid) { - return result.value; - } else { - throw new PodspecError(result.issues); - } - } - - // public cloneSchema(): E; - // public cloneSchema(modify: (schema: E) => R): R; - // public cloneSchema(modify?: (schema: E) => R): E | R { - // const clonedSchema = structuredClone(this.schema); - // if (modify) { - // return modify(clonedSchema); - // } - // return clonedSchema; - // } - - /** - * Creates an EntriesSpec object from a given schema. - * - * @param schema The schema to use for this set of entries. - * @returns A new EntriesSpec object - */ - public static create( - schema: E - ): EntriesSpec { - return new EntriesSpec(schema); - } -} - -/** - * Exported creation function, for convenience. - */ -export const entries = (schema: E) => - EntriesSpec.create(schema); - -/** - * Default entries parsing options. - */ -export const DEFAULT_ENTRIES_PARSE_OPTIONS: EntriesParseOptions = - { - exitEarly: false, - strict: false, - coerce: false - } as const; - -/** - * Parser function for entries. - * - * @param schema The schema for the entries - * @param input Input values for parsing - * @param options Options controlling how parsing of entries is performed. - * @param path The path leading to this object. - * @returns A ParseResult containing either a valid result or list of issues. - */ -export function safeParseEntries( - schema: E, - input: Record< - string, - PODValue | string | bigint | number | boolean | Uint8Array | null | Date - >, - options: EntriesParseOptions = DEFAULT_ENTRIES_PARSE_OPTIONS, - path: string[] = [] -): ParseResult> { - if (typeof input !== "object" || input === null) { - const issue: PodspecInvalidTypeIssue = { - code: IssueCode.invalid_type, - expectedType: "PODEntries", - path - }; - return FAILURE([issue]); - } - - const output: PODEntries = {}; - const issues: PodspecBaseIssue[] = []; - - for (const [name, entry] of Object.entries(schema)) { - const isOptional = entry.type === "optional"; - - if (!(name in input) && !isOptional) { - const issue: PodspecMissingEntryIssue = { - code: IssueCode.missing_entry, - key: name, - path: [...path, name] - }; - issues.push(issue); - if (options.exitEarly) { - return FAILURE(issues); - } - } - } - - for (const [key, value] of Object.entries(input)) { - if (!(key in schema)) { - if (options.strict) { - const issue: PodspecUnexpectedInputEntryIssue = { - code: IssueCode.unexpected_input_entry, - name: key, - path: [...path, key] - }; - if (options.exitEarly) { - return FAILURE([issue]); - } else { - issues.push(issue); - } - } else { - continue; - } - } - - const entryPath = [...path, key]; - try { - // Will throw if the key is invalid - checkPODName(key); - } catch (e) { - const issue: PodspecInvalidEntryNameIssue = { - code: IssueCode.invalid_entry_name, - name: key, - description: (e as Error).message, - path: entryPath - }; - issues.push(issue); - if (options.exitEarly) { - return FAILURE(issues); - } - } - - let entrySchema: EntrySchema = schema[key]!; - if (entrySchema.type === "optional") { - entrySchema = entrySchema.innerType; - } - const result = parseEntry( - entrySchema, - value, - options, - entryPath, - TYPE_VALIDATORS[entrySchema.type], - COERCERS[entrySchema.type] - ); - if (!result.isValid) { - if (options.exitEarly) { - return FAILURE(result.issues); - } else { - issues.push(...result.issues); - } - } else { - output[key] = result.value; - } - } - - if (options.tuples) { - for (const [tupleIndex, tupleSchema] of options.tuples.entries()) { - const result = safeCheckTuple(output, tupleSchema, options, [ - ...path, - "$tuples", - tupleIndex.toString() - ]); - - if (!result.isValid) { - if (options.exitEarly) { - return FAILURE(result.issues); - } else { - issues.push(...result.issues); - } - } - } - } - - return issues.length > 0 - ? FAILURE(issues) - : SUCCESS(output as EntriesOutputType); -} diff --git a/packages/podspec/src/parse/entry.ts b/packages/podspec/src/parse/entry.ts deleted file mode 100644 index 930e3e9..0000000 --- a/packages/podspec/src/parse/entry.ts +++ /dev/null @@ -1,122 +0,0 @@ -import type { - PODCryptographicValue, - PODDateValue, - PODIntValue -} from "@pcd/pod"; -import { checkBigintBounds } from "@pcd/pod/podChecks"; -import type { PodspecBaseIssue, PodspecNotInRangeIssue } from "../error.js"; -import { IssueCode } from "../error.js"; -import type { DefinedEntrySchema } from "../schemas/entry.js"; -import { DEFAULT_ENTRIES_PARSE_OPTIONS } from "./entries.js"; -import type { PODValueTypeNameToPODValue, ParseResult } from "./parse_utils.js"; -import { - FAILURE, - SUCCESS, - safeCheckPODValue, - safeMembershipChecks -} from "./parse_utils.js"; - -/** - * Options controlling how parsing of a single entry is performed. - */ -export interface EntryParseOptions { - exitEarly?: boolean; - coerce?: boolean; -} - -/** - * Parses a single entry according to a given schema. - * - * @param schema The schema for the entry - * @param input Input values for parsing - * @param options Options controlling how parsing of entries is performed. - * @param path The path leading to this object. - * @param typeValidator A function that validates the type of the input. - * @param coercer A function that coerces the input to the correct type. - * @returns A ParseResult containing either a valid result or list of issues. - */ -export function parseEntry( - schema: S, - input: unknown, - options: EntryParseOptions = DEFAULT_ENTRIES_PARSE_OPTIONS, - path: string[] = [], - typeValidator: ( - data: unknown, - path: string[] - ) => ParseResult, - coercer: (data: unknown) => unknown -): ParseResult { - const issues: PodspecBaseIssue[] = []; - - const checkedType = typeValidator( - options.coerce ? coercer(input) : input, - path - ); - if (!checkedType.isValid) { - return FAILURE(checkedType.issues); - } - - const { value } = checkedType; - - const checkedPodValue = safeCheckPODValue(path, value); - if (!checkedPodValue.isValid) { - if (options.exitEarly) { - return FAILURE(checkedPodValue.issues); - } else { - issues.push(...checkedPodValue.issues); - } - } - - if (schema.type !== "null") { - const checkedForMatches = safeMembershipChecks( - schema, - value, - options, - path - ); - if (!checkedForMatches.isValid) { - if (options.exitEarly) { - return FAILURE(checkedForMatches.issues); - } else { - issues.push(...checkedForMatches.issues); - } - } - } - - if (schema.type === "cryptographic" || schema.type === "int") { - if (schema.inRange) { - const { min, max } = schema.inRange; - const valueToCheck = (value as PODIntValue | PODCryptographicValue).value; - try { - checkBigintBounds("", valueToCheck, min, max); - } catch (_error) { - const issue: PodspecNotInRangeIssue = { - code: IssueCode.not_in_range, - value: valueToCheck, - min, - max, - path - }; - if (options.exitEarly) { - return FAILURE([issue]); - } else { - issues.push(issue); - } - } - } - } - - if (schema.type === "date") { - if (schema.inRange) { - const { min, max } = schema.inRange; - checkBigintBounds( - "", - BigInt((value as PODDateValue).value.getTime()), - min, - max - ); - } - } - - return issues.length > 0 ? FAILURE(issues) : SUCCESS(value); -} diff --git a/packages/podspec/src/parse/parse_utils.ts b/packages/podspec/src/parse/parse_utils.ts deleted file mode 100644 index cc9edba..0000000 --- a/packages/podspec/src/parse/parse_utils.ts +++ /dev/null @@ -1,361 +0,0 @@ -import type { - PODBooleanValue, - PODBytesValue, - PODCryptographicValue, - PODDateValue, - PODEdDSAPublicKeyValue, - PODIntValue, - PODNullValue, - PODStringValue, - PODValue -} from "@pcd/pod"; -import { - checkBigintBounds, - checkPODValue, - checkPublicKeyFormat -} from "@pcd/pod/podChecks"; -import type { - PodspecBaseIssue, - PodspecExcludedByListIssue, - PodspecExcludedByTupleListIssue, - PodspecInvalidPodValueIssue, - PodspecInvalidTupleEntryIssue, - PodspecNotInListIssue, - PodspecNotInTupleListIssue -} from "../error.js"; -import { IssueCode } from "../error.js"; -import type { EntriesSchema } from "../schemas/entries.js"; -import type { DefinedEntrySchema } from "../schemas/entry.js"; -import type { NullSchema } from "../schemas/null.js"; -import type { PODTupleSchema } from "../schemas/pod.js"; -import type { EntriesParseOptions } from "./entries.js"; -import type { EntryParseOptions } from "./entry.js"; - -type ParseSuccess = { - value: T; - isValid: true; -}; - -type ParseFailure = { - issues: PodspecBaseIssue[]; - isValid: false; -}; - -/** - * A ParseResult is a container for either a valid value or a list of issues. - */ -export type ParseResult = ParseSuccess | ParseFailure; - -/** - * Creates a ParseFailure containing a list of issues. - * - * @param errors The issues to include in the failure. - * @returns A ParseFailure containing the issues. - */ -export function FAILURE(errors: PodspecBaseIssue[]): ParseFailure { - return { isValid: false, issues: errors ?? [] }; -} - -/** - * Creates a ParseSuccess containing a valid value. - * - * @param value The value to include in the success. - * @returns A ParseSuccess containing the value. - */ -export function SUCCESS(value: T): ParseSuccess { - return { - isValid: true, - value - }; -} - -/** - * Wraps {@link checkPODValue} in a ParseResult, rather than throwing an exception. - * - * @param path The path leading to this value. - * @param podValue The value to check. - * @returns A ParseResult containing either a valid result or list of issues. - */ -export function safeCheckPODValue( - path: string[], - podValue: PODValue -): ParseResult { - try { - checkPODValue(path.join("."), podValue); - } catch (error) { - const issue: PodspecInvalidPodValueIssue = { - code: IssueCode.invalid_pod_value, - value: podValue, - reason: (error as Error).message, - path - }; - return FAILURE([issue]); - } - - return SUCCESS(podValue); -} - -/** - * Wraps {@link checkBigintBounds} in a ParseResult, rather than throwing an exception. - * - * @param path The path leading to this value. - * @param podValue The value to check. - * @param min The minimum value for the range. - * @param max The maximum value for the range. - * @returns A ParseResult containing either a valid result or list of issues. - */ -export function safeCheckBigintBounds< - T extends PODCryptographicValue | PODIntValue ->(path: string[], podValue: T, min: bigint, max: bigint): ParseResult { - try { - const value = podValue.value; - checkBigintBounds(path.join("."), value, min, max); - } catch (error) { - const issue: PodspecInvalidPodValueIssue = { - code: IssueCode.invalid_pod_value, - value: podValue, - reason: (error as Error).message, - path - }; - return FAILURE([issue]); - } - - return SUCCESS(podValue); -} - -/** - * Wraps {@link checkPublicKeyFormat} in a ParseResult, rather than throwing an exception. - * - * @param path The path leading to this value. - * @param podValue The value to check. - * @returns A ParseResult containing either a valid result or list of issues. - */ -export function safeCheckPublicKeyFormat( - path: string[], - podValue: PODEdDSAPublicKeyValue -): ParseResult { - try { - checkPublicKeyFormat(podValue.value, path.join(".")); - } catch (error) { - const issue: PodspecInvalidPodValueIssue = { - code: IssueCode.invalid_pod_value, - value: podValue, - reason: (error as Error).message, - path - }; - return FAILURE([issue]); - } - - return SUCCESS(podValue); -} - -/** - * Checks if two PODValues are equal. - * - * @param a The first value to compare. - * @param b The second value to compare. - * @returns True if the values are equal, false otherwise. - */ -export function isEqualPODValue(a: PODValue, b: PODValue): boolean { - if (a.type !== b.type) { - return false; - } - - switch (a.type) { - case "string": - case "int": - case "cryptographic": - case "eddsa_pubkey": - case "boolean": - case "null": - return a.value === b.value; - case "date": - return a.value.getTime() === (b as PODDateValue).value.getTime(); - case "bytes": - return ( - a.value.length === (b as PODBytesValue).value.length && - a.value.every( - (byte, index) => byte === (b as PODBytesValue).value[index] - ) - ); - } -} - -/** - * Checks if a PODValue is a member of a list of PODValues. - * - * @param schema The schema to check against. - * @param value The value to check. - * @param options The parse options. - * @param path The path leading to this value. - * @returns A ParseResult containing either a valid result or list of issues. - */ -export function safeMembershipChecks< - S extends Exclude, - T extends PODValue ->( - schema: S, - value: T, - options: EntryParseOptions, - path: string[] -): ParseResult { - const issues: PodspecBaseIssue[] = []; - const isMatchingValue = (otherValue: PODValue): boolean => - isEqualPODValue(value, otherValue); - - if (schema.isMemberOf && !schema.isMemberOf.find(isMatchingValue)) { - const issue: PodspecNotInListIssue = { - code: IssueCode.not_in_list, - value: value, - list: schema.isMemberOf, - path - }; - if (options.exitEarly) { - return FAILURE([issue]); - } else { - issues.push(issue); - } - } - - if ( - schema.isNotMemberOf && - schema.isNotMemberOf.length > 0 && - schema.isNotMemberOf.find(isMatchingValue) - ) { - const issue: PodspecExcludedByListIssue = { - code: IssueCode.excluded_by_list, - value: value, - list: schema.isNotMemberOf, - path - }; - if (options.exitEarly) { - return FAILURE([issue]); - } else { - issues.push(issue); - } - } - - return issues.length > 0 ? FAILURE(issues) : SUCCESS(value); -} - -/** - * Checks if the tuples specified for a set of entries match the values of - * the entries provided. - * - * @param output The output to check. - * @param tupleSchema The schema to check against. - * @param options The parse options. - * @param path The path leading to this value. - * @returns A ParseResult containing either a valid result or list of issues. - */ -export function safeCheckTuple( - output: Record, - tupleSchema: PODTupleSchema, - options: EntriesParseOptions, - path: string[] -): ParseResult> { - const issues: PodspecBaseIssue[] = []; - const outputKeys = Object.keys(output); - const tuple: PODValue[] = []; - let validTuple = true; - - for (const k of tupleSchema.entries) { - const entryKey = k.toString(); - if (!outputKeys.includes(entryKey)) { - const issue: PodspecInvalidTupleEntryIssue = { - code: IssueCode.invalid_tuple_entry, - name: entryKey, - path: [...path, entryKey] - }; - if (options.exitEarly) { - return FAILURE([issue]); - } else { - issues.push(issue); - } - validTuple = false; - } - tuple.push(output[entryKey]!); - } - - if (validTuple) { - if (tupleSchema.isMemberOf) { - const matched = tupleSchema.isMemberOf.some((tupleToCheck) => - tupleToCheck.every((val, index) => isEqualPODValue(val, tuple[index]!)) - ); - if (!matched) { - const issue: PodspecNotInTupleListIssue = { - code: IssueCode.not_in_tuple_list, - value: tuple, - list: tupleSchema.isMemberOf, - path - }; - if (options.exitEarly) { - return FAILURE([issue]); - } else { - issues.push(issue); - } - } - } - - if (tupleSchema.isNotMemberOf && tupleSchema.isNotMemberOf.length > 0) { - const matched = tupleSchema.isNotMemberOf.some((tupleToCheck) => - tupleToCheck.every((val, index) => isEqualPODValue(val, tuple[index]!)) - ); - - if (matched) { - const issue: PodspecExcludedByTupleListIssue = { - code: IssueCode.excluded_by_tuple_list, - value: tuple, - list: tupleSchema.isNotMemberOf, - path: [...path] - }; - if (options.exitEarly) { - return FAILURE([issue]); - } else { - issues.push(issue); - } - } - } - } - - return issues.length > 0 ? FAILURE(issues) : SUCCESS(output); -} - -/** - * Mapping of PODValue types to their TypeScript native equivalents. - */ -export type PODValueNativeTypes = { - string: string; - int: bigint; - cryptographic: bigint; - eddsa_pubkey: string; - boolean: boolean; - bytes: Uint8Array; - date: Date; - null: null; -}; - -export type PODValueCoerceableNativeTypes = { - string: string; - int: bigint | number | boolean; - cryptographic: bigint | number | boolean; - eddsa_pubkey: string; - boolean: boolean; - bytes: Uint8Array; - date: Date; - null: null; -}; - -/** - * Mapping of PODValue type names to their PODValue data types. - */ -export type PODValueTypeNameToPODValue = { - string: PODStringValue; - int: PODIntValue; - cryptographic: PODCryptographicValue; - eddsa_pubkey: PODEdDSAPublicKeyValue; - boolean: PODBooleanValue; - bytes: PODBytesValue; - date: PODDateValue; - null: PODNullValue; -}; diff --git a/packages/podspec/src/parse/pod.ts b/packages/podspec/src/parse/pod.ts deleted file mode 100644 index e3b79f0..0000000 --- a/packages/podspec/src/parse/pod.ts +++ /dev/null @@ -1,370 +0,0 @@ -import type { POD, PODContent, PODEntries, PODValue } from "@pcd/pod"; -import type { - PodspecSignatureExcludedByListIssue, - PodspecSignatureNotInListIssue, - PodspecSignerExcludedByListIssue, - PodspecSignerNotInListIssue -} from "../error.js"; -import { IssueCode, PodspecError } from "../error.js"; -import type { - ProofConfigOwner, - ProofConfigPODSchema -} from "../gpc/proof_request.js"; -import type { EntriesSchema } from "../schemas/entries.js"; -import type { PODSchema } from "../schemas/pod.js"; -import type { EntriesOutputType } from "../type_inference.js"; -import type { EntriesParseOptions } from "./entries.js"; -import { - DEFAULT_ENTRIES_PARSE_OPTIONS, - EntriesSpec, - safeParseEntries -} from "./entries.js"; -import type { ParseResult } from "./parse_utils.js"; -import { FAILURE, SUCCESS, safeCheckTuple } from "./parse_utils.js"; - -export interface PODData { - entries: PODEntries; - signature: string; - signerPublicKey: string; -} - -/** - * "Strong" PODContent is an extension of PODContent which extends the - * `asEntries()` method to return a strongly-typed PODEntries. - */ -interface StrongPODContent extends PODContent { - asEntries(): T; -} - -/** - * A "strong" POD is a POD with a strongly-typed entries. - */ -export interface StrongPOD extends POD { - content: StrongPODContent; -} - -type SchemaIdentityFn = ( - s: PODSchema -) => PODSchema; - -/** - * A PodSpec is a specification for a POD, including its schema and any - * additional constraints. - */ -export class PodSpec { - public schema: PODSchema; - - /** - * Create a new PodSpec. The constructor is private, see {@link create} for - * public creation. - * - * @param schema The schema for the POD. - */ - private constructor(schema: PODSchema) { - this.schema = Object.freeze(schema); - } - - /** - * Parse a POD according to this PodSpec. - * Returns a {@link ParseResult} rather than throwing an exception. - * - * @param input The POD to parse. - * @param options The options to use when parsing. - * @param path The path to use when parsing. - * @returns A result containing either a valid value or a list of errors. - */ - public safeParse( - input: POD, - options: EntriesParseOptions = DEFAULT_ENTRIES_PARSE_OPTIONS, - path: string[] = [] - ): ParseResult>> { - return safeParsePod(this.schema, input, options, path); - } - - /** - * Identical to {@link safeParse}, except it throws an exception if errors - * are found, rather than returning a {@link ParseResult}. - * - * @param input The POD to parse. - * @param options The options to use when parsing. - * @param path The path to use when parsing. - * @returns The parsed POD. - */ - public parse( - input: POD, - options: EntriesParseOptions = DEFAULT_ENTRIES_PARSE_OPTIONS, - path: string[] = [] - ): StrongPOD> { - const result = this.safeParse(input, options, path); - if (result.isValid) { - return result.value; - } else { - throw new PodspecError(result.issues); - } - } - - /** - * Parse input data as PODEntries according to this PodSpec's schema. - * Returns a {@link ParseResult} rather than throwing an exception. - * - * @param input The entries to parse. - * @param options The options to use when parsing. - * @param path The path to use when parsing. - * @returns A result containing either a valid value or a list of errors. - */ - public safeParseEntries( - input: Record, - options: EntriesParseOptions = DEFAULT_ENTRIES_PARSE_OPTIONS, - path: string[] = [] - ): ParseResult> { - return EntriesSpec.create(this.schema.entries as E).safeParse( - input, - options, - path - ); - } - - /** - * Identical to {@link safeParseEntries}, except it throws an exception if errors - * are found, rather than returning a {@link ParseResult}. - * - * @param input The entries to parse. - * @param options The options to use when parsing. - * @param path The path to use when parsing. - * @returns The parsed entries. - */ - public parseEntries( - input: Record, - options: EntriesParseOptions = DEFAULT_ENTRIES_PARSE_OPTIONS, - path: string[] = [] - ): EntriesOutputType { - const result = this.safeParseEntries(input, options, path); - if (result.isValid) { - return result.value; - } else { - throw new PodspecError(result.issues); - } - } - - /** - * Tests an array of PODs against this Podspec. - * Useful for query-like operations where you need to find the matching PODs - * from a list. - * - * @param input The PODs to test - * @returns An array of matches and their indexes within the input array. - */ - public query(input: POD[]): { - matches: StrongPOD>[]; - matchingIndexes: number[]; - } { - const matchingIndexes: number[] = []; - const matches: StrongPOD>[] = []; - const signatures = new Set(); - for (const [index, pod] of input.entries()) { - const result = this.safeParse(pod, { exitEarly: true }); - if (result.isValid) { - if (signatures.has(result.value.signature)) { - continue; - } - signatures.add(result.value.signature); - matchingIndexes.push(index); - matches.push(result.value); - } - } - return { - matches, - matchingIndexes - }; - } - - /** - * Extends the current PodSpec with a new schema. - * - * The "updater" function is provided by the caller, and is responsible for - * creating a new schema. It is passed a clone of current schema and a - * function which, strictly speaking, does nothing and is used only to - * enforce types. In the updater function, the caller should return the - * result of calling `f` on the updated schema. - * - * @param updater A function which takes the current schema and returns a new schema. - * @returns A new PodSpec with the extended schema. - */ - public extend( - updater: (schema: PODSchema, f: SchemaIdentityFn) => PODSchema - ): PodSpec { - const clone = structuredClone(this.schema); - const newSchema = updater(clone, (s) => s); - return PodSpec.create(newSchema); - } - - public cloneSchema(): PODSchema { - return structuredClone(this.schema); - } - - public proofConfig({ - revealed, - owner - }: { - revealed?: Partial< - Record - >; - owner?: ProofConfigOwner; - }): ProofConfigPODSchema { - return { - pod: this.schema, - revealed, - owner - }; - } - - /** - * Creates a new PodSpec instance. - * - * @param schema The schema defining the valid POD. - * @returns A new PodSpec instance. - */ - public static create( - schema: PODSchema - ): PodSpec { - return new PodSpec(schema); - } -} - -/** - * Exported version of static create method, for convenience. - */ -export const pod = (schema: PODSchema) => - PodSpec.create(schema); - -/** - * Parses the POD and its entries, returning a {@link ParseResult}. - * - * @param schema The schema defining the valid POD. - * @param data A POD. - * @param options Options determining how parsing is performed. - * @param path The path to this object. - * @returns A result containing either a valid value or a list of errors. - */ -export function safeParsePod( - schema: PODSchema, - data: POD, - options: EntriesParseOptions = DEFAULT_ENTRIES_PARSE_OPTIONS, - path: string[] = [] -): ParseResult>> { - const entriesResult = safeParseEntries( - schema.entries, - data.content.asEntries(), - options, - [...path, "entries"] - ); - - if (!entriesResult.isValid) { - return FAILURE(entriesResult.issues); - } - - const issues = []; - - if (schema.signature) { - const { isMemberOf, isNotMemberOf } = schema.signature; - if (isMemberOf && !isMemberOf.includes(data.signature)) { - const issue: PodspecSignatureNotInListIssue = { - code: IssueCode.signature_not_in_list, - signature: data.signature, - list: isMemberOf, - path - }; - if (options.exitEarly) { - return FAILURE([issue]); - } else { - issues.push(issue); - } - } - - if ( - isNotMemberOf && - isNotMemberOf.length > 0 && - isNotMemberOf.includes(data.signature) - ) { - const issue: PodspecSignatureExcludedByListIssue = { - code: IssueCode.signature_excluded_by_list, - signature: data.signature, - list: isNotMemberOf, - path - }; - if (options.exitEarly) { - return FAILURE([issue]); - } else { - issues.push(issue); - } - } - } - - if (schema.signerPublicKey) { - const { isMemberOf, isNotMemberOf } = schema.signerPublicKey; - if (isMemberOf && !isMemberOf.includes(data.signerPublicKey)) { - const issue: PodspecSignerNotInListIssue = { - code: IssueCode.signer_not_in_list, - signer: data.signerPublicKey, - list: isMemberOf, - path - }; - if (options.exitEarly) { - return FAILURE([issue]); - } else { - issues.push(issue); - } - } - - if ( - isNotMemberOf && - isNotMemberOf.length > 0 && - isNotMemberOf.includes(data.signerPublicKey) - ) { - const issue: PodspecSignerExcludedByListIssue = { - code: IssueCode.signer_excluded_by_list, - signer: data.signerPublicKey, - list: isNotMemberOf, - path - }; - if (options.exitEarly) { - return FAILURE([issue]); - } else { - issues.push(issue); - } - } - } - - if (schema.tuples) { - for (const [tupleIndex, tupleSchema] of schema.tuples.entries()) { - const result = safeCheckTuple( - { - ...entriesResult.value, - $signerPublicKey: { - type: "eddsa_pubkey", - value: data.signerPublicKey - } - } as Record, - tupleSchema, - options, - [...path, "$tuples", tupleIndex.toString()] - ); - - if (!result.isValid) { - if (options.exitEarly) { - return FAILURE(result.issues); - } else { - issues.push(...result.issues); - } - } - } - } - - return issues.length > 0 - ? FAILURE(issues) - : SUCCESS( - // We can return the POD as is, since we know it matches the spec, but - //with a type that tells TypeScript what entries it has - data as StrongPOD> - ); -} diff --git a/packages/podspec/src/pod_value_utils.ts b/packages/podspec/src/pod_value_utils.ts index 9e619d1..4a9c085 100644 --- a/packages/podspec/src/pod_value_utils.ts +++ b/packages/podspec/src/pod_value_utils.ts @@ -6,7 +6,7 @@ import type { PODEdDSAPublicKeyValue, PODIntValue, PODNullValue, - PODStringValue + PODStringValue, } from "@pcd/pod"; // Some terse utility functions for converting native JavaScript values, or diff --git a/packages/podspec/src/processors/db/podDB.ts b/packages/podspec/src/processors/db/podDB.ts new file mode 100644 index 0000000..e177b39 --- /dev/null +++ b/packages/podspec/src/processors/db/podDB.ts @@ -0,0 +1,204 @@ +import type { POD } from "@pcd/pod"; +import type { NamedPODSpecs, PODGroupSpec } from "../../builders/group.js"; +import type { PODSpec } from "../../builders/pod.js"; +import type { EntryTypes } from "../../builders/types/entries.js"; +import type { StatementMap } from "../../builders/types/statements.js"; +import type { + NamedStrongPODs, + PODEntriesFromEntryTypes, + StrongPOD, +} from "../../spec/types.js"; +import { groupValidator } from "../validate/groupValidator.js"; +import { podValidator } from "../validate/podValidator.js"; + +interface PODIndexes { + // Index for finding signatures by entry name + type + byEntryNameAndType: Map>>; + // Index for finding signatures by value hash + // byValueHash: Map>; + // Index for finding PODs by signature + bySignature: Map; +} + +export class PODDB { + private pods = new Set(); + private indexes: PODIndexes = { + byEntryNameAndType: new Map(), + // byValueHash: new Map(), + bySignature: new Map(), + }; + + /** + * Insert a single POD into the database + */ + public insert(pod: POD): void { + this.pods.add(pod); + this.indexPOD(pod); + } + + /** + * Insert multiple PODs into the database + */ + public insertMany(pods: POD[]): void { + pods.forEach((pod) => this.insert(pod)); + } + + /** + * Index a POD's entries for quick lookups + */ + private indexPOD(pod: POD): void { + const entries = pod.content.listEntries(); + for (const entry of entries) { + // Index by entry name + type + if (!this.indexes.byEntryNameAndType.has(entry.name)) { + this.indexes.byEntryNameAndType.set(entry.name, new Map()); + } + const typeMap = this.indexes.byEntryNameAndType.get(entry.name)!; + if (!typeMap.has(entry.value.type)) { + typeMap.set(entry.value.type, new Set()); + } + typeMap.get(entry.value.type)!.add(pod.signature); + + // Index by value hash + // const hash = podValueHash(entry.value); + // if (!this.indexes.byValueHash.has(hash)) { + // this.indexes.byValueHash.set(hash, new Set()); + // } + // this.indexes.byValueHash.get(hash)!.add(pod.signature); + + // Index by signature + if (!this.indexes.bySignature.has(pod.signature)) { + this.indexes.bySignature.set(pod.signature, pod); + } + } + } + + /** + * Query PODs that match a PODSpec + */ + public queryBySpec( + spec: PODSpec + ): StrongPOD>[] { + const initialResults = new Set(); + + // Find all PODs that have the required entries + for (const [entryName, entryType] of Object.entries(spec.entries)) { + const typeMap = this.indexes.byEntryNameAndType.get(entryName); + if (!typeMap) { + initialResults.clear(); + break; + } + + const signaturesForNameAndType = typeMap.get(entryType); + if (!signaturesForNameAndType) { + initialResults.clear(); + break; + } + + if (initialResults.size === 0) { + signaturesForNameAndType.forEach((signature) => + initialResults.add(signature) + ); + } else { + for (const signature of initialResults) { + if (!signaturesForNameAndType.has(signature)) { + initialResults.delete(signature); + } + } + } + + if (initialResults.size === 0) { + break; + } + } + + const pods = Array.from(initialResults).map( + (signature) => this.indexes.bySignature.get(signature)! + ); + + const finalResults = []; + const validator = podValidator(spec); + + for (const pod of pods) { + // This is not fully optimal as this will repeat some of the checks we + // did by looking up via indexes, but it will also apply any statements + // that are not covered by the indexes. We've still massively reduced + // the number of PODs we need to check by using the indexes. + if (validator.check(pod)) { + finalResults.push(pod); + } + } + + return finalResults; + } + + /** + * Query PODs that match a PODGroupSpec + */ + public queryByGroupSpec

( + groupSpec: PODGroupSpec + ): NamedStrongPODs

[] { + // Get candidates for each slot + const candidates = new Map>(); + + for (const [name, spec] of Object.entries(groupSpec.pods)) { + const result = this.queryBySpec(spec); + candidates.set(name, new Set(result)); + } + + // Generate all possible combinations + // This is a _very_ naive implementation that will quickly become + // infeasible as the number of slots and PODs grows. + // However, we can implement specialized checks for certain statements + // which will greatly speed this up. + const slotNames = Array.from(candidates.keys()); + const combinations = this.generateCombinations

(slotNames, candidates); + + const validator = groupValidator(groupSpec); + + return Array.from(combinations).filter((combo) => { + return validator.check(combo); + }); + } + + private generateCombinations

( + slotNames: string[], + candidates: Map> + ): Set> { + // Start with an empty combination + let results: Map[] = [new Map()]; + + // For each slot + for (const slotName of slotNames) { + const slotCandidates = candidates.get(slotName)!; + const newResults: Map[] = []; + + // For each existing partial combination + for (const partial of results) { + // For each candidate POD in this slot + for (const pod of slotCandidates) { + // Create a new combination with this POD added + const newCombination = new Map(partial); + newCombination.set(slotName, pod); + newResults.push(newCombination); + } + } + + results = newResults; + } + + // Convert to the expected return type + return new Set(results.map((combo) => Object.fromEntries(combo))) as Set< + NamedStrongPODs

+ >; + } + + /** + * Clear all PODs and indexes + */ + public clear(): void { + this.pods.clear(); + this.indexes.byEntryNameAndType.clear(); + // this.indexes.byValueHash.clear(); + } +} diff --git a/packages/podspec/src/processors/proof.ts b/packages/podspec/src/processors/proof.ts new file mode 100644 index 0000000..e69de29 diff --git a/packages/podspec/src/processors/proof/proofRequest.ts b/packages/podspec/src/processors/proof/proofRequest.ts new file mode 100644 index 0000000..9343d2c --- /dev/null +++ b/packages/podspec/src/processors/proof/proofRequest.ts @@ -0,0 +1,13 @@ +import type { NamedPODSpecs, PODGroupSpec } from "../../builders/group.js"; +import type { StatementMap } from "../../builders/types/statements.js"; + +export interface ProofRequest

{ + gpcVersion: number; + groupSpec: PODGroupSpec; +} + +export function proofRequest

( + spec: PODGroupSpec +) { + return spec; +} diff --git a/packages/podspec/src/processors/query.ts b/packages/podspec/src/processors/query.ts new file mode 100644 index 0000000..e69de29 diff --git a/packages/podspec/src/processors/validate/EntrySource.ts b/packages/podspec/src/processors/validate/EntrySource.ts new file mode 100644 index 0000000..0df390d --- /dev/null +++ b/packages/podspec/src/processors/validate/EntrySource.ts @@ -0,0 +1,233 @@ +import type { POD, PODEntries, PODName, PODValue } from "@pcd/pod"; +import type { NamedPODSpecs, PODGroupSpec } from "../../builders/group.js"; +import type { PODSpec } from "../../builders/pod.js"; +import type { EntryTypes } from "../../builders/types/entries.js"; +import type { StatementMap } from "../../builders/types/statements.js"; +import { + IssueCode, + type ValidationBaseIssue, + type ValidationMissingEntryIssue, + type ValidationMissingPodIssue, + type ValidationTypeMismatchIssue, + type ValidationUnexpectedInputEntryIssue, + type ValidationUnexpectedInputPodIssue, +} from "./issues.js"; +import type { ValidateOptions } from "./podValidator.js"; + +export interface EntrySource { + getEntry(entryName: string): PODValue | undefined; + getEntryTypeFromSpec(entryName: string): PODValue["type"] | undefined; + audit(path: string[], options: ValidateOptions): ValidationBaseIssue[]; +} + +function auditEntries( + podEntries: PODEntries, + spec: PODSpec, + path: string[], + { exitOnError, strict }: ValidateOptions +): ValidationBaseIssue[] { + const issues = []; + + for (const [key, entryType] of Object.entries(spec.entries)) { + if (!Object.prototype.hasOwnProperty.call(podEntries, key)) { + issues.push({ + code: IssueCode.missing_entry, + path: [...path, key], + key, + } satisfies ValidationMissingEntryIssue); + if (exitOnError) { + return issues; + } + } + if (podEntries[key]?.type !== entryType) { + issues.push({ + code: IssueCode.type_mismatch, + path: [...path, key], + expectedType: entryType, + } satisfies ValidationTypeMismatchIssue); + if (exitOnError) { + return issues; + } + } + } + + if (strict) { + for (const key of Object.keys(podEntries)) { + if (!Object.prototype.hasOwnProperty.call(spec.entries, key)) { + issues.push({ + code: IssueCode.unexpected_input_entry, + path: [...path, key], + key, + } satisfies ValidationUnexpectedInputEntryIssue); + if (exitOnError) { + return issues; + } + } + } + } + return issues; +} + +export class EntrySourcePodSpec implements EntrySource { + private podSpec: PODSpec; + private pod: POD; + + constructor(podSpec: PODSpec, pod: POD) { + this.podSpec = podSpec; + this.pod = pod; + } + + public audit( + path: string[], + options: ValidateOptions + ): ValidationBaseIssue[] { + return auditEntries( + this.pod.content.asEntries(), + this.podSpec, + path, + options + ); + } + + public getEntry(entryName: string): PODValue | undefined { + if (entryName === "$signerPublicKey") { + return { + type: "eddsa_pubkey", + value: this.pod.signerPublicKey, + }; + } else if (entryName === "$contentID") { + return { + type: "cryptographic", + value: this.pod.content.contentID, + }; + } else { + return this.pod.content.getValue(entryName); + } + } + + public getEntryTypeFromSpec(entryName: string): PODValue["type"] | undefined { + if (entryName === "$signerPublicKey") { + return "eddsa_pubkey"; + } else if (entryName === "$contentID") { + return "cryptographic"; + } else { + return this.podSpec.entries[entryName]; + } + } +} + +export class EntrySourcePodGroupSpec implements EntrySource { + private podGroupSpec: PODGroupSpec; + private pods: Record; + + constructor( + podGroupSpec: PODGroupSpec, + pods: Record + ) { + this.podGroupSpec = podGroupSpec; + this.pods = pods; + } + + /** + * Audit the pod group for missing entries and pods. + * @param path - The path to the pod group. + * @returns - An array of issues. + */ + public audit( + path: string[], + options: ValidateOptions + ): ValidationBaseIssue[] { + const issues = []; + + for (const [podName, podSpec] of Object.entries(this.podGroupSpec.pods)) { + const pod = this.pods[podName]; + if (!pod) { + issues.push({ + code: IssueCode.missing_pod, + path: [...path, podName], + podName, + } satisfies ValidationMissingPodIssue); + if (options.exitOnError) { + return issues; + } + continue; + } + + issues.push( + ...auditEntries( + pod.content.asEntries(), + podSpec, + [...path, podName], + options + ) + ); + + if (options.exitOnError && issues.length > 0) { + return issues; + } + } + + if (options.strict) { + for (const podName of Object.keys(this.pods)) { + if ( + !Object.prototype.hasOwnProperty.call(this.podGroupSpec.pods, podName) + ) { + issues.push({ + code: IssueCode.unexpected_input_pod, + path: [...path, podName], + podName, + } satisfies ValidationUnexpectedInputPodIssue); + if (options.exitOnError) { + return issues; + } + } + } + } + return issues; + } + + public getEntry(qualifiedEntryName: string): PODValue | undefined { + const [podName, entryName] = qualifiedEntryName.split("."); + if ( + podName === undefined || + entryName === undefined || + !this.pods[podName] + ) { + return undefined; + } + + if (entryName === "$signerPublicKey") { + return { + type: "eddsa_pubkey", + value: this.pods[podName].signerPublicKey, + }; + } else if (entryName === "$contentID") { + return { + type: "cryptographic", + value: this.pods[podName].content.contentID, + }; + } else { + return this.pods[podName].content.getValue(entryName); + } + } + + public getEntryTypeFromSpec( + qualifiedEntryName: string + ): PODValue["type"] | undefined { + const [podName, entryName] = qualifiedEntryName.split("."); + if ( + podName === undefined || + entryName === undefined || + !this.podGroupSpec.pods[podName] + ) { + return undefined; + } + if (entryName === "$signerPublicKey") { + return "eddsa_pubkey"; + } else if (entryName === "$contentID") { + return "cryptographic"; + } else { + return this.podGroupSpec.pods[podName].entries[entryName]; + } + } +} diff --git a/packages/podspec/src/processors/validate/checks/checkEqualsEntry.ts b/packages/podspec/src/processors/validate/checks/checkEqualsEntry.ts new file mode 100644 index 0000000..82c9029 --- /dev/null +++ b/packages/podspec/src/processors/validate/checks/checkEqualsEntry.ts @@ -0,0 +1,96 @@ +import type { EqualsEntry } from "../../../builders/types/statements.js"; +import type { EntrySource } from "../EntrySource.js"; +import type { + ValidationBaseIssue, + ValidationStatementNegativeResultIssue, +} from "../issues.js"; +import { IssueCode } from "../issues.js"; +import { valueIsEqual } from "../utils.js"; + +export function checkEqualsEntry( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + statement: EqualsEntry, + statementName: string, + path: string[], + entrySource: EntrySource, + _exitOnError: boolean +): ValidationBaseIssue[] { + const [leftEntry, rightEntry] = statement.entries; + const entry1 = entrySource.getEntry(leftEntry); + const entry2 = entrySource.getEntry(rightEntry); + + const issues = []; + + // TODO pre-process? might need more detailed issue type for invalid statements + if (entry1 === undefined) { + issues.push({ + code: IssueCode.invalid_statement, + statementName: statementName, + statementType: statement.type, + entries: statement.entries, + path: [...path, statementName], + }); + return issues; + } + if (entry2 === undefined) { + issues.push({ + code: IssueCode.invalid_statement, + statementName: statementName, + statementType: statement.type, + entries: statement.entries, + path: [...path, statementName], + }); + return issues; + } + + const entry1Type = entrySource.getEntryTypeFromSpec(leftEntry); + const entry2Type = entrySource.getEntryTypeFromSpec(rightEntry); + + if (entry1Type !== entry2Type) { + issues.push({ + code: IssueCode.invalid_statement, + statementName: statementName, + statementType: statement.type, + entries: statement.entries, + path: [...path, statementName], + }); + return issues; + } + + if (entry1Type !== entry1.type) { + issues.push({ + code: IssueCode.invalid_statement, + statementName: statementName, + statementType: statement.type, + entries: statement.entries, + path: [...path, statementName], + }); + return issues; + } + + if (entry1Type !== entry2.type) { + issues.push({ + code: IssueCode.invalid_statement, + statementName: statementName, + statementType: statement.type, + entries: statement.entries, + path: [...path, statementName], + }); + return issues; + } + + const isEqual = valueIsEqual(entry1.value, entry2.value); + + if (!isEqual) { + const issue = { + code: IssueCode.statement_negative_result, + statementName: statementName, + statementType: statement.type, + entries: statement.entries, + path: [...path, statementName], + } satisfies ValidationStatementNegativeResultIssue; + return [issue]; + } + + return []; +} diff --git a/packages/podspec/src/processors/validate/checks/checkGreaterThan.ts b/packages/podspec/src/processors/validate/checks/checkGreaterThan.ts new file mode 100644 index 0000000..75ae8aa --- /dev/null +++ b/packages/podspec/src/processors/validate/checks/checkGreaterThan.ts @@ -0,0 +1,102 @@ +import { isPODArithmeticValue } from "@pcd/pod"; +import type { GreaterThan } from "../../../builders/types/statements.js"; +import type { EntrySource } from "../EntrySource.js"; +import { IssueCode } from "../issues.js"; + +export function checkGreaterThan( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + statement: GreaterThan, + statementName: string, + path: string[], + entrySource: EntrySource, + _exitOnError: boolean +) { + const [leftEntry, rightEntry] = statement.entries; + const entry1 = entrySource.getEntry(leftEntry); + const entry2 = entrySource.getEntry(rightEntry); + + const issues = []; + + // TODO pre-process? might need more detailed issue type for invalid statements + if (entry1 === undefined) { + issues.push({ + code: IssueCode.invalid_statement, + statementName: statementName, + statementType: statement.type, + entries: statement.entries, + path: [...path, statementName], + }); + return issues; + } + if (entry2 === undefined) { + issues.push({ + code: IssueCode.invalid_statement, + statementName: statementName, + statementType: statement.type, + entries: statement.entries, + path: [...path, statementName], + }); + return issues; + } + + const entry1Type = entrySource.getEntryTypeFromSpec(leftEntry); + const entry2Type = entrySource.getEntryTypeFromSpec(rightEntry); + + // TODO this may be too restrictive + if (entry1Type !== entry2Type) { + issues.push({ + code: IssueCode.invalid_statement, + statementName: statementName, + statementType: statement.type, + entries: statement.entries, + path: [...path, statementName], + }); + return issues; + } + + if (entry1Type !== entry1.type) { + issues.push({ + code: IssueCode.invalid_statement, + statementName: statementName, + statementType: statement.type, + entries: statement.entries, + path: [...path, statementName], + }); + return issues; + } + + if (entry1Type !== entry2.type) { + issues.push({ + code: IssueCode.invalid_statement, + statementName: statementName, + statementType: statement.type, + entries: statement.entries, + path: [...path, statementName], + }); + return issues; + } + + if (!isPODArithmeticValue(entry1) || !isPODArithmeticValue(entry2)) { + issues.push({ + code: IssueCode.invalid_statement, + statementName: statementName, + statementType: statement.type, + entries: statement.entries, + path: [...path, statementName], + }); + return issues; + } + + const isGreaterThan = entry1.value > entry2.value; + + if (!isGreaterThan) { + issues.push({ + code: IssueCode.statement_negative_result, + statementName: statementName, + statementType: statement.type, + entries: statement.entries, + path: [...path, statementName], + }); + } + return issues; +} diff --git a/packages/podspec/src/processors/validate/checks/checkGreaterThanEq.ts b/packages/podspec/src/processors/validate/checks/checkGreaterThanEq.ts new file mode 100644 index 0000000..faa123b --- /dev/null +++ b/packages/podspec/src/processors/validate/checks/checkGreaterThanEq.ts @@ -0,0 +1,102 @@ +import { isPODArithmeticValue } from "@pcd/pod"; +import type { GreaterThanEq } from "../../../builders/types/statements.js"; +import type { EntrySource } from "../EntrySource.js"; +import { IssueCode } from "../issues.js"; + +export function checkGreaterThanEq( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + statement: GreaterThanEq, + statementName: string, + path: string[], + entrySource: EntrySource, + _exitOnError: boolean +) { + const [leftEntry, rightEntry] = statement.entries; + const entry1 = entrySource.getEntry(leftEntry); + const entry2 = entrySource.getEntry(rightEntry); + + const issues = []; + + // TODO pre-process? might need more detailed issue type for invalid statements + if (entry1 === undefined) { + issues.push({ + code: IssueCode.invalid_statement, + statementName: statementName, + statementType: statement.type, + entries: statement.entries, + path: [...path, statementName], + }); + return issues; + } + if (entry2 === undefined) { + issues.push({ + code: IssueCode.invalid_statement, + statementName: statementName, + statementType: statement.type, + entries: statement.entries, + path: [...path, statementName], + }); + return issues; + } + + const entry1Type = entrySource.getEntryTypeFromSpec(leftEntry); + const entry2Type = entrySource.getEntryTypeFromSpec(rightEntry); + + // TODO this may be too restrictive + if (entry1Type !== entry2Type) { + issues.push({ + code: IssueCode.invalid_statement, + statementName: statementName, + statementType: statement.type, + entries: statement.entries, + path: [...path, statementName], + }); + return issues; + } + + if (entry1Type !== entry1.type) { + issues.push({ + code: IssueCode.invalid_statement, + statementName: statementName, + statementType: statement.type, + entries: statement.entries, + path: [...path, statementName], + }); + return issues; + } + + if (entry1Type !== entry2.type) { + issues.push({ + code: IssueCode.invalid_statement, + statementName: statementName, + statementType: statement.type, + entries: statement.entries, + path: [...path, statementName], + }); + return issues; + } + + if (!isPODArithmeticValue(entry1) || !isPODArithmeticValue(entry2)) { + issues.push({ + code: IssueCode.invalid_statement, + statementName: statementName, + statementType: statement.type, + entries: statement.entries, + path: [...path, statementName], + }); + return issues; + } + + const isGreaterThanOrEqual = entry1.value >= entry2.value; + + if (!isGreaterThanOrEqual) { + issues.push({ + code: IssueCode.statement_negative_result, + statementName: statementName, + statementType: statement.type, + entries: statement.entries, + path: [...path, statementName], + }); + } + return issues; +} diff --git a/packages/podspec/src/processors/validate/checks/checkInRange.ts b/packages/podspec/src/processors/validate/checks/checkInRange.ts new file mode 100644 index 0000000..83aff47 --- /dev/null +++ b/packages/podspec/src/processors/validate/checks/checkInRange.ts @@ -0,0 +1,67 @@ +import { isPODArithmeticValue } from "@pcd/pod"; +import type { InRange } from "../../../builders/types/statements.js"; +import type { EntrySource } from "../EntrySource.js"; +import { + IssueCode, + type ValidationBaseIssue, + type ValidationInvalidStatementIssue, + type ValidationStatementNegativeResultIssue, +} from "../issues.js"; + +export function checkInRange( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + statement: InRange, + statementName: string, + path: string[], + entrySource: EntrySource, + _exitOnError: boolean +): ValidationBaseIssue[] { + const [entryName] = statement.entries; + const entry = entrySource.getEntry(entryName); + const issues = []; + + // TODO need an issue type for statement referring to a non-existent entry + // or entry of the wrong type + if (entry === undefined) { + issues.push({ + code: IssueCode.invalid_statement, + statementName: statementName, + statementType: statement.type, + entries: [entryName], + path: [...path, statementName], + } satisfies ValidationInvalidStatementIssue); + return issues; + } + + const isDate = entry.type === "date"; + const min = isDate + ? new Date(parseInt(statement.inRange.min)) + : BigInt(statement.inRange.min); + const max = isDate + ? new Date(parseInt(statement.inRange.max)) + : BigInt(statement.inRange.max); + + if (isPODArithmeticValue(entry)) { + const value = entry.value; + if (value < min || value > max) { + return [ + { + code: IssueCode.statement_negative_result, + statementName: statementName, + statementType: statement.type, + entries: [entryName], + path: [...path, statementName], + } satisfies ValidationStatementNegativeResultIssue as ValidationBaseIssue, + ]; + } + } else { + issues.push({ + code: IssueCode.invalid_statement, + statementName: statementName, + statementType: statement.type, + entries: [entryName], + path: [...path, statementName], + } satisfies ValidationInvalidStatementIssue); + } + return issues; +} diff --git a/packages/podspec/src/processors/validate/checks/checkIsMemberOf.ts b/packages/podspec/src/processors/validate/checks/checkIsMemberOf.ts new file mode 100644 index 0000000..8b14eb0 --- /dev/null +++ b/packages/podspec/src/processors/validate/checks/checkIsMemberOf.ts @@ -0,0 +1,91 @@ +import type { + ValidationBaseIssue, + ValidationInvalidStatementIssue, + ValidationStatementNegativeResultIssue, +} from "../issues.js"; +import { IssueCode } from "../issues.js"; +import type { IsMemberOf } from "../../../builders/types/statements.js"; +import { tupleToPODValueTypeValues, valueIsEqual } from "../utils.js"; +import type { EntrySource } from "../EntrySource.js"; + +function validateIsMemberOfStatement( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + statement: IsMemberOf, + statementName: string, + path: string[], + entrySource: EntrySource +): ValidationInvalidStatementIssue[] { + if (statement.entries.some((entry) => !entrySource.getEntry(entry))) { + return [ + { + code: IssueCode.invalid_statement, + statementName: statementName, + statementType: statement.type, + entries: statement.entries, + path: [...path, statementName], + }, + ]; + } + return []; +} + +export function checkIsMemberOf( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + statement: IsMemberOf, + statementName: string, + path: string[], + entrySource: EntrySource, + exitOnError: boolean +): ValidationBaseIssue[] { + // TODO Move this to a pre-processing step + const issues: ValidationBaseIssue[] = validateIsMemberOfStatement( + statement, + statementName, + path, + entrySource + ); + if (issues.length > 0) { + // Can't proceed if there are issues with the statement + return issues; + } + + const tuple = statement.entries.map( + (entry) => entrySource.getEntry(entry)?.value + ); + + // TODO Move this to a pre-processing step + const tuplesToMatch = tupleToPODValueTypeValues( + statement.isMemberOf, + statement.entries.map( + (entry) => entrySource.getEntryTypeFromSpec(entry) as string + ) + ); + + let match = false; + for (const listMember of tuplesToMatch) { + for (let index = 0; index < tuple.length; index++) { + if (valueIsEqual(tuple[index]!, listMember[index]!)) { + match = true; + break; + } + } + if (match) { + break; + } + } + if (!match) { + const issue = { + code: IssueCode.statement_negative_result, + statementName: statementName, + statementType: statement.type, + entries: statement.entries, + path: [...path, statementName], + } satisfies ValidationStatementNegativeResultIssue; + if (exitOnError) { + return [issue]; + } else { + issues.push(issue); + } + } + return issues; +} diff --git a/packages/podspec/src/processors/validate/checks/checkIsNotMemberOf.ts b/packages/podspec/src/processors/validate/checks/checkIsNotMemberOf.ts new file mode 100644 index 0000000..120047f --- /dev/null +++ b/packages/podspec/src/processors/validate/checks/checkIsNotMemberOf.ts @@ -0,0 +1,92 @@ +import { + IssueCode, + type ValidationBaseIssue, + type ValidationInvalidStatementIssue, + type ValidationStatementNegativeResultIssue, +} from "../issues.js"; +import type { IsNotMemberOf } from "../../../builders/types/statements.js"; +import { tupleToPODValueTypeValues, valueIsEqual } from "../utils.js"; +import type { EntrySource } from "../EntrySource.js"; + +function validateIsNotMemberOfStatement( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + statement: IsNotMemberOf, + statementName: string, + path: string[], + entrySource: EntrySource +): ValidationInvalidStatementIssue[] { + if (statement.entries.some((entry) => !entrySource.getEntry(entry))) { + return [ + { + code: IssueCode.invalid_statement, + statementName: statementName, + statementType: statement.type, + entries: statement.entries, + path: [...path, statementName], + }, + ]; + } + return []; +} + +export function checkIsNotMemberOf( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + statement: IsNotMemberOf, + statementName: string, + path: string[], + entrySource: EntrySource, + exitOnError: boolean +): ValidationBaseIssue[] { + // TODO Move this to a pre-processing step + const issues: ValidationBaseIssue[] = validateIsNotMemberOfStatement( + statement, + statementName, + path, + entrySource + ); + + // Can't proceed if there are any issues with the statement + if (issues.length > 0) { + return issues; + } + + const tuple = statement.entries.map( + (entry) => entrySource.getEntry(entry)?.value + ); + // TODO Move this to a pre-processing step + const tuplesToMatch = tupleToPODValueTypeValues( + statement.isNotMemberOf, + statement.entries.map( + (entry) => entrySource.getEntryTypeFromSpec(entry) as string + ) + ); + + let match = false; + for (const listMember of tuplesToMatch) { + for (let index = 0; index < tuple.length; index++) { + if (valueIsEqual(tuple[index]!, listMember[index]!)) { + match = true; + break; + } + } + if (match) { + break; + } + } + // If we found a match, then the result is negative + if (match) { + const issue = { + code: IssueCode.statement_negative_result, + statementName: statementName, + statementType: statement.type, + entries: statement.entries, + path: [...path, statementName], + } satisfies ValidationStatementNegativeResultIssue; + if (exitOnError) { + return [issue]; + } else { + issues.push(issue); + } + } + return issues; +} diff --git a/packages/podspec/src/processors/validate/checks/checkLessThan.ts b/packages/podspec/src/processors/validate/checks/checkLessThan.ts new file mode 100644 index 0000000..898c816 --- /dev/null +++ b/packages/podspec/src/processors/validate/checks/checkLessThan.ts @@ -0,0 +1,102 @@ +import { isPODArithmeticValue } from "@pcd/pod"; +import type { LessThan } from "../../../builders/types/statements.js"; +import type { EntrySource } from "../EntrySource.js"; +import { IssueCode } from "../issues.js"; + +export function checkLessThan( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + statement: LessThan, + statementName: string, + path: string[], + entrySource: EntrySource, + _exitOnError: boolean +) { + const [leftEntry, rightEntry] = statement.entries; + const entry1 = entrySource.getEntry(leftEntry); + const entry2 = entrySource.getEntry(rightEntry); + + const issues = []; + + // TODO pre-process? might need more detailed issue type for invalid statements + if (entry1 === undefined) { + issues.push({ + code: IssueCode.invalid_statement, + statementName: statementName, + statementType: statement.type, + entries: statement.entries, + path: [...path, statementName], + }); + return issues; + } + if (entry2 === undefined) { + issues.push({ + code: IssueCode.invalid_statement, + statementName: statementName, + statementType: statement.type, + entries: statement.entries, + path: [...path, statementName], + }); + return issues; + } + + const entry1Type = entrySource.getEntryTypeFromSpec(leftEntry); + const entry2Type = entrySource.getEntryTypeFromSpec(rightEntry); + + // TODO this may be too restrictive + if (entry1Type !== entry2Type) { + issues.push({ + code: IssueCode.invalid_statement, + statementName: statementName, + statementType: statement.type, + entries: statement.entries, + path: [...path, statementName], + }); + return issues; + } + + if (entry1Type !== entry1.type) { + issues.push({ + code: IssueCode.invalid_statement, + statementName: statementName, + statementType: statement.type, + entries: statement.entries, + path: [...path, statementName], + }); + return issues; + } + + if (entry1Type !== entry2.type) { + issues.push({ + code: IssueCode.invalid_statement, + statementName: statementName, + statementType: statement.type, + entries: statement.entries, + path: [...path, statementName], + }); + return issues; + } + + if (!isPODArithmeticValue(entry1) || !isPODArithmeticValue(entry2)) { + issues.push({ + code: IssueCode.invalid_statement, + statementName: statementName, + statementType: statement.type, + entries: statement.entries, + path: [...path, statementName], + }); + return issues; + } + + const isLessThan = entry1.value < entry2.value; + + if (!isLessThan) { + issues.push({ + code: IssueCode.statement_negative_result, + statementName: statementName, + statementType: statement.type, + entries: statement.entries, + path: [...path, statementName], + }); + } + return issues; +} diff --git a/packages/podspec/src/processors/validate/checks/checkLessThanEq.ts b/packages/podspec/src/processors/validate/checks/checkLessThanEq.ts new file mode 100644 index 0000000..12bcc84 --- /dev/null +++ b/packages/podspec/src/processors/validate/checks/checkLessThanEq.ts @@ -0,0 +1,102 @@ +import { isPODArithmeticValue } from "@pcd/pod"; +import type { LessThanEq } from "../../../builders/types/statements.js"; +import type { EntrySource } from "../EntrySource.js"; +import { IssueCode } from "../issues.js"; + +export function checkLessThanEq( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + statement: LessThanEq, + statementName: string, + path: string[], + entrySource: EntrySource, + _exitOnError: boolean +) { + const [leftEntry, rightEntry] = statement.entries; + const entry1 = entrySource.getEntry(leftEntry); + const entry2 = entrySource.getEntry(rightEntry); + + const issues = []; + + // TODO pre-process? might need more detailed issue type for invalid statements + if (entry1 === undefined) { + issues.push({ + code: IssueCode.invalid_statement, + statementName: statementName, + statementType: statement.type, + entries: statement.entries, + path: [...path, statementName], + }); + return issues; + } + if (entry2 === undefined) { + issues.push({ + code: IssueCode.invalid_statement, + statementName: statementName, + statementType: statement.type, + entries: statement.entries, + path: [...path, statementName], + }); + return issues; + } + + const entry1Type = entrySource.getEntryTypeFromSpec(leftEntry); + const entry2Type = entrySource.getEntryTypeFromSpec(rightEntry); + + // TODO this may be too restrictive + if (entry1Type !== entry2Type) { + issues.push({ + code: IssueCode.invalid_statement, + statementName: statementName, + statementType: statement.type, + entries: statement.entries, + path: [...path, statementName], + }); + return issues; + } + + if (entry1Type !== entry1.type) { + issues.push({ + code: IssueCode.invalid_statement, + statementName: statementName, + statementType: statement.type, + entries: statement.entries, + path: [...path, statementName], + }); + return issues; + } + + if (entry1Type !== entry2.type) { + issues.push({ + code: IssueCode.invalid_statement, + statementName: statementName, + statementType: statement.type, + entries: statement.entries, + path: [...path, statementName], + }); + return issues; + } + + if (!isPODArithmeticValue(entry1) || !isPODArithmeticValue(entry2)) { + issues.push({ + code: IssueCode.invalid_statement, + statementName: statementName, + statementType: statement.type, + entries: statement.entries, + path: [...path, statementName], + }); + return issues; + } + + const isLessThanOrEqual = entry1.value <= entry2.value; + + if (!isLessThanOrEqual) { + issues.push({ + code: IssueCode.statement_negative_result, + statementName: statementName, + statementType: statement.type, + entries: statement.entries, + path: [...path, statementName], + }); + } + return issues; +} diff --git a/packages/podspec/src/processors/validate/checks/checkNotEqualsEntry.ts b/packages/podspec/src/processors/validate/checks/checkNotEqualsEntry.ts new file mode 100644 index 0000000..d1881f9 --- /dev/null +++ b/packages/podspec/src/processors/validate/checks/checkNotEqualsEntry.ts @@ -0,0 +1,96 @@ +import type { NotEqualsEntry } from "../../../builders/types/statements.js"; +import type { EntrySource } from "../EntrySource.js"; +import { + IssueCode, + type ValidationBaseIssue, + type ValidationStatementNegativeResultIssue, +} from "../issues.js"; +import { valueIsEqual } from "../utils.js"; + +export function checkNotEqualsEntry( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + statement: NotEqualsEntry, + statementName: string, + path: string[], + entrySource: EntrySource, + _exitOnError: boolean +): ValidationBaseIssue[] { + const [leftEntry, rightEntry] = statement.entries; + const entry1 = entrySource.getEntry(leftEntry); + const entry2 = entrySource.getEntry(rightEntry); + + const issues = []; + + // TODO pre-process? might need more detailed issue type for invalid statements + if (entry1 === undefined) { + issues.push({ + code: IssueCode.invalid_statement, + statementName: statementName, + statementType: statement.type, + entries: statement.entries, + path: [...path, statementName], + }); + return issues; + } + if (entry2 === undefined) { + issues.push({ + code: IssueCode.invalid_statement, + statementName: statementName, + statementType: statement.type, + entries: statement.entries, + path: [...path, statementName], + }); + return issues; + } + + const entry1Type = entrySource.getEntryTypeFromSpec(leftEntry); + const entry2Type = entrySource.getEntryTypeFromSpec(rightEntry); + + if (entry1Type !== entry2Type) { + issues.push({ + code: IssueCode.invalid_statement, + statementName: statementName, + statementType: statement.type, + entries: statement.entries, + path: [...path, statementName], + }); + return issues; + } + + if (entry1Type !== entry1.type) { + issues.push({ + code: IssueCode.invalid_statement, + statementName: statementName, + statementType: statement.type, + entries: statement.entries, + path: [...path, statementName], + }); + return issues; + } + + if (entry1Type !== entry2.type) { + issues.push({ + code: IssueCode.invalid_statement, + statementName: statementName, + statementType: statement.type, + entries: statement.entries, + path: [...path, statementName], + }); + return issues; + } + + const isNotEqual = !valueIsEqual(entry1.value, entry2.value); + + if (!isNotEqual) { + const issue = { + code: IssueCode.statement_negative_result, + statementName: statementName, + statementType: statement.type, + entries: statement.entries, + path: [...path, statementName], + } satisfies ValidationStatementNegativeResultIssue; + return [issue]; + } + + return []; +} diff --git a/packages/podspec/src/processors/validate/checks/checkNotInRange.ts b/packages/podspec/src/processors/validate/checks/checkNotInRange.ts new file mode 100644 index 0000000..241d2cf --- /dev/null +++ b/packages/podspec/src/processors/validate/checks/checkNotInRange.ts @@ -0,0 +1,61 @@ +import { isPODArithmeticValue } from "@pcd/pod"; +import type { NotInRange } from "../../../builders/types/statements.js"; +import type { EntrySource } from "../EntrySource.js"; +import { + IssueCode, + type ValidationBaseIssue, + type ValidationInvalidStatementIssue, + type ValidationStatementNegativeResultIssue, +} from "../issues.js"; + +export function checkNotInRange( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + statement: NotInRange, + statementName: string, + path: string[], + entrySource: EntrySource, + _exitOnError: boolean +): ValidationBaseIssue[] { + const [entryName] = statement.entries; + const entry = entrySource.getEntry(entryName); + + // TODO need an issue type for statement referring to a non-existent entry + // or entry of the wrong type + if (entry === undefined) { + const issues = [ + { + code: IssueCode.invalid_statement, + statementName: statementName, + statementType: statement.type, + entries: [entryName], + path: [...path, statementName], + } satisfies ValidationInvalidStatementIssue, + ]; + return issues; + } + + const isDate = entry.type === "date"; + const min = isDate + ? new Date(statement.notInRange.min) + : BigInt(statement.notInRange.min); + const max = isDate + ? new Date(statement.notInRange.max) + : BigInt(statement.notInRange.max); + + if (isPODArithmeticValue(entry)) { + const value = entry.value; + if (value >= min && value <= max) { + return [ + { + code: IssueCode.statement_negative_result, + statementName: statementName, + statementType: statement.type, + entries: [entryName], + path: [...path, statementName], + } satisfies ValidationStatementNegativeResultIssue as ValidationBaseIssue, + ]; + } + } + + return []; +} diff --git a/packages/podspec/src/processors/validate/groupValidator.ts b/packages/podspec/src/processors/validate/groupValidator.ts new file mode 100644 index 0000000..c59a9a2 --- /dev/null +++ b/packages/podspec/src/processors/validate/groupValidator.ts @@ -0,0 +1,234 @@ +import type { POD, PODName } from "@pcd/pod"; +import type { NamedPODSpecs } from "../../builders/group.js"; +import type { PODGroupSpec } from "../../builders/group.js"; +import type { StatementMap } from "../../builders/types/statements.js"; +import { assertPODGroupSpec } from "../../generated/podspec.js"; +import type { NamedStrongPODs } from "../../spec/types.js"; +import { EntrySourcePodGroupSpec } from "./EntrySource.js"; +import { checkEqualsEntry } from "./checks/checkEqualsEntry.js"; +import { checkGreaterThan } from "./checks/checkGreaterThan.js"; +import { checkGreaterThanEq } from "./checks/checkGreaterThanEq.js"; +import { checkInRange } from "./checks/checkInRange.js"; +import { checkIsMemberOf } from "./checks/checkIsMemberOf.js"; +import { checkIsNotMemberOf } from "./checks/checkIsNotMemberOf.js"; +import { checkLessThan } from "./checks/checkLessThan.js"; +import { checkLessThanEq } from "./checks/checkLessThanEq.js"; +import { checkNotEqualsEntry } from "./checks/checkNotEqualsEntry.js"; +import { checkNotInRange } from "./checks/checkNotInRange.js"; +import { type ValidateOptions, podValidator } from "./podValidator.js"; +import { FAILURE } from "./result.js"; +import type { ValidateResult } from "./types.js"; + +interface PODGroupValidator

{ + validate(pods: Record): ValidateResult>; + check(pods: Record): boolean; + assert(pods: Record): asserts pods is NamedStrongPODs

; + strictValidate( + pods: Record + ): ValidateResult>; + strictCheck(pods: Record): boolean; + strictAssert(pods: Record): asserts pods is NamedStrongPODs

; +} + +const SpecValidatorState = new WeakMap< + PODGroupSpec, + boolean +>(); + +export function groupValidator

( + spec: PODGroupSpec +): PODGroupValidator

{ + const validSpec = SpecValidatorState.get(spec); + if (validSpec === undefined) { + // If we haven't seen this spec before, we need to validate it + try { + assertPODGroupSpec(spec); + // TODO check statement configuration + // If we successfully validated the spec, we can cache the result + SpecValidatorState.set(spec, true); + } catch (e) { + SpecValidatorState.set(spec, false); + throw e; + } + } + + return { + validate: (pods, exitOnError = false) => + validate(spec, pods, { exitOnError }), + check: (pods) => validate(spec, pods, { exitOnError: true }).isValid, + assert: (pods) => { + const result = validate(spec, pods, { exitOnError: true }); + if (!result.isValid) throw new Error("POD group is not valid"); + }, + strictValidate: (pods, exitOnError = false) => + validate(spec, pods, { strict: true, exitOnError }), + strictCheck: (pods) => + validate(spec, pods, { strict: true, exitOnError: true }).isValid, + strictAssert: (pods) => { + const result = validate(spec, pods, { strict: true, exitOnError: true }); + if (!result.isValid) throw new Error("POD group is not valid"); + }, + }; +} + +function validate

( + spec: PODGroupSpec, + pods: Record, + options: ValidateOptions +): ValidateResult> { + const issues = []; + const path: string[] = []; + + const entrySource = new EntrySourcePodGroupSpec(spec, pods); + + // TODO audit the group + + const podValidators = Object.fromEntries( + Object.entries(spec.pods).map(([name, podSpec]) => [ + name, + podValidator(podSpec), + ]) + ); + + for (const [name, validator] of Object.entries(podValidators)) { + if (pods[name] === undefined) { + throw new Error(`POD "${name}" is not defined`); + } + + const result = options.strict + ? validator.strictValidate(pods[name], options.exitOnError) + : validator.validate(pods[name], options.exitOnError); + + if (!result.isValid) { + throw new Error(`POD "${name}" is not valid`); + } + } + + for (const [key, statement] of Object.entries(spec.statements)) { + switch (statement.type) { + case "isMemberOf": + issues.push( + ...checkIsMemberOf( + statement, + key, + path, + entrySource, + options.exitOnError ?? false + ) + ); + break; + case "isNotMemberOf": + issues.push( + ...checkIsNotMemberOf( + statement, + key, + path, + entrySource, + options.exitOnError ?? false + ) + ); + break; + case "inRange": + issues.push( + ...checkInRange( + statement, + key, + path, + entrySource, + options.exitOnError ?? false + ) + ); + break; + case "notInRange": + issues.push( + ...checkNotInRange( + statement, + key, + path, + entrySource, + options.exitOnError ?? false + ) + ); + break; + case "equalsEntry": + issues.push( + ...checkEqualsEntry( + statement, + key, + path, + entrySource, + options.exitOnError ?? false + ) + ); + break; + case "notEqualsEntry": + issues.push( + ...checkNotEqualsEntry( + statement, + key, + path, + entrySource, + options.exitOnError ?? false + ) + ); + break; + case "greaterThan": + issues.push( + ...checkGreaterThan( + statement, + key, + path, + entrySource, + options.exitOnError ?? false + ) + ); + break; + case "greaterThanEq": + issues.push( + ...checkGreaterThanEq( + statement, + key, + path, + entrySource, + options.exitOnError ?? false + ) + ); + break; + case "lessThan": + issues.push( + ...checkLessThan( + statement, + key, + path, + entrySource, + options.exitOnError ?? false + ) + ); + break; + case "lessThanEq": + issues.push( + ...checkLessThanEq( + statement, + key, + path, + entrySource, + options.exitOnError ?? false + ) + ); + break; + default: + // prettier-ignore + statement satisfies never; + } + if (options.exitOnError && issues.length > 0) { + return FAILURE(issues); + } + } + + return issues.length > 0 + ? FAILURE(issues) + : { + isValid: true, + value: pods as NamedStrongPODs

, + }; +} diff --git a/packages/podspec/src/processors/validate/issues.ts b/packages/podspec/src/processors/validate/issues.ts new file mode 100644 index 0000000..014e84e --- /dev/null +++ b/packages/podspec/src/processors/validate/issues.ts @@ -0,0 +1,140 @@ +import type { PODValue } from "@pcd/pod"; +import type { Statements } from "../../builders/types/statements.js"; + +export const IssueCode = { + type_mismatch: "type_mismatch", + missing_entry: "missing_entry", + missing_pod: "missing_pod", + invalid_entry_name: "invalid_entry_name", + invalid_pod_value: "invalid_pod_value", + invalid_statement: "invalid_statement", + unexpected_input_entry: "unexpected_input_entry", + unexpected_input_pod: "unexpected_input_pod", + statement_negative_result: "statement_negative_result", +} as const; + +/** + @todo + - [ ] include statement name where relevant + - [ ] include position in list where relevant + */ + +/** + * Base interface for all issues that can occur when validating a POD + * against a Podspec. + */ +export interface ValidationBaseIssue { + message?: string; + path: (string | number)[]; + code: (typeof IssueCode)[keyof typeof IssueCode]; +} + +/** + * Issue that occurs when an input value is of an invalid type. + */ +export interface ValidationTypeMismatchIssue extends ValidationBaseIssue { + code: typeof IssueCode.type_mismatch; + expectedType: PODValue["type"] | "PODEntries"; +} + +/** + * Issue that occurs when an input value is missing from the PODEntries. + */ +export interface ValidationMissingEntryIssue extends ValidationBaseIssue { + code: typeof IssueCode.missing_entry; + key: string; +} + +/** + * Issue that occurs when a pod is missing from a pod group. + */ +export interface ValidationMissingPodIssue extends ValidationBaseIssue { + code: typeof IssueCode.missing_pod; + podName: string; +} + +/** + * Issue that occurs when an input value has an invalid entry name. + */ +export interface ValidationInvalidEntryNameIssue extends ValidationBaseIssue { + code: typeof IssueCode.invalid_entry_name; + name: string; + description: string; +} + +/** + * Issue that occurs when a statement is invalid. + */ +export interface ValidationInvalidStatementIssue extends ValidationBaseIssue { + code: typeof IssueCode.invalid_statement; + statementName: string; + statementType: Statements["type"]; + entries: string[]; +} + +/** + * Issue that occurs when a POD value is invalid. + */ +export interface ValidationInvalidPodValueIssue extends ValidationBaseIssue { + code: typeof IssueCode.invalid_pod_value; + value: PODValue; + reason: string; +} + +/** + * Issue that occurs when an unexpected entry is encountered. + * Only relevant for "strict" parsing modes. + */ +export interface ValidationUnexpectedInputEntryIssue + extends ValidationBaseIssue { + code: typeof IssueCode.unexpected_input_entry; + key: string; +} + +/** + * Issue that occurs when an unexpected pod is encountered. + * Only relevant for "strict" parsing modes. + */ +export interface ValidationUnexpectedInputPodIssue extends ValidationBaseIssue { + code: typeof IssueCode.unexpected_input_pod; + podName: string; +} + +/** + * Issue that occurs when a statement fails. + */ +export interface ValidationStatementNegativeResultIssue + extends ValidationBaseIssue { + code: typeof IssueCode.statement_negative_result; + statementName: string; + statementType: Statements["type"]; + entries: string[]; +} + +export type ValidationIssue = + | ValidationTypeMismatchIssue + | ValidationMissingEntryIssue + | ValidationMissingPodIssue + | ValidationInvalidEntryNameIssue + | ValidationInvalidStatementIssue + | ValidationInvalidPodValueIssue + | ValidationUnexpectedInputEntryIssue + | ValidationUnexpectedInputPodIssue + | ValidationStatementNegativeResultIssue; + +/** + * Exception class for errors that occur when parsing. + */ +export class ValidationError extends Error { + issues: ValidationBaseIssue[] = []; + + public errors(): ValidationBaseIssue[] { + return this.issues; + } + + constructor(issues: ValidationBaseIssue[]) { + super(); + this.name = "ValidationError"; + this.issues = issues; + } +} diff --git a/packages/podspec/src/processors/validate/podValidator.ts b/packages/podspec/src/processors/validate/podValidator.ts new file mode 100644 index 0000000..8265045 --- /dev/null +++ b/packages/podspec/src/processors/validate/podValidator.ts @@ -0,0 +1,251 @@ +import type { POD } from "@pcd/pod"; +import type { PODSpec } from "../../builders/pod.js"; +import type { EntryTypes } from "../../builders/types/entries.js"; +import type { StatementMap } from "../../builders/types/statements.js"; +import { assertPODSpec } from "../../generated/podspec.js"; +import type { PODEntriesFromEntryTypes, StrongPOD } from "../../spec/types.js"; +import { EntrySourcePodSpec } from "./EntrySource.js"; +import { checkEqualsEntry } from "./checks/checkEqualsEntry.js"; +import { checkGreaterThan } from "./checks/checkGreaterThan.js"; +import { checkGreaterThanEq } from "./checks/checkGreaterThanEq.js"; +import { checkInRange } from "./checks/checkInRange.js"; +import { checkIsMemberOf } from "./checks/checkIsMemberOf.js"; +import { checkIsNotMemberOf } from "./checks/checkIsNotMemberOf.js"; +import { checkLessThan } from "./checks/checkLessThan.js"; +import { checkLessThanEq } from "./checks/checkLessThanEq.js"; +import { checkNotEqualsEntry } from "./checks/checkNotEqualsEntry.js"; +import { checkNotInRange } from "./checks/checkNotInRange.js"; +import { FAILURE, SUCCESS } from "./result.js"; +import type { ValidateResult } from "./types.js"; + +/** + @TOOO + - [ ] "Compile" a spec by hashing the statement parameters where necessary? +*/ + +export interface ValidateOptions { + /** + * If true, the validation will exit as soon as the first error is encountered. + */ + exitOnError?: boolean; + /** + * If true, the validation will reject entries in the input which are not in the spec. + */ + strict?: boolean; +} + +const DEFAULT_VALIDATE_OPTIONS: ValidateOptions = { + exitOnError: false, + strict: false, +}; + +interface PODValidator { + validate( + pod: POD, + exitOnError?: boolean + ): ValidateResult>>; + check(pod: POD): pod is StrongPOD>; + assert(pod: POD): asserts pod is StrongPOD>; + strictValidate( + pod: POD, + exitOnError?: boolean + ): ValidateResult>>; + strictCheck(pod: POD): pod is StrongPOD>; + strictAssert(pod: POD): asserts pod is StrongPOD>; +} + +const SpecValidatorState = new WeakMap< + PODSpec, + boolean +>(); + +export function podValidator( + spec: PODSpec +): PODValidator { + const validSpec = SpecValidatorState.get(spec); + if (validSpec === undefined) { + // If we haven't seen this spec before, we need to validate it + try { + assertPODSpec(spec); + // TODO check statement configuration + // If we successfully validated the spec, we can cache the result + SpecValidatorState.set(spec, true); + } catch (e) { + SpecValidatorState.set(spec, false); + throw e; + } + } + + return { + validate: (pod, exitOnError = false) => + validatePOD(pod, spec, { exitOnError }), + check: (pod): pod is StrongPOD> => + validatePOD(pod, spec, { exitOnError: true }).isValid, + assert: (pod) => { + const result = validatePOD(pod, spec, { exitOnError: true }); + if (!result.isValid) throw new Error("POD is not valid"); + }, + strictValidate: (pod, exitOnError = false) => + validatePOD(pod, spec, { strict: true, exitOnError }), + strictCheck: (pod): pod is StrongPOD> => + validatePOD(pod, spec, { strict: true, exitOnError: true }).isValid, + strictAssert: (pod) => { + const result = validatePOD(pod, spec, { + strict: true, + exitOnError: true, + }); + if (!result.isValid) throw new Error("POD is not valid"); + }, + }; +} + +/** + * Validate a POD against a PODSpec. + * + * @param pod - The POD to validate. + * @param spec - The PODSpec to validate against. + * @param options - The options to use for validation. + * @returns true if the POD is valid, false otherwise. + */ +function validatePOD( + pod: POD, + spec: PODSpec, + options: ValidateOptions = DEFAULT_VALIDATE_OPTIONS +): ValidateResult>> { + const issues = []; + const path: string[] = []; + + const entrySource = new EntrySourcePodSpec(spec, pod); + + issues.push(...entrySource.audit(path, options)); + if (issues.length > 0) { + // If we have missing, malformed, or unexpected entries, we should return + // before trying to validate the statements. + return FAILURE(issues); + } + + for (const [key, statement] of Object.entries(spec.statements)) { + switch (statement.type) { + case "isMemberOf": + issues.push( + ...checkIsMemberOf( + statement, + key, + path, + entrySource, + options.exitOnError ?? false + ) + ); + break; + case "isNotMemberOf": + issues.push( + ...checkIsNotMemberOf( + statement, + key, + path, + entrySource, + options.exitOnError ?? false + ) + ); + break; + case "inRange": + issues.push( + ...checkInRange( + statement, + key, + path, + entrySource, + options.exitOnError ?? false + ) + ); + break; + case "notInRange": + issues.push( + ...checkNotInRange( + statement, + key, + path, + entrySource, + options.exitOnError ?? false + ) + ); + break; + case "equalsEntry": + issues.push( + ...checkEqualsEntry( + statement, + key, + path, + entrySource, + options.exitOnError ?? false + ) + ); + break; + case "notEqualsEntry": + issues.push( + ...checkNotEqualsEntry( + statement, + key, + path, + entrySource, + options.exitOnError ?? false + ) + ); + break; + case "greaterThan": + issues.push( + ...checkGreaterThan( + statement, + key, + path, + entrySource, + options.exitOnError ?? false + ) + ); + break; + case "greaterThanEq": + issues.push( + ...checkGreaterThanEq( + statement, + key, + path, + entrySource, + options.exitOnError ?? false + ) + ); + break; + case "lessThan": + issues.push( + ...checkLessThan( + statement, + key, + path, + entrySource, + options.exitOnError ?? false + ) + ); + break; + case "lessThanEq": + issues.push( + ...checkLessThanEq( + statement, + key, + path, + entrySource, + options.exitOnError ?? false + ) + ); + break; + default: + // prettier-ignore + statement satisfies never; + } + if (options.exitOnError && issues.length > 0) { + return FAILURE(issues); + } + } + + return issues.length > 0 + ? FAILURE(issues) + : SUCCESS(pod as StrongPOD>); +} diff --git a/packages/podspec/src/processors/validate/result.ts b/packages/podspec/src/processors/validate/result.ts new file mode 100644 index 0000000..f601637 --- /dev/null +++ b/packages/podspec/src/processors/validate/result.ts @@ -0,0 +1,25 @@ +import type { ValidationBaseIssue } from "./issues.js"; +import type { ValidateFailure, ValidateSuccess } from "./types.js"; + +/** + * Creates a ValidateFailure containing a list of issues. + * + * @param errors The issues to include in the failure. + * @returns A ValidateFailure containing the issues. + */ +export function FAILURE(errors: ValidationBaseIssue[]): ValidateFailure { + return { isValid: false, issues: errors ?? [] }; +} + +/** + * Creates a ValidateSuccess containing a valid value. + * + * @param value The value to include in the success. + * @returns A ValidateSuccess containing the value. + */ +export function SUCCESS(value: T): ValidateSuccess { + return { + isValid: true, + value, + }; +} diff --git a/packages/podspec/src/processors/validate/types.ts b/packages/podspec/src/processors/validate/types.ts new file mode 100644 index 0000000..f269577 --- /dev/null +++ b/packages/podspec/src/processors/validate/types.ts @@ -0,0 +1,13 @@ +import type { ValidationIssue } from "./issues.js"; + +export type ValidateSuccess = { + value: T; + isValid: true; +}; + +export type ValidateFailure = { + issues: ValidationIssue[]; + isValid: false; +}; + +export type ValidateResult = ValidateSuccess | ValidateFailure; diff --git a/packages/podspec/src/processors/validate/utils.ts b/packages/podspec/src/processors/validate/utils.ts new file mode 100644 index 0000000..c96dd1c --- /dev/null +++ b/packages/podspec/src/processors/validate/utils.ts @@ -0,0 +1,62 @@ +import type { PODValue } from "@pcd/pod"; +import { toByteArray } from "base64-js"; + +export function tupleToPODValueTypeValues( + tuples: string[][], + types: string[] +): PODValue["value"][][] { + return tuples.map((tuple) => { + return tuple.map((value, index) => { + const type = types[index] as PODValue["type"] | undefined; + if (type === undefined) { + throw new Error(`Type for index ${index} is undefined`); + } + switch (type) { + case "string": + case "eddsa_pubkey": + return value; + case "boolean": + return value === "true" ? true : false; + case "bytes": + return toByteArray(value); + case "cryptographic": + case "int": + return BigInt(value); + case "date": + return new Date(value); + case "null": + return null; + default: + const _exhaustiveCheck: never = type; + return _exhaustiveCheck; + } + }); + }); +} + +export function valueIsEqual( + a: PODValue["value"], + b: PODValue["value"] +): boolean { + if (a instanceof Uint8Array) { + return ( + b instanceof Uint8Array && + a.length === b.length && + a.every((value, index) => value === b[index]) + ); + } + if (a instanceof Date) { + return b instanceof Date && a.getTime() === b.getTime(); + } + if (a === null) { + return b === null; + } + if (typeof a === "bigint") { + return typeof b === "bigint" && a === b; + } + + // We can just do a simple equality check now + a satisfies string | boolean; + + return a === b; +} diff --git a/packages/podspec/src/schemas/boolean.ts b/packages/podspec/src/schemas/boolean.ts deleted file mode 100644 index 6b2edd7..0000000 --- a/packages/podspec/src/schemas/boolean.ts +++ /dev/null @@ -1,54 +0,0 @@ -import type { PODBooleanValue, PODValue } from "@pcd/pod"; -import { checkPODValue } from "@pcd/pod/podChecks"; -import type { PodspecInvalidTypeIssue } from "../error.js"; -import { IssueCode } from "../error.js"; -import type { ParseResult } from "../parse/parse_utils.js"; -import { FAILURE, SUCCESS } from "../parse/parse_utils.js"; - -/** - * Schema for a PODBooleanValue. - */ -export interface BooleanSchema { - type: "boolean"; - isMemberOf?: PODBooleanValue[]; - isNotMemberOf?: PODBooleanValue[]; -} - -/** - * Checks if the given input is a PODBytesValue. - * @param data - The input to check. - * @returns A ParseResult wrapping the value - */ -export function checkPODBooleanValue( - data: unknown, - path: string[] -): ParseResult { - try { - checkPODValue("", data as PODValue); - } catch { - const issue = { - code: IssueCode.invalid_type, - expectedType: "boolean", - path: path - } satisfies PodspecInvalidTypeIssue; - return FAILURE([issue]); - } - - return SUCCESS(data as PODBooleanValue); -} - -/** - * @param input - The input to coerce. - * @returns A PODBytesValue or undefined if coercion is not possible. - */ -export function booleanCoercer(input: unknown): PODBooleanValue | undefined { - let value: PODBooleanValue | undefined = undefined; - if (typeof input === "boolean") { - value = { - type: "boolean", - value: input - }; - } - - return value; -} diff --git a/packages/podspec/src/schemas/bytes.ts b/packages/podspec/src/schemas/bytes.ts deleted file mode 100644 index f85ee8a..0000000 --- a/packages/podspec/src/schemas/bytes.ts +++ /dev/null @@ -1,55 +0,0 @@ -import type { PODBytesValue, PODName, PODValue } from "@pcd/pod"; -import { checkPODValue } from "@pcd/pod/podChecks"; -import type { PodspecInvalidTypeIssue } from "../error.js"; -import { IssueCode } from "../error.js"; -import type { ParseResult } from "../parse/parse_utils.js"; -import { FAILURE, SUCCESS } from "../parse/parse_utils.js"; - -/** - * Schema for a PODBytesValue. - */ -export interface BytesSchema { - type: "bytes"; - isMemberOf?: PODBytesValue[]; - isNotMemberOf?: PODBytesValue[]; - equalsEntry?: PODName; -} - -/** - * Checks if the given input is a PODBytesValue. - * @param data - The input to check. - * @returns A ParseResult wrapping the value - */ -export function checkPODBytesValue( - data: unknown, - path: string[] -): ParseResult { - try { - checkPODValue("", data as PODValue); - } catch { - const issue = { - code: IssueCode.invalid_type, - expectedType: "bytes", - path: path - } satisfies PodspecInvalidTypeIssue; - return FAILURE([issue]); - } - - return SUCCESS(data as PODBytesValue); -} - -/** - * @param input - The input to coerce. - * @returns A PODBytesValue or undefined if coercion is not possible. - */ -export function bytesCoercer(input: unknown): PODBytesValue | undefined { - let value: PODBytesValue | undefined = undefined; - if (input instanceof Uint8Array) { - value = { - type: "bytes", - value: input - }; - } - - return value; -} diff --git a/packages/podspec/src/schemas/cryptographic.ts b/packages/podspec/src/schemas/cryptographic.ts deleted file mode 100644 index 377ce6e..0000000 --- a/packages/podspec/src/schemas/cryptographic.ts +++ /dev/null @@ -1,81 +0,0 @@ -import type { PODCryptographicValue, PODName } from "@pcd/pod"; -import { - POD_CRYPTOGRAPHIC_MAX, - POD_CRYPTOGRAPHIC_MIN -} from "@pcd/pod/podTypes"; -import type { PodspecInvalidTypeIssue } from "../error.js"; -import { IssueCode } from "../error.js"; -import type { ParseResult } from "../parse/parse_utils.js"; -import { FAILURE, safeCheckBigintBounds } from "../parse/parse_utils.js"; - -/** - * Schema for a cryptographic value. - */ -export interface CryptographicSchema { - type: "cryptographic"; - isMemberOf?: PODCryptographicValue[]; - isNotMemberOf?: PODCryptographicValue[]; - equalsEntry?: PODName; - inRange?: { min: bigint; max: bigint }; - // isOwnerID is supported for cryptographic values, e.g. a Semaphore commitment - isOwnerID?: boolean; -} - -/** - * Checks if the given input is a PODCryptographicValue. - * - * @param data - The input to check. - * @returns A ParseResult wrapping the value - */ -export function checkPODCryptographicValue( - data: unknown, - path: string[] -): ParseResult { - if ( - typeof data !== "object" || - data === null || - !("type" in data && "value" in data) || - data.type !== "cryptographic" || - typeof data.value !== "bigint" - ) { - const issue = { - code: IssueCode.invalid_type, - expectedType: "cryptographic", - path: path - } satisfies PodspecInvalidTypeIssue; - return FAILURE([issue]); - } - - return safeCheckBigintBounds( - path, - data as PODCryptographicValue, - POD_CRYPTOGRAPHIC_MIN, - POD_CRYPTOGRAPHIC_MAX - ); -} - -/** - * Coerces an input to a PODCryptographicValue. - * Supports the conversion of JavaScript numbers and bigints to PODCryptographicValue. - * - * @param input - The input to coerce. - * @returns A PODCryptographicValue or undefined if coercion is not possible. - */ -export function cryptographicCoercer( - input: unknown -): PODCryptographicValue | undefined { - let value: PODCryptographicValue | undefined = undefined; - if (typeof input === "number") { - value = { - type: "cryptographic", - value: BigInt(input) - }; - } else if (typeof input === "bigint") { - value = { - type: "cryptographic", - value: input - }; - } - - return value; -} diff --git a/packages/podspec/src/schemas/dates.ts b/packages/podspec/src/schemas/dates.ts deleted file mode 100644 index 22c354f..0000000 --- a/packages/podspec/src/schemas/dates.ts +++ /dev/null @@ -1,55 +0,0 @@ -import type { PODDateValue, PODValue } from "@pcd/pod"; -import { checkPODValue } from "@pcd/pod/podChecks"; -import type { PodspecInvalidTypeIssue } from "../error.js"; -import { IssueCode } from "../error.js"; -import type { ParseResult } from "../parse/parse_utils.js"; -import { FAILURE, SUCCESS } from "../parse/parse_utils.js"; - -/** - * Schema for a PODDateValue. - */ -export interface DateSchema { - type: "date"; - isMemberOf?: PODDateValue[]; - isNotMemberOf?: PODDateValue[]; - inRange?: { min: bigint; max: bigint }; -} - -/** - * Checks if the given input is a PODBytesValue. - * @param data - The input to check. - * @returns A ParseResult wrapping the value - */ -export function checkPODDateValue( - data: unknown, - path: string[] -): ParseResult { - try { - checkPODValue("", data as PODValue); - } catch { - const issue = { - code: IssueCode.invalid_type, - expectedType: "date", - path: path - } satisfies PodspecInvalidTypeIssue; - return FAILURE([issue]); - } - - return SUCCESS(data as PODDateValue); -} - -/** - * @param input - The input to coerce. - * @returns A PODDateValue or undefined if coercion is not possible. - */ -export function dateCoercer(input: unknown): PODDateValue | undefined { - let value: PODDateValue | undefined = undefined; - if (input instanceof Date) { - value = { - type: "date", - value: input - }; - } - - return value; -} diff --git a/packages/podspec/src/schemas/eddsa_pubkey.ts b/packages/podspec/src/schemas/eddsa_pubkey.ts deleted file mode 100644 index 9085590..0000000 --- a/packages/podspec/src/schemas/eddsa_pubkey.ts +++ /dev/null @@ -1,63 +0,0 @@ -import type { PODEdDSAPublicKeyValue, PODName } from "@pcd/pod"; -import type { PodspecInvalidTypeIssue } from "../error.js"; -import { IssueCode } from "../error.js"; -import type { ParseResult } from "../parse/parse_utils.js"; -import { FAILURE, safeCheckPublicKeyFormat } from "../parse/parse_utils.js"; - -/** - * Schema for an EdDSA public key. - */ -export interface EdDSAPublicKeySchema { - type: "eddsa_pubkey"; - isMemberOf?: PODEdDSAPublicKeyValue[]; - isNotMemberOf?: PODEdDSAPublicKeyValue[]; - equalsEntry?: PODName; -} - -/** - * Checks if the given input is a PODEdDSAPublicKeyValue. - * @param data - The input to check. - * @returns A ParseResult wrapping the value - */ -export function checkPODEdDSAPublicKeyValue( - data: unknown, - path: string[] -): ParseResult { - if ( - typeof data !== "object" || - data === null || - !("type" in data && "value" in data) || - data.type !== "eddsa_pubkey" || - typeof data.value !== "string" - ) { - const issue = { - code: IssueCode.invalid_type, - expectedType: "eddsa_pubkey", - path: path - } satisfies PodspecInvalidTypeIssue; - return FAILURE([issue]); - } - - return safeCheckPublicKeyFormat(path, data as PODEdDSAPublicKeyValue); -} - -/** - * Coerces an input to a PODEdDSAPublicKeyValue. - * Supports the conversion of JavaScript strings to PODEdDSAPublicKeyValue. - * - * @param input - The input to coerce. - * @returns A PODEdDSAPublicKeyValue or undefined if coercion is not possible. - */ -export function eddsaPublicKeyCoercer( - input: unknown -): PODEdDSAPublicKeyValue | undefined { - let value: PODEdDSAPublicKeyValue | undefined = undefined; - if (typeof input === "string") { - value = { - type: "eddsa_pubkey", - value: input - }; - } - - return value; -} diff --git a/packages/podspec/src/schemas/entries.ts b/packages/podspec/src/schemas/entries.ts deleted file mode 100644 index d712d2b..0000000 --- a/packages/podspec/src/schemas/entries.ts +++ /dev/null @@ -1,16 +0,0 @@ -import type { PODValue } from "@pcd/pod"; -import type { EntrySchema } from "./entry.js"; - -/** - * Schema for validating a PODEntries object. - */ -export type EntriesSchema = Readonly>; - -/** - * Schema for a tuple of entries. - */ -export type EntriesTupleSchema = { - entries: (keyof E & string)[]; - isMemberOf?: PODValue[][]; - isNotMemberOf?: PODValue[][]; -}; diff --git a/packages/podspec/src/schemas/entry.ts b/packages/podspec/src/schemas/entry.ts deleted file mode 100644 index a8a98d2..0000000 --- a/packages/podspec/src/schemas/entry.ts +++ /dev/null @@ -1,34 +0,0 @@ -import type { BooleanSchema } from "./boolean.js"; -import type { BytesSchema } from "./bytes.js"; -import type { CryptographicSchema } from "./cryptographic.js"; -import type { DateSchema } from "./dates.js"; -import type { EdDSAPublicKeySchema } from "./eddsa_pubkey.js"; -import type { IntSchema } from "./int.js"; -import type { NullSchema } from "./null.js"; -import type { StringSchema } from "./string.js"; - -/** - * Union of schemas for non-optional entries. - */ -export type DefinedEntrySchema = - | StringSchema - | CryptographicSchema - | IntSchema - | EdDSAPublicKeySchema - | BooleanSchema - | BytesSchema - | DateSchema - | NullSchema; - -/** - * Schema for an optional entry. - */ -export interface OptionalSchema { - type: "optional"; - innerType: DefinedEntrySchema; -} - -/** - * Union of schemas for entries. - */ -export type EntrySchema = DefinedEntrySchema | OptionalSchema; diff --git a/packages/podspec/src/schemas/int.ts b/packages/podspec/src/schemas/int.ts deleted file mode 100644 index 63ed69d..0000000 --- a/packages/podspec/src/schemas/int.ts +++ /dev/null @@ -1,74 +0,0 @@ -import type { PODIntValue, PODName } from "@pcd/pod"; -import { POD_INT_MAX, POD_INT_MIN } from "@pcd/pod/podTypes"; -import type { PodspecInvalidTypeIssue } from "../error.js"; -import { IssueCode } from "../error.js"; -import type { ParseResult } from "../parse/parse_utils.js"; -import { FAILURE, safeCheckBigintBounds } from "../parse/parse_utils.js"; - -/** - * Schema for a PODIntValue. - */ -export interface IntSchema { - type: "int"; - isMemberOf?: PODIntValue[]; - isNotMemberOf?: PODIntValue[]; - equalsEntry?: PODName; - inRange?: { min: bigint; max: bigint }; -} - -/** - * Checks if the given input is a valid POD integer. - * - * @param data - The input to check - * @returns A ParseResult wrapping the value - */ -export function checkPODIntValue( - data: unknown, - path: string[] -): ParseResult { - if ( - typeof data !== "object" || - data === null || - !("type" in data && "value" in data) || - data.type !== "int" || - typeof data.value !== "bigint" - ) { - const issue = { - code: IssueCode.invalid_type, - expectedType: "int", - path: path - } satisfies PodspecInvalidTypeIssue; - return FAILURE([issue]); - } - - return safeCheckBigintBounds( - path, - data as PODIntValue, - POD_INT_MIN, - POD_INT_MAX - ); -} - -/** - * Coerces an input to a PODIntValue. - * Supports the conversion of JavaScript numbers and bigints to PODIntValue. - * - * @param input - The input to coerce. - * @returns A PODIntValue or undefined if coercion is not possible. - */ -export function intCoercer(input: unknown): PODIntValue | undefined { - let value: PODIntValue | undefined = undefined; - if (typeof input === "number") { - value = { - type: "int", - value: BigInt(input) - }; - } else if (typeof input === "bigint") { - value = { - type: "int", - value: input - }; - } - - return value; -} diff --git a/packages/podspec/src/schemas/null.ts b/packages/podspec/src/schemas/null.ts deleted file mode 100644 index ba650ca..0000000 --- a/packages/podspec/src/schemas/null.ts +++ /dev/null @@ -1,52 +0,0 @@ -import type { PODNullValue, PODValue } from "@pcd/pod"; -import { checkPODValue } from "@pcd/pod/podChecks"; -import type { PodspecInvalidTypeIssue } from "../error.js"; -import { IssueCode } from "../error.js"; -import type { ParseResult } from "../parse/parse_utils.js"; -import { FAILURE, SUCCESS } from "../parse/parse_utils.js"; - -/** - * Schema for a PODNullValue. - */ -export interface NullSchema { - type: "null"; -} - -/** - * Checks if the given input is a PODNullValue. - * @param data - The input to check. - * @returns A ParseResult wrapping the value - */ -export function checkPODNullValue( - data: unknown, - path: string[] -): ParseResult { - try { - checkPODValue("", data as PODValue); - } catch { - const issue = { - code: IssueCode.invalid_type, - expectedType: "null", - path: path - } satisfies PodspecInvalidTypeIssue; - return FAILURE([issue]); - } - - return SUCCESS(data as PODNullValue); -} - -/** - * @param input - The input to coerce. - * @returns A PODNullValue or undefined if coercion is not possible. - */ -export function nullCoercer(input: unknown): PODNullValue | undefined { - let value: PODNullValue | undefined = undefined; - if (input === null) { - value = { - type: "null", - value: input - }; - } - - return value; -} diff --git a/packages/podspec/src/schemas/pod.ts b/packages/podspec/src/schemas/pod.ts deleted file mode 100644 index a34ffa9..0000000 --- a/packages/podspec/src/schemas/pod.ts +++ /dev/null @@ -1,33 +0,0 @@ -import type { PODValue } from "@pcd/pod"; -import type { EntriesSchema } from "./entries.js"; - -/** - * Schema for validating a POD. - */ -export type PODSchema = { - entries: E & EntriesSchema; - tuples?: { - entries: (keyof (E & { - $signerPublicKey: never; - }) & - string)[]; - isMemberOf?: PODValue[][]; - isNotMemberOf?: PODValue[][]; - }[]; - signerPublicKey?: { - isRevealed?: boolean; - isMemberOf?: string[]; - isNotMemberOf?: string[]; - }; - signature?: { - isMemberOf?: string[]; - isNotMemberOf?: string[]; - }; - meta?: { - labelEntry: keyof E & string; - }; -}; - -export type PODTupleSchema = Required< - PODSchema ->["tuples"][number]; diff --git a/packages/podspec/src/schemas/string.ts b/packages/podspec/src/schemas/string.ts deleted file mode 100644 index d9364b1..0000000 --- a/packages/podspec/src/schemas/string.ts +++ /dev/null @@ -1,58 +0,0 @@ -import type { PODName, PODStringValue } from "@pcd/pod"; -import type { PodspecInvalidTypeIssue } from "../error.js"; -import { IssueCode } from "../error.js"; -import type { ParseResult } from "../parse/parse_utils.js"; -import { FAILURE, SUCCESS } from "../parse/parse_utils.js"; - -/** - * Schema for a PODStringValue. - */ -export interface StringSchema { - type: "string"; - isMemberOf?: PODStringValue[]; - isNotMemberOf?: PODStringValue[]; - equalsEntry?: PODName; -} - -/** - * Checks if the given input is a PODEdDSAPublicKeyValue. - * @param data - The input to check. - * @returns A ParseResult wrapping the value - */ -export function checkPODStringValue( - data: unknown, - path: string[] -): ParseResult { - if ( - typeof data !== "object" || - data === null || - !("type" in data && "value" in data) || - data.type !== "string" || - typeof data.value !== "string" - ) { - const issue = { - code: IssueCode.invalid_type, - expectedType: "string", - path: path - } satisfies PodspecInvalidTypeIssue; - return FAILURE([issue]); - } - - return SUCCESS(data as PODStringValue); -} - -/** - * @param input - The input to coerce. - * @returns A PODStringValue or undefined if coercion is not possible. - */ -export function stringCoercer(input: unknown): PODStringValue | undefined { - let value: PODStringValue | undefined = undefined; - if (typeof input === "string") { - value = { - type: "string", - value: input - }; - } - - return value; -} diff --git a/packages/podspec/src/shared/jsonSafe.ts b/packages/podspec/src/shared/jsonSafe.ts new file mode 100644 index 0000000..bbe680a --- /dev/null +++ b/packages/podspec/src/shared/jsonSafe.ts @@ -0,0 +1,12 @@ +type JsonPrimitive = string | number | boolean | null; +type JsonArray = JsonSafe[]; +// eslint-disable-next-line @typescript-eslint/consistent-indexed-object-style +type JsonObject = { [key: string]: JsonSafe }; +type JsonSafe = JsonPrimitive | JsonArray | JsonObject; + +/** + * Type helper that checks if a type is JSON-safe. + * Returns true if the type only contains JSON-safe values, + * false if it contains any non-JSON-safe values like undefined, bigint, Date, etc. + */ +export type IsJsonSafe = T extends JsonSafe ? true : false; diff --git a/packages/podspec/src/shared/types.ts b/packages/podspec/src/shared/types.ts new file mode 100644 index 0000000..a59bb40 --- /dev/null +++ b/packages/podspec/src/shared/types.ts @@ -0,0 +1,15 @@ +type IsSingleLiteralType = [P] extends [never] + ? false + : P extends unknown + ? [PCopy] extends [P] + ? true + : false + : never; + +export type IsSingleLiteralString = IsSingleLiteralType extends true + ? T extends string + ? string extends T + ? false + : true + : false + : false; diff --git a/packages/podspec/src/spec/types.ts b/packages/podspec/src/spec/types.ts new file mode 100644 index 0000000..702d8c0 --- /dev/null +++ b/packages/podspec/src/spec/types.ts @@ -0,0 +1,32 @@ +import type { POD, PODContent, PODEntries, PODName, PODValue } from "@pcd/pod"; +import type { NamedPODSpecs } from "../builders/group.js"; +import type { EntryTypes } from "../builders/types/entries.js"; + +export type NamedStrongPODs

= { + [K in keyof P]: StrongPOD>; +}; + +/** + * "Strong" PODContent is an extension of PODContent which extends the + * `asEntries()` method to return a strongly-typed PODEntries. + */ +interface StrongPODContent extends PODContent { + asEntries(): T & PODEntries; + getValue( + name: N + ): N extends keyof T ? T[N] : PODValue; + getRawValue( + name: N + ): N extends keyof T ? T[N]["value"] : PODValue["value"]; +} + +/** + * A "strong" POD is a POD with a strongly-typed entries. + */ +export interface StrongPOD extends POD { + content: StrongPODContent; +} + +export type PODEntriesFromEntryTypes = { + [K in keyof E]: Extract; +}; diff --git a/packages/podspec/src/type_inference.ts b/packages/podspec/src/type_inference.ts deleted file mode 100644 index 4ad1fdc..0000000 --- a/packages/podspec/src/type_inference.ts +++ /dev/null @@ -1,70 +0,0 @@ -import type { EntriesSpec } from "./parse/entries.js"; -import type { - PODValueCoerceableNativeTypes, - PODValueNativeTypes -} from "./parse/parse_utils.js"; -import type { PodSpec, StrongPOD } from "./parse/pod.js"; -import type { EntriesSchema } from "./schemas/entries.js"; -import type { DefinedEntrySchema, OptionalSchema } from "./schemas/entry.js"; - -/** - * Infer a typed version of a POD from a given Podspec. - */ -export type InferPodType = T extends PodSpec - ? StrongPOD> - : never; - -export type InferEntriesType = T extends PodSpec - ? E - : T extends EntriesSpec - ? E - : never; - -type JavaScriptEntriesType = AddQuestionMarks<{ - [k in keyof E]: E[k] extends DefinedEntrySchema - ? PODValueCoerceableNativeTypes[E[k]["type"]] - : E[k] extends OptionalSchema - ? PODValueCoerceableNativeTypes[E[k]["innerType"]["type"]] | undefined - : never; -}>; - -export type InferJavaScriptEntriesType = T extends PodSpec - ? JavaScriptEntriesType - : T extends EntriesSpec - ? JavaScriptEntriesType - : never; - -/** - * Gets the output type for entries matching a given schema. - * The output type is a record mapping string keys to PODValues, and is - * therefore similar to {@link PODEntries}, but is specific about the values - * that certain entries ought to have. - */ -export type EntriesOutputType = AddQuestionMarks<{ - [k in keyof E]: E[k] extends DefinedEntrySchema - ? { - type: E[k]["type"]; - value: PODValueNativeTypes[E[k]["type"]]; - } - : E[k] extends OptionalSchema - ? - | { - type: E[k]["innerType"]["type"]; - value: PODValueNativeTypes[E[k]["innerType"]["type"]]; - } - | undefined - : never; -}>; -type optionalKeys = { - [k in keyof T]: undefined extends T[k] ? k : never; -}[keyof T]; -type requiredKeys = { - [k in keyof T]: undefined extends T[k] ? never : k; -}[keyof T]; - -// eslint-disable-next-line @typescript-eslint/no-explicit-any -type AddQuestionMarks = { - [K in requiredKeys]: T[K]; -} & { - [K in optionalKeys]?: T[K]; -}; diff --git a/packages/podspec/src/typia/podspec.ts b/packages/podspec/src/typia/podspec.ts new file mode 100644 index 0000000..59f044d --- /dev/null +++ b/packages/podspec/src/typia/podspec.ts @@ -0,0 +1,12 @@ +import typia from "typia"; +import type { NamedPODSpecs } from "../builders/group.js"; +import type { PODGroupSpec } from "../builders/group.js"; +import type { PODSpec } from "../builders/pod.js"; +import type { EntryTypes } from "../builders/types/entries.js"; +import type { StatementMap } from "../builders/types/statements.js"; + +export const assertPODSpec = + typia.createAssert>(); + +export const assertPODGroupSpec = + typia.createAssert>(); diff --git a/packages/podspec/src/utils.ts b/packages/podspec/src/utils.ts deleted file mode 100644 index b28d813..0000000 --- a/packages/podspec/src/utils.ts +++ /dev/null @@ -1,16 +0,0 @@ -export function deepFreeze(obj: T): T { - if (typeof obj !== "object" || obj === null) { - return obj; - } - - Object.freeze(obj); - - Object.values(obj).forEach((value) => { - if (value instanceof Uint8Array) { - return; - } - deepFreeze(value); - }); - - return obj; -} diff --git a/packages/podspec/test/builders/PODGroupSpecBuilder.spec.ts b/packages/podspec/test/builders/PODGroupSpecBuilder.spec.ts new file mode 100644 index 0000000..6d8a283 --- /dev/null +++ b/packages/podspec/test/builders/PODGroupSpecBuilder.spec.ts @@ -0,0 +1,80 @@ +import { assertType, describe, expect, it } from "vitest"; +import type { AllPODEntries } from "../../src/builders/group.js"; +import { PODGroupSpecBuilder, PODSpecBuilder } from "../../src/index.js"; + +describe("PODGroupSpecBuilder", () => { + it("should be a test", () => { + expect(true).toBe(true); + }); + + it("PODGroupSpecBuilder", () => { + const group = PODGroupSpecBuilder.create(); + const podBuilder = PODSpecBuilder.create() + .entry("my_string", "string") + .entry("my_num", "int"); + const groupWithPod = group.pod("foo", podBuilder.spec()); + const _spec = groupWithPod.spec(); + + // Here we can see that, at the type level, we have the entry we defined + // for the 'foo' pod, as well as the virtual entries. + assertType>({ + "foo.my_string": "string", + "foo.my_num": "int", + "foo.$signerPublicKey": "eddsa_pubkey", + "foo.$contentID": "cryptographic", + }); + + expect(groupWithPod.spec()).toEqual({ + pods: { + foo: podBuilder.spec(), + }, + statements: {}, + }); + + const groupWithPodAndStatement = groupWithPod.isMemberOf( + ["foo.my_string"], + ["hello"] + ); + const spec3 = groupWithPodAndStatement.spec(); + + expect(spec3).toEqual({ + pods: { + foo: podBuilder.spec(), + }, + statements: { + "foo.my_string_isMemberOf": { + entries: ["foo.my_string"], + isMemberOf: [["hello"]], + type: "isMemberOf", + }, + }, + }); + }); + + it("debug equalsEntry types", () => { + const group = PODGroupSpecBuilder.create(); + const podBuilder = PODSpecBuilder.create() + .entry("my_string", "string") + .entry("my_other_string", "string") + .entry("my_num", "int") + .entry("my_other_num", "int"); + + const groupWithPod = group.pod("foo", podBuilder.spec()); + + // This should show us the concrete types + assertType["pods"]>>({ + "foo.my_string": "string", + "foo.my_other_string": "string", + "foo.my_num": "int", + "foo.my_other_num": "int", + "foo.$contentID": "cryptographic", + "foo.$signerPublicKey": "eddsa_pubkey", + }); + + groupWithPod.equalsEntry("foo.my_num", "foo.my_other_num"); + + // Now let's try to see what happens in equalsEntry + type _T1 = Parameters[0]; // First parameter type + type _T2 = Parameters[1]; // Second parameter type + }); +}); diff --git a/packages/podspec/test/builders/PODSpecBuilder.spec.ts b/packages/podspec/test/builders/PODSpecBuilder.spec.ts new file mode 100644 index 0000000..f6230a6 --- /dev/null +++ b/packages/podspec/test/builders/PODSpecBuilder.spec.ts @@ -0,0 +1,135 @@ +import { describe, expect, it } from "vitest"; +import type { EqualsEntry } from "../../src/builders/types/statements.js"; +import { PODSpecBuilder } from "../../src/index.js"; + +/* + @todo + - [ ] Adding statements of each kind + - [ ] isMemberOf + - [ ] isNotMemberOf + - [ ] inRange + - [ ] notInRange + - [ ] equalsEntry + - [ ] notEqualsEntry + - [ ] greaterThan + - [ ] greaterThanEq + - [ ] lessThan + - [ ] lessThanEq + - [x] Custom statement names + - [ ] Pick entries + - [ ] Pick statements + - [ ] Omit entries + - [ ] Omit statements + - [ ] Spec output matches expected output + - [ ] Test outputs for all of the above cases + - [ ] Erroneous inputs of all kinds + - [ ] Property tests +*/ + +describe("PODSpecBuilder", () => { + it("PODSpecBuilder", () => { + const a = PODSpecBuilder.create().entry("zzz", "string"); + const b = a.entry("a", "string").entry("b", "int"); + expect(b.spec().entries).toEqual({ + a: "string", + b: "int", + zzz: "string", + }); + + b.isMemberOf(["zzz"], ["fooo"]); + const c = b.isMemberOf(["a"], ["foo"]); + expect(c.spec().statements).toEqual({ + a_isMemberOf: { + entries: ["a"], + type: "isMemberOf", + isMemberOf: [["foo"]], + }, + }); + + const d = c.inRange("b", { min: 10n, max: 100n }); + expect(d.spec().statements).toEqual({ + a_isMemberOf: { + entries: ["a"], + type: "isMemberOf", + isMemberOf: [["foo"]], + }, + b_inRange: { + entries: ["b"], + type: "inRange", + inRange: { min: "10", max: "100" }, + }, + }); + + const e = d.isMemberOf(["a", "b"], [["foo", 10n]]); + expect(e.spec().statements.a_b_isMemberOf.entries).toEqual(["a", "b"]); + + const f = e.pickEntries(["b"]); + expect(f.spec().statements).toEqual({ + b_inRange: { + entries: ["b"], + type: "inRange", + inRange: { min: "10", max: "100" }, + }, + }); + + const g = e.entry("new", "string").equalsEntry("a", "new"); + const _GSpec = g.spec(); + const _GEntries = _GSpec.entries; + type EntriesType = typeof _GEntries; + _GSpec.statements.a_new_equalsEntry satisfies EqualsEntry< + EntriesType, + "a", + "new" + >; + + expect(g.spec().statements).toMatchObject({ + a_new_equalsEntry: { + entries: ["a", "new"], + type: "equalsEntry", + }, + }); + + expect(g.spec()).toEqual({ + entries: { + a: "string", + b: "int", + new: "string", + zzz: "string", + }, + statements: { + a_isMemberOf: { + entries: ["a"], + type: "isMemberOf", + isMemberOf: [["foo"]], + }, + a_b_isMemberOf: { + entries: ["a", "b"], + type: "isMemberOf", + // Note that the values are strings here, because we convert them to + // strings when persisting the spec. + isMemberOf: [["foo", "10"]], + }, + b_inRange: { + entries: ["b"], + type: "inRange", + // Note that the values are strings here, because we convert them to + // strings when persisting the spec. + inRange: { min: "10", max: "100" }, + }, + a_new_equalsEntry: { + entries: ["a", "new"], + type: "equalsEntry", + }, + }, + } satisfies typeof _GSpec); + + const h = g.pickStatements(["a_isMemberOf"]); + expect(h.spec().statements).toEqual({ + a_isMemberOf: { + entries: ["a"], + type: "isMemberOf", + isMemberOf: [["foo"]], + }, + }); + }); +}); diff --git a/packages/podspec/test/builders/fast-check/definitions.ts b/packages/podspec/test/builders/fast-check/definitions.ts new file mode 100644 index 0000000..6ffd8fe --- /dev/null +++ b/packages/podspec/test/builders/fast-check/definitions.ts @@ -0,0 +1,67 @@ +import { fc } from "@fast-check/vitest"; +import { + type PODValue, + POD_INT_MAX, + POD_INT_MIN, + POD_NAME_REGEX, +} from "@pcd/pod"; + +export const validEntryName = fc.oneof( + // Regular valid names + fc + .string({ minLength: 1 }) + .filter((s) => POD_NAME_REGEX.test(s)), + // Tricky JavaScript property names + fc.constantFrom( + "constructor", + "prototype", + "toString", + "valueOf", + "hasOwnProperty", + "length", + "name" + ) +); + +export const validEntryType = fc.constantFrom( + "string", + "int", + "boolean", + "date", + "bytes", + "cryptographic", + "null", + "eddsa_pubkey" +); + +const podValidInt = fc.bigInt(POD_INT_MIN, POD_INT_MAX); + +// For range statements +export const validIntRange = fc + .tuple(podValidInt, podValidInt) + .map(([a, b]) => ({ + min: a < b ? a : b, + max: a < b ? b : a, + })); + +export const validDateRange = fc.tuple(fc.date(), fc.date()).map(([a, b]) => ({ + min: a <= b ? a : b, + max: a <= b ? b : a, +})); + +// For membership statements +export const validStringValue = fc.string(); +export const validIntValue = podValidInt; +export const validBoolValue = fc.boolean(); +export const validDateValue = fc.date(); +export const validBytesValue = fc.uint8Array(); +// etc for other types + +// Invalid ranges +export const invalidRange = fc + .tuple(podValidInt, podValidInt) + .filter(([a, b]) => a !== b) + .map(([a, b]) => ({ + min: a > b ? a : b, + max: a > b ? b : a, + })); diff --git a/packages/podspec/test/builders/fast-check/pod.spec.ts b/packages/podspec/test/builders/fast-check/pod.spec.ts new file mode 100644 index 0000000..92986a6 --- /dev/null +++ b/packages/podspec/test/builders/fast-check/pod.spec.ts @@ -0,0 +1,94 @@ +import { fc, test } from "@fast-check/vitest"; +import { expect } from "vitest"; +import type { PODValueType } from "../../../src/builders/types/entries.js"; +import { UntypedPODSpecBuilder } from "../../../src/builders/untypedPod.js"; +import { validEntryName, validEntryType } from "./definitions.js"; + +test("UntypedPODSpecBuilder entries validation", () => { + fc.assert( + fc.property(fc.dictionary(validEntryName, validEntryType), (entries) => { + let builder = UntypedPODSpecBuilder.create(); + + // Add all entries + Object.entries(entries).forEach(([key, type]) => { + builder = builder.entry(key, type); + }); + + const result = builder.spec(); + + // Check that all entries are present with correct types + Object.entries(entries).forEach(([key, type]) => { + expect(result.entries[key]).toEqual(type); + }); + }) + ); +}); + +test("UntypedPODSpecBuilder pick entries", () => { + fc.assert( + fc.property( + fc.dictionary(validEntryName, fc.constant("string" as PODValueType)), + fc.array(fc.string({ minLength: 1 })), + (entries, toPick) => { + let builder = UntypedPODSpecBuilder.create(); + + // Add all entries + Object.entries(entries).forEach(([key, type]) => { + builder = builder.entry(key, type); + }); + + // Pick only specific entries + const pickedEntries = toPick + .filter((key) => Object.prototype.hasOwnProperty.call(entries, key)) + .reduce( + (acc, key) => { + acc[key] = entries[key] as string; + return acc; + }, + {} as Record + ); + + const result = builder.pickEntries(toPick).spec(); + + // Check that only picked entries are present + expect(Object.keys(result.entries)).toEqual(Object.keys(pickedEntries)); + Object.entries(pickedEntries).forEach(([key, type]) => { + expect(result.entries[key]).toEqual(type); + }); + } + ) + ); +}); + +test("UntypedPODSpecBuilder omit entries", () => { + fc.assert( + fc.property( + fc.dictionary(validEntryName, fc.constant("string" as PODValueType)), + fc.array(fc.string({ minLength: 1 })), + (entries, toOmit) => { + let builder = UntypedPODSpecBuilder.create(); + + // Add all entries + Object.entries(entries).forEach(([key, type]) => { + builder = builder.entry(key, type); + }); + + // Create expected entries after omission + const expectedEntries = { ...entries }; + toOmit.forEach((key) => { + delete expectedEntries[key]; + }); + + const result = builder.omitEntries(toOmit).spec(); + + // Check that omitted entries are not present + expect(Object.keys(result.entries)).toEqual( + Object.keys(expectedEntries) + ); + Object.entries(expectedEntries).forEach(([key, type]) => { + expect(result.entries[key]).toEqual(type); + }); + } + ) + ); +}); diff --git a/packages/podspec/test/builders/fast-check/statements.spec.ts b/packages/podspec/test/builders/fast-check/statements.spec.ts new file mode 100644 index 0000000..f7f4e33 --- /dev/null +++ b/packages/podspec/test/builders/fast-check/statements.spec.ts @@ -0,0 +1,86 @@ +import { fc, test } from "@fast-check/vitest"; +import { describe, expect } from "vitest"; +import { UntypedPODSpecBuilder } from "../../../src/builders/untypedPod.js"; +import { + invalidRange, + validEntryName, + validIntRange, + validIntValue, + validStringValue, +} from "./definitions.js"; + +describe("Statement Validation", () => { + // Range Statements + test("valid inRange statements should be accepted", () => { + fc.assert( + fc.property(validEntryName, validIntRange, (name, range) => { + const builder = UntypedPODSpecBuilder.create().entry(name, "int"); + expect(() => builder.inRange(name, range)).not.toThrow(); + }) + ); + }); + + test("invalid inRange statements should be rejected", () => { + fc.assert( + fc.property(validEntryName, invalidRange, (name, range) => { + const builder = UntypedPODSpecBuilder.create().entry(name, "int"); + + expect(() => builder.inRange(name, range)).toThrow(); + }) + ); + }); + + // Membership Statements + test("valid isMemberOf statements should be accepted", () => { + fc.assert( + fc.property( + validEntryName, + fc.array(validStringValue, { minLength: 1 }), + (name, values) => { + const builder = UntypedPODSpecBuilder.create().entry(name, "string"); + + expect(() => builder.isMemberOf([name], values)).not.toThrow(); + } + ) + ); + }); + + test("valid isMemberOf statements with tuples should be accepted", () => { + fc.assert( + fc.property( + fc.tuple(validEntryName, validEntryName).filter(([a, b]) => a !== b), + fc.array(fc.tuple(validStringValue, validIntValue), { + minLength: 1, + maxLength: 5, // Limit array size + }), + ([stringName, intName], tupleValues) => { + const builder = UntypedPODSpecBuilder.create() + .entry(stringName, "string") + .entry(intName, "int"); + + expect(() => + builder.isMemberOf([stringName, intName], tupleValues) + ).not.toThrow(); + } + ) + ); + }); + + // Comparison Statements + test("valid equalsEntry statements should be accepted", () => { + fc.assert( + fc.property( + fc.tuple(validEntryName, validEntryName).filter(([a, b]) => a !== b), + ([name1, name2]) => { + const builder = UntypedPODSpecBuilder.create() + .entry(name1, "string") + .entry(name2, "string"); + + expect(() => builder.equalsEntry(name1, name2)).not.toThrow(); + } + ) + ); + }); + + // ... similar tests for other statement types +}); diff --git a/packages/podspec/test/endToEnd.spec.ts b/packages/podspec/test/endToEnd.spec.ts new file mode 100644 index 0000000..2558fdf --- /dev/null +++ b/packages/podspec/test/endToEnd.spec.ts @@ -0,0 +1,332 @@ +import type { PODValue } from "@pcd/pod"; +import { assert, describe, expect, it } from "vitest"; +import { PODGroupSpecBuilder, PODSpecBuilder } from "../src/index.js"; +import { PODDB } from "../src/processors/db/podDB.js"; +import { groupValidator } from "../src/processors/validate/groupValidator.js"; +import { podValidator } from "../src/processors/validate/podValidator.js"; +import { signPOD, signerKeyPair } from "./fixtures.js"; + +describe("endToEnd", () => { + it("should be able to use specs to do various things", () => { + // First of all, we want to be able to describe a POD in the abstract. + { + // Here is a PODSpecBuilder: + const builder = PODSpecBuilder.create().entries({ + name: "string", + date_of_birth: "date", + email: "string", + }); + + // Calling the spec() method on the builder outputs a Spec. + const spec = builder.spec(); + + // A Spec is just some JSON-compatible data: + expect(spec).toEqual({ + entries: { + name: "string", + date_of_birth: "date", + email: "string", + }, + statements: {}, + }); + + // We can extend our builder to add more entries. + // Each builder instance is immutable, so adding an entry returns a new + // builder. + const builder2 = builder.entry("phone", "string"); + + // builder2 outputs a different spec, which includes the new entry: + const spec2 = builder2.spec(); + expect(spec2.entries).toMatchObject({ + phone: "string", + }); + + // We can also add "statements" to the spec. + // Let's add an "inRange" statement, constraining the date_of_birth + // entry. + const builder3 = builder2.inRange( + "date_of_birth", + { + min: new Date("1990-01-01"), + max: new Date("1999-12-31"), + }, + // We can optionally give our statements names, otherwise they'll be + // given a default name based on the statement type and the entries + // it operates on. Memorable names can convey intent. + "bornInThe90s" + ); + + // The new spec includes our statement: + const spec3 = builder3.spec(); + expect(spec3.statements).toEqual({ + bornInThe90s: { + type: "inRange", + entries: ["date_of_birth"], + inRange: { + // For JSON-compatibility, complex values are serialized to strings. + min: new Date("1990-01-01").getTime().toString(), + max: new Date("1999-12-31").getTime().toString(), + }, + }, + }); + + // From the top, our spec looks like this: + expect(spec3).toEqual({ + entries: { + name: "string", + date_of_birth: "date", + email: "string", + phone: "string", + }, + statements: { + bornInThe90s: { + type: "inRange", + entries: ["date_of_birth"], + inRange: { + min: new Date("1990-01-01").getTime().toString(), + max: new Date("1999-12-31").getTime().toString(), + }, + }, + }, + }); + + // What can we do with this? + + // We can use it to validate a POD + const pod = signPOD({ + name: { + type: "string", + value: "John Doe", + }, + date_of_birth: { + type: "date", + value: new Date("1995-01-01"), + }, + email: { + type: "string", + value: "john.doe@example.com", + }, + phone: { + type: "string", + value: "1234567890", + }, + }); + + const result = podValidator(spec3).validate(pod); + // This POD has all of the expect entries, and the statements are true, + // so it passes validation. + assert(result.isValid === true); + + // The result gives us our POD back, but now it's strongly-typed: + const validatedPod = result.value; + // Here's some TypeScript to prove it: + validatedPod.content.asEntries().date_of_birth.value satisfies Date; + validatedPod.content.asEntries().name.value satisfies string; + validatedPod.content.asEntries().email.value satisfies string; + validatedPod.content.asEntries().phone.value satisfies string; + // It's still a regular POD, and might have other entries our spec + // doesn't cover, and these have their original types: + validatedPod.content.asEntries().somethingElse satisfies + | PODValue + | undefined; + + // Let's try a bad POD, where the statements are false. + const badPod = signPOD({ + name: { + type: "string", + value: "Jim Bob", + }, + date_of_birth: { + type: "date", + // Such a near miss! + value: new Date("1989-12-31"), + }, + email: { + type: "string", + value: "jim.bob@example.com", + }, + phone: { + type: "string", + value: "1234567890", + }, + }); + + const result2 = podValidator(spec3).validate(badPod); + // Jim's POD is not valid + assert(!result2.isValid); + // We got a negative result from a statement + assert(result2.issues[0]?.code === "statement_negative_result"); + // Jim wasn't born in the 90s, so this statement is false + assert(result2.issues[0]?.statementName === "bornInThe90s"); + assert(result2.issues[0]?.statementType === "inRange"); + expect(result2.issues[0]?.entries).toEqual(["date_of_birth"]); + + // We can also create a new spec with some statements removed. + const builder4 = builder3.omitStatements(["bornInThe90s"]); + const spec4 = builder4.spec(); + expect(spec4.statements).toEqual({}); + + // Since we're not longer checking the "bornInThe90s" statement, this POD + // should now be valid. + const result3 = podValidator(spec4).validate(pod); + assert(result3.isValid === true); + + // We can reference virtual entries in statements: + const builder5 = builder4.isMemberOf( + ["$signerPublicKey"], + [signerKeyPair.publicKey] + ); + const spec5 = builder5.spec(); + + // We didn't give that statement a name, so it's given a default name + // based on the statement type and the entries it operates on, which in + // this case is "$signerPublicKey_isMemberOf". + expect(spec5.statements).toEqual({ + $signerPublicKey_isMemberOf: { + type: "isMemberOf", + entries: ["$signerPublicKey"], + isMemberOf: [[signerKeyPair.publicKey]], + }, + }); + + // Since the statement is true, the POD is valid. + const result4 = podValidator(spec5).validate(pod); + assert(result4.isValid === true); + + // We can also perform membership checks on tuples of entries, including + // a mix of regular and virtual entries: + const builder6 = builder5.isMemberOf( + ["$signerPublicKey", "date_of_birth", "$contentID"], + [ + [ + // Obviously these will match! + pod.signerPublicKey, + // Tuples are strongly-typed based on the entry types, so we need + // to provide inputs of the correct types, e.g. dates + new Date("1995-01-01"), + pod.contentID, + ], + ] + ); + const spec6 = builder6.spec(); + + // Since all of these entries match values in the POD, the statement is + // true, and the POD is valid. + const result5 = podValidator(spec6).validate(pod); + assert(result5.isValid === true); + + // We can also create specs for groups of PODs. + // Let's say we have a POD for a person, and a POD for a ticket. + // We'll use our existing builder for the person POD, and create a new + // builder for the ticket POD. + const otherBuilder = PODSpecBuilder.create().entries({ + eventName: "string", + attendeeEmail: "string", + }); + + // The PODs are named and grouped together in the spec. + const groupBuilder = PODGroupSpecBuilder.create() + .pod("person", builder6.spec()) + .pod("ticket", otherBuilder.spec()); + + // The spec for the group includes the specs for the individual PODs, + // and further statements, which can now cross-reference entries from + // either POD. + expect(groupBuilder.spec()).toEqual({ + pods: { + person: builder6.spec(), + ticket: otherBuilder.spec(), + }, + statements: {}, + }); + + // Let's say that the email addresses on the person and ticket PODs + // should match. We can add a statement to the group spec to enforce + // this. + const groupBuilder2 = groupBuilder.equalsEntry( + "person.email", + "ticket.attendeeEmail", + "matchingEmails" + ); + expect(groupBuilder2.spec()).toEqual({ + pods: { + person: builder6.spec(), + ticket: otherBuilder.spec(), + }, + statements: { + matchingEmails: { + type: "equalsEntry", + entries: ["person.email", "ticket.attendeeEmail"], + }, + }, + }); + + const ticketPod = signPOD({ + eventName: { + type: "string", + value: "Example Event", + }, + attendeeEmail: { + type: "string", + // Same email as used in the original `pod` above. + value: "john.doe@example.com", + }, + }); + + // Let's validate that each POD is valid. + const result6 = groupValidator(groupBuilder2.spec()).validate({ + person: pod, + ticket: ticketPod, + }); + + assert(result6.isValid === true); + + const result7 = groupValidator(groupBuilder2.spec()).validate({ + person: badPod, // This one has a different email address + ticket: ticketPod, + }); + + // The group is invalid because the person POD has a different email + // address than the ticket POD. + assert(result7.isValid === false); + assert(result7.issues[0]?.code === "statement_negative_result"); + assert(result7.issues[0]?.statementName === "matchingEmails"); + assert(result7.issues[0]?.statementType === "equalsEntry"); + expect(result7.issues[0]?.entries).toEqual([ + "person.email", + "ticket.attendeeEmail", + ]); + + // We can also use specs to find PODs in a database. + + // Let's use `spec3`, which finds people who were born in the 90s. + const db = new PODDB(); + db.insertMany([pod, ticketPod, badPod]); + + const peopleBornInThe90s = db.queryBySpec(spec3); + assert(peopleBornInThe90s.length === 1); + assert(peopleBornInThe90s[0] !== undefined); + assert( + peopleBornInThe90s[0].content.asEntries().name.value === "John Doe" + ); + // Query results are strongly-typed + peopleBornInThe90s[0].content.asEntries().name.value satisfies string; + + // Now let's use our group spec to find people who have a matching email + // address to the ticket POD. + const peopleWithMatchingEmails = db.queryByGroupSpec( + groupBuilder2.spec() + ); + assert(peopleWithMatchingEmails.length === 1); + assert(peopleWithMatchingEmails[0] !== undefined); + assert( + peopleWithMatchingEmails[0].person.content.asEntries().email.value === + "john.doe@example.com" + ); + assert( + peopleWithMatchingEmails[0].ticket.content.asEntries().attendeeEmail + .value === "john.doe@example.com" + ); + } + }); +}); diff --git a/packages/podspec/test/fixtures.ts b/packages/podspec/test/fixtures.ts new file mode 100644 index 0000000..482a402 --- /dev/null +++ b/packages/podspec/test/fixtures.ts @@ -0,0 +1,102 @@ +import { POD, type PODEntries } from "@pcd/pod"; +import { generateKeyPair } from "./utils.js"; + +export const signerKeyPair = generateKeyPair(); + +const serializedFrogPOD = { + entries: { + beauty: 4, + biome: 3, + description: + 'The Blue Poison Dart Frog\'s vivid blue skin contains potent alkaloid toxins, which are derived from the insects they consume in the wild and serve as a defense mechanism against predators. Indigenous people of Central and South America have utilized these toxins to poison the tips of blowdarts, a traditional method of hunting small game, giving the frog its "dart frog" name.', + frogId: 24, + imageUrl: + "https://api.zupass.org/frogcrypto/images/84155a67-7004-45a6-b4b4-88ecf82aa685", + intelligence: 6, + jump: 2, + name: "Blue Poison Dart Frog", + owner: { + cryptographic: + "0x16949a3e1331b41d4e3c75897e131659c0fee2645246409c31e8736cf3fef2ad", + }, + ownerPubKey: { + eddsa_pubkey: "iidEhpJV0+1wbABFyXAZaI0xPBTAzfj37s27yIIzeKs", + }, + pod_type: "frogcrypto.frog", + rarity: 1, + speed: 7, + temperament: 16, + timestampSigned: 1736429797878, + }, + signature: + "4BXTY+DwMGR74ftdQ6MgVldOoM0alWn6WPgI2bLxeoPWYMOOMK8bJHpJkV0rbvXlKyjKw9nkK6eJ5UOby5pOBA", + signerPublicKey: "4sGycsjU8rG8FzKyvZd9h9632oR0qhs6DoNk4YTkHSE", +}; + +const frogPODEntries = POD.fromJSON(serializedFrogPOD).content.asEntries(); + +export function signPOD(entries: PODEntries): POD { + return POD.sign(entries, signerKeyPair.privateKey); +} + +export function newFrogPOD({ + owner = signerKeyPair.publicKey, + beauty = Math.round(Math.random() * 10), + intelligence = Math.round(Math.random() * 10), + jump = Math.round(Math.random() * 10), + speed = Math.round(Math.random() * 10), +}: { + owner?: string; + beauty?: number; + intelligence?: number; + jump?: number; + speed?: number; +} = {}): POD { + const entries: PODEntries = { + ...frogPODEntries, + beauty: { value: BigInt(beauty), type: "int" }, + intelligence: { value: BigInt(intelligence), type: "int" }, + jump: { value: BigInt(jump), type: "int" }, + speed: { value: BigInt(speed), type: "int" }, + ownerPubKey: { type: "eddsa_pubkey", value: owner }, + }; + return POD.sign(entries, signerKeyPair.privateKey); +} + +export function newTicketPOD({ + owner = signerKeyPair.publicKey, + eventId = "event", + productId = "product", + ticketId = "ticket", +}: { + owner?: string; + eventId?: string; + productId?: string; + ticketId?: string; +} = {}): POD { + const entries: PODEntries = { + attendeeEmail: { value: "user@test.com", type: "string" }, + attendeeName: { value: "test name", type: "string" }, + attendeeSemaphoreId: { value: 12345n, type: "cryptographic" }, + checkerEmail: { value: "checker@test.com", type: "string" }, + eventId: { value: eventId, type: "string" }, + eventName: { value: "event", type: "string" }, + isConsumed: { value: 0n, type: "int" }, + isRevoked: { value: 0n, type: "int" }, + owner: { + value: owner, + type: "eddsa_pubkey", + }, + pod_type: { value: "zupass.ticket", type: "string" }, + productId: { + value: productId, + type: "string", + }, + ticketCategory: { value: 1n, type: "int" }, + ticketId: { value: ticketId, type: "string" }, + ticketName: { value: "ticket", type: "string" }, + timestampConsumed: { value: 1731888000000n, type: "int" }, + timestampSigned: { value: 1731283200000n, type: "int" }, + }; + return POD.sign(entries, signerKeyPair.privateKey); +} diff --git a/packages/podspec/test/podspec.spec.ts b/packages/podspec/test/podspec.spec.ts deleted file mode 100644 index 9a3f0f7..0000000 --- a/packages/podspec/test/podspec.spec.ts +++ /dev/null @@ -1,817 +0,0 @@ -import type { GPCBoundConfig } from "@pcd/gpc"; -import { gpcProve, gpcVerify } from "@pcd/gpc"; -import { POD, POD_INT_MAX, POD_INT_MIN } from "@pcd/pod"; -import { v4 as uuidv4 } from "uuid"; -import { assert, describe, expect, it } from "vitest"; -import type { - PodspecNotInListIssue, - PodspecNotInRangeIssue, - PodspecNotInTupleListIssue -} from "../src/error.js"; -import { IssueCode } from "../src/error.js"; -import * as p from "../src/index.js"; -import { $b, $bs, $c, $i, $s } from "../src/pod_value_utils.js"; -import type { EntriesTupleSchema } from "../src/schemas/entries.js"; -import { GPC_NPM_ARTIFACTS_PATH } from "./constants.js"; -import { generateKeyPair, generateRandomHex } from "./utils.js"; - -describe("podspec should work", function () { - it("should validate POD entries", () => { - // To begin with, we can create a specification for POD entries. - // This is useful for validating POD entries before signing them. - const entriesSpec = p.entries({ - firstName: { - type: "string", - // $s is a utility function for turning strings into PODStringValues - isMemberOf: $s(["test", "1234"]) - }, - age: { - type: "int", - isNotMemberOf: $i([42]) - }, - semaphoreId: { - type: "cryptographic", - isMemberOf: $c([1000]) - }, - publicKey: { - type: "eddsa_pubkey" - }, - isActive: { - type: "boolean", - isMemberOf: [$b(true)] - }, - noneExistent: { - type: "null" - }, - byteSequence: { - type: "bytes", - isMemberOf: [$bs(new Uint8Array([1, 2, 3]))] - }, - registrationDate: { - type: "date", - inRange: { - min: BigInt(new Date("2024-01-01").getTime()), - max: BigInt(new Date("2024-12-31").getTime()) - } - } - }); - - // Generate a random valid public key - const { publicKey } = generateKeyPair(); - - // Parse some POD entries - const result = entriesSpec.safeParse({ - firstName: { - type: "string", - value: "test" - }, - age: { - type: "int", - value: 41n - }, - semaphoreId: { - type: "cryptographic", - value: 1000n - }, - publicKey: { - type: "eddsa_pubkey", - value: publicKey - }, - isActive: { - type: "boolean", - value: true - }, - noneExistent: { - type: "null", - value: null - }, - byteSequence: { - type: "bytes", - value: new Uint8Array([1, 2, 3]) - }, - registrationDate: { - type: "date", - value: new Date("2024-05-01") - } - }); - - // The result should be valid - assert(result.isValid); - expect(result.value.firstName).to.eql({ type: "string", value: "test" }); - expect(result.value.age).to.eql({ type: "int", value: 41n }); - expect(result.value.semaphoreId).to.eql({ - type: "cryptographic", - value: 1000n - }); - expect(result.value.publicKey).to.eql({ - type: "eddsa_pubkey", - value: publicKey - }); - expect(result.value.isActive).to.eql({ type: "boolean", value: true }); - expect(result.value.noneExistent).to.eql({ type: "null", value: null }); - expect(result.value.byteSequence).to.eql({ - type: "bytes", - value: new Uint8Array([1, 2, 3]) - }); - expect(result.value.registrationDate).to.eql({ - type: "date", - value: new Date("2024-05-01") - }); - }); - - /** - * If we want to create a new POD and we have some JavaScript values, we can - * do some simple coercion of those JavaScript values into POD values. This - * is achieved by wrapping the plain values in a {@link PODValue}, or in the - * case of JavaScript numbers, converting them to bigints before wrapping - * them. - */ - it("should coerce javascript values into POD types", function () { - const entriesSpec = p.entries({ - firstName: { - type: "string", - isMemberOf: $s(["test", "1234"]) - }, - age: { - type: "int", - isNotMemberOf: $i([42]) - }, - semaphoreId: { - type: "cryptographic", - isMemberOf: $c([1000]) - }, - publicKey: { - type: "eddsa_pubkey" - }, - isActive: { - type: "boolean", - isMemberOf: [$b(true)] - }, - noneExistent: { - type: "null" - }, - byteSequence: { - type: "bytes", - isMemberOf: [$bs(new Uint8Array([1, 2, 3]))] - }, - registrationDate: { - type: "date", - inRange: { - min: BigInt(new Date("2024-01-01").getTime()), - max: BigInt(new Date("2024-12-31").getTime()) - } - } - }); - - const { publicKey } = generateKeyPair(); - - const result = entriesSpec.safeParse( - { - firstName: "test", - age: 41, // numbers can be coerced to bigint - semaphoreId: 1000n, - publicKey: publicKey, - isActive: true, - noneExistent: null, - byteSequence: new Uint8Array([1, 2, 3]), - registrationDate: new Date("2024-05-01") - }, - { coerce: true } - ); - - assert(result.isValid); - expect(result.value.firstName).to.eql({ type: "string", value: "test" }); - expect(result.value.age).to.eql({ type: "int", value: 41n }); - expect(result.value.semaphoreId).to.eql({ - type: "cryptographic", - value: 1000n - }); - expect(result.value.publicKey).to.eql({ - type: "eddsa_pubkey", - value: publicKey - }); - }); - - it("should fail with bad inputs", function () { - const myPodSpec = p.entries({ - foo: { type: "string" }, - bar: { type: "int" } - }); - - const result = myPodSpec.safeParse( - { - foo: "test", - bar: POD_INT_MAX + 1n - }, - { coerce: true } - ); - expect(result.isValid).to.eq(false); - assert(result.isValid === false); - expect(result.issues).to.eql([ - { - code: IssueCode.invalid_pod_value, - value: { - type: "int", - value: POD_INT_MAX + 1n - }, - reason: `Invalid value for entry ${"bar"}. Value ${ - POD_INT_MAX + 1n - } is outside supported bounds: (min ${POD_INT_MIN}, max ${POD_INT_MAX}).`, - path: ["bar"] - } - ]); - }); - - it("should fail to instantiate a Podspec with invalid entries", function () { - expect(() => - p.entries({ - foo: { type: "string" }, - // This is simply invalid data, which would normally cause a type error - // in TypeScript, but such errors can be overridden and would never - // occur in JavaScript. - bar: { type: "invalid" } as never - }) - ).to.throw(); - }); - - // Integer and Cryptographic entries can be checked to ensure that their - // values are within certain bounds. - it("should apply range checks", function () { - const myPodSpec = p.entries({ - foo: { type: "int", inRange: { min: 1n, max: 10n } } - }); - - const result = myPodSpec.safeParse({ - foo: { type: "int", value: 11n } - }); - expect(result.isValid).to.eq(false); - assert(result.isValid === false); - expect(result.issues).to.eql([ - { - code: IssueCode.not_in_range, - min: 1n, - max: 10n, - value: 11n, - path: ["foo"] - } satisfies PodspecNotInRangeIssue - ]); - }); - - // All entries can be checked to ensure that their values are members of - // a list. - it("should test string entries for list membership", function () { - const myPodSpec = p.entries({ - foo: { - type: "string", - isMemberOf: $s(["test", "other_string"]) - } - }); - - const result = myPodSpec.safeParse({ - foo: { type: "string", value: "test" } - }); - expect(result.isValid).to.eq(true); - assert(result.isValid); - - const result2 = myPodSpec.safeParse({ - foo: { type: "string", value: "not in list" } - }); - expect(result2.isValid).to.eq(false); - assert(result2.isValid === false); - expect(result2.issues).to.eql([ - { - code: IssueCode.not_in_list, - value: { type: "string", value: "not in list" }, - list: $s(["test", "other_string"]), - path: ["foo"] - } satisfies PodspecNotInListIssue - ]); - }); - - // All entries can be checked to ensure that their values are members of - // a list. - it("should test integer entries for list membership", function () { - const myPodSpec = p.entries({ - foo: { type: "int", isMemberOf: $i([1n, 2n, 3n]) } - }); - - const result = myPodSpec.safeParse({ - foo: { - type: "int", - value: 1n - } - }); - expect(result.isValid).to.eq(true); - assert(result.isValid); - - const result2 = myPodSpec.safeParse({ - foo: { - type: "int", - value: 4n - } - }); - expect(result2.isValid).to.eq(false); - assert(!result2.isValid); - expect(result2.issues).to.eql([ - { - code: IssueCode.not_in_list, - value: { type: "int", value: 4n }, - list: $i([1n, 2n, 3n]), - path: ["foo"] - } satisfies PodspecNotInListIssue - ]); - }); - - // Tuples are used to match on multiple entries at once. - it("should match on tuples", function () { - const myPodSpec = p.entries({ - foo: { type: "string" }, - bar: { type: "int" } - }); - - // Define a tuple of valid entries - const tuples: EntriesTupleSchema>[] = [ - { - // The entries to be checked - entries: ["foo", "bar"], - // The list of valid tuples - isMemberOf: [ - [ - { type: "string", value: "test" }, - { type: "int", value: 1n } - ] - ] - } - ]; - - { - const result = myPodSpec.safeParse( - { - foo: { type: "string", value: "test" }, - bar: { type: "int", value: 1n } - }, - { - tuples - } - ); - expect(result.isValid).to.eq(true); - } - { - const result = myPodSpec.safeParse( - { - foo: { type: "string", value: "other string" }, - bar: { type: "int", value: 1n } - }, - { tuples } - ); - expect(result.isValid).to.eq(false); - assert(result.isValid === false); - expect(result.issues).to.eql([ - { - code: IssueCode.not_in_tuple_list, - value: [ - { type: "string", value: "other string" }, - { type: "int", value: 1n } - ], - list: [ - [ - { type: "string", value: "test" }, - { type: "int", value: 1n } - ] - ], - path: ["$tuples", "0"] - } satisfies PodspecNotInTupleListIssue - ]); - } - { - const result = myPodSpec.safeParse( - { - foo: { type: "string", value: "test" }, - bar: { type: "int", value: 2n } - }, - { tuples } - ); - expect(result.isValid).to.eq(false); - assert(result.isValid === false); - expect(result.issues).to.eql([ - { - code: IssueCode.not_in_tuple_list, - value: [ - { type: "string", value: "test" }, - { type: "int", value: 2n } - ], - list: [ - [ - { type: "string", value: "test" }, - { type: "int", value: 1n } - ] - ], - path: ["$tuples", "0"] - } satisfies PodspecNotInTupleListIssue - ]); - } - }); - - // Optional entries are those which may or may not be present in a POD. - // If present, the entry's "innerType" is used for validation. - it("should handle optional entries", function () { - const optionalPodSpec = p.entries({ - foo: { type: "string" }, - bar: { type: "optional", innerType: { type: "int" } } - }); - - const resultWithOptional = optionalPodSpec.safeParse({ - foo: { type: "string", value: "test" }, - bar: { type: "int", value: 123n } - }); - expect(resultWithOptional.isValid).to.eq(true); - - const resultWithoutOptional = optionalPodSpec.safeParse({ - foo: { type: "string", value: "test" } - }); - expect(resultWithoutOptional.isValid).to.eq(true); - }); - - // In addition to validating sets of entries, we can validate existing PODs. - it("should validate entire PODs", function () { - const { privateKey } = generateKeyPair(); - const eventId = "d1390b7b-4ccb-42bf-8c8b-e397b7c26e6c"; - const productId = "d38f0c3f-586b-44c6-a69a-1348481e927d"; - - const myPodSpec = p.pod({ - entries: { - eventId: { type: "string" }, - productId: { type: "string" }, - imageUrl: { type: "optional", innerType: { type: "string" } } - } - }); - - const pod = POD.sign( - { - eventId: { type: "string", value: eventId }, - productId: { type: "string", value: productId } - }, - privateKey - ); - - const result = myPodSpec.safeParse(pod); - assert(result.isValid); - }); - - // Tuple checks on PODs include a special virtual entry representing the - // POD's signer. - it("should perform tuple checks on PODs including virtual signer entry", function () { - const { publicKey, privateKey } = generateKeyPair(); - const eventId = "d1390b7b-4ccb-42bf-8c8b-e397b7c26e6c"; - const productId = "d38f0c3f-586b-44c6-a69a-1348481e927d"; - - const myPodSpec = p.pod({ - entries: { - eventId: { type: "string" }, - productId: { type: "string" } - }, - tuples: [ - { - entries: ["eventId", "productId", "$signerPublicKey"], - isMemberOf: [ - [ - { type: "string", value: eventId }, - { type: "string", value: productId }, - { type: "eddsa_pubkey", value: publicKey } - ] - ] - } - ] - }); - - { - const pod = POD.sign( - { - eventId: { type: "string", value: eventId }, - productId: { type: "string", value: productId } - }, - privateKey - ); - - const result = myPodSpec.safeParse(pod); - expect(result.isValid).to.eq(true); - assert(result.isValid); - } - { - const pod = POD.sign( - { - eventId: { type: "string", value: uuidv4() }, - productId: { type: "string", value: uuidv4() }, - foo: { type: "eddsa_pubkey", value: publicKey } - }, - privateKey - ); - - const result = myPodSpec.safeParse(pod); - expect(result.isValid).to.eq(false); - assert(result.isValid === false); - assert(result.issues[0] !== undefined); - expect(result.issues[0].code).to.eq(IssueCode.not_in_tuple_list); - } - }); - - // Queries can be performed across multiple PODs to find those which match - // a given PODSpec. - it("should query across multiple PODs", function () { - const key = generateRandomHex(32); - - const myPodSpec = p.pod({ - entries: { - foo: { type: "string" }, - bar: { type: "int" } - } - }); - - const pods = [ - POD.sign( - { - foo: { type: "string", value: "just a string" } - }, - key - ), - POD.sign( - { - foo: { type: "string", value: "test" }, - bar: { type: "int", value: 1n } - }, - key - ) - ]; - - const queryResult = myPodSpec.query(pods); - - expect(queryResult.matches[0]).to.eq(pods[1]); - expect(queryResult.matchingIndexes).to.eql([1]); - }); - - // Range checks can also be applied in queries. - it("should apply range checks in queries", function () { - const key = generateRandomHex(32); - const myPodSpec = p.pod({ - entries: { - foo: { type: "int", inRange: { min: 1n, max: 10n } } - } - }); - - const pods = [ - POD.sign({ foo: { type: "int", value: 1n } }, key), - POD.sign({ foo: { type: "int", value: 11n } }, key), - POD.sign({ foo: { type: "int", value: 0n } }, key), - POD.sign({ foo: { type: "int", value: 10n } }, key) - ]; - - const queryResult = myPodSpec.query(pods); - - expect(queryResult.matches).to.eql([pods[0], pods[3]]); - expect(queryResult.matchingIndexes).to.eql([0, 3]); - }); - - // Tuple checks also work in queries. - it("should match on tuples in queries", function () { - const { publicKey, privateKey } = generateKeyPair(); - const eventId = "d1390b7b-4ccb-42bf-8c8b-e397b7c26e6c"; - const productId = "d38f0c3f-586b-44c6-a69a-1348481e927d"; - - const myPodSpec = p.pod({ - entries: { - eventId: { type: "string" }, - productId: { type: "string" } - }, - tuples: [ - { - entries: ["eventId", "productId", "$signerPublicKey"], - isMemberOf: [ - [ - { type: "string", value: eventId }, - { type: "string", value: productId }, - { type: "eddsa_pubkey", value: publicKey } - ] - ] - } - ] - }); - - const pods = [ - POD.sign( - { - eventId: { type: "string", value: eventId }, - productId: { type: "string", value: productId } - }, - privateKey - ), - POD.sign( - { - eventId: { type: "string", value: uuidv4() }, - productId: { type: "string", value: uuidv4() } - }, - privateKey - ) - ]; - - const queryResult = myPodSpec.query(pods); - - expect(queryResult.matches).to.eql([pods[0]]); - expect(queryResult.matchingIndexes).to.eql([0]); - }); - - // Queries can also be used to match on signatures. - it("can query for PODs with matching signatures", function () { - const { publicKey, privateKey } = generateKeyPair(); - const eventId = "d1390b7b-4ccb-42bf-8c8b-e397b7c26e6c"; - const productId = "d38f0c3f-586b-44c6-a69a-1348481e927d"; - - const pods = [ - POD.sign( - { - eventId: { type: "string", value: eventId }, - productId: { type: "string", value: productId }, - signerPublicKey: { type: "eddsa_pubkey", value: publicKey } - }, - privateKey - ), - POD.sign( - { - eventId: { type: "string", value: uuidv4() }, - productId: { type: "string", value: uuidv4() }, - signerPublicKey: { type: "eddsa_pubkey", value: publicKey } - }, - privateKey - ) - ] as const; - - const myPodSpec = p.pod({ - entries: { - eventId: { type: "string" }, - productId: { type: "string" } - }, - signature: { - isMemberOf: [pods[1].signature] - } - }); - - const queryResult = myPodSpec.query(pods.slice()); - - expect(queryResult.matches).to.eql([pods[1]]); - expect(queryResult.matchingIndexes).to.eql([1]); - }); - - it("should generate proof requests", async function () { - const pod1 = p.pod({ - entries: { - foo: { type: "string" }, - bar: { - type: "int", - inRange: { min: 0n, max: 10n } - } - }, - tuples: [ - { - entries: ["foo", "bar"], - isMemberOf: [ - [ - { type: "string", value: "test" }, - { type: "int", value: 5n } - ] - ] - } - ] - }); - const prs = p.proofRequest({ - pods: { - pod1: pod1.proofConfig({ revealed: { foo: true, bar: true } }) - }, - watermark: { type: "string", value: "1" }, - externalNullifier: { type: "string", value: "1" } - }); - - const { privateKey } = generateKeyPair(); - - const pod = POD.sign( - { - foo: { type: "string", value: "test" }, - bar: { type: "int", value: 5n } - }, - privateKey - ); - - const pr = prs.getProofRequest(); - - const proof = await gpcProve( - pr.proofConfig, - { - membershipLists: pr.membershipLists, - pods: { pod1: pod } - }, - GPC_NPM_ARTIFACTS_PATH - ); - - pr.proofConfig.circuitIdentifier = proof.boundConfig.circuitIdentifier; - - const result = await gpcVerify( - proof.proof, - pr.proofConfig as GPCBoundConfig, - proof.revealedClaims, - GPC_NPM_ARTIFACTS_PATH - ); - assert(result); - }); - - it("should generate candidate inputs from a POD collection", async function () { - const firstPodSpec = p.pod({ - entries: { - foo: { type: "string" }, - bar: { - type: "int", - inRange: { min: 0n, max: 10n } - } - } - }); - - const secondPodSpec = p.pod({ - entries: { - baz: { type: "cryptographic" }, - quux: { - type: "string", - isMemberOf: [{ type: "string", value: "magic" }] - } - } - }); - - const prs = p.proofRequest({ - pods: { - pod1: firstPodSpec.proofConfig({ revealed: { foo: true, bar: true } }), - pod2: secondPodSpec.proofConfig({ - revealed: { baz: true, quux: false } - }) - } - }); - - const { privateKey } = generateKeyPair(); - - // This simulates a user's POD collection, where we want to find the PODs - // which could be inputs for the proof request - const pods: POD[] = [ - POD.sign( - { - foo: { type: "string", value: "aaaaaa" }, - bar: { type: "int", value: 11n } - }, - privateKey - ), - POD.sign( - { - foo: { type: "string", value: "bbbbbb" }, - bar: { type: "int", value: 10n } - }, - privateKey - ), - POD.sign( - { - baz: { type: "cryptographic", value: 1000000000n }, - quux: { type: "string", value: "doesn't match" } - }, - privateKey - ), - POD.sign( - { - baz: { type: "cryptographic", value: 1000n }, - quux: { type: "string", value: "magic" } - }, - privateKey - ) - ]; - - const candidatePODs = prs.queryForInputs(pods); - - expect(candidatePODs.pod1).to.eql([pods[1]]); - expect(candidatePODs.pod2).to.eql([pods[3]]); - assert(candidatePODs.pod1[0] !== undefined); - assert(candidatePODs.pod2[0] !== undefined); - - const pr = prs.getProofRequest(); - - const proof = await gpcProve( - pr.proofConfig, - { - membershipLists: pr.membershipLists, - pods: { pod1: candidatePODs.pod1[0], pod2: candidatePODs.pod2[0] } - }, - GPC_NPM_ARTIFACTS_PATH - ); - - pr.proofConfig.circuitIdentifier = proof.boundConfig.circuitIdentifier; - - const result = await gpcVerify( - proof.proof, - pr.proofConfig as GPCBoundConfig, - proof.revealedClaims, - GPC_NPM_ARTIFACTS_PATH - ); - assert(result); - }); -}); diff --git a/packages/podspec/test/processors/db/db.spec.ts b/packages/podspec/test/processors/db/db.spec.ts new file mode 100644 index 0000000..c4db0c3 --- /dev/null +++ b/packages/podspec/test/processors/db/db.spec.ts @@ -0,0 +1,46 @@ +import { POD, type PODEntries } from "@pcd/pod"; +import { describe, expect, it } from "vitest"; +import { PODSpecBuilder } from "../../../src/builders/pod.js"; +import { PODGroupSpecBuilder } from "../../../src/index.js"; +import { PODDB } from "../../../src/processors/db/podDB.js"; +import { generateKeyPair } from "../../utils.js"; + +describe("PODDB", () => { + const { privateKey } = generateKeyPair(); + const podSign = (pod: PODEntries) => POD.sign(pod, privateKey); + + const pod1 = podSign({ + id: { type: "int", value: 1n }, + num: { type: "int", value: 50n }, + }); + + const pod2 = podSign({ + id: { type: "int", value: 2n }, + other_id: { type: "int", value: 1n }, + }); + + const pod3 = podSign({ + id: { type: "int", value: 3n }, + other_id: { type: "int", value: 1n }, + }); + + it("should be able to query by spec", () => { + const db = new PODDB(); + db.insertMany([pod1, pod2, pod3]); + + const podSpec = PODGroupSpecBuilder.create() + .pod("pod1", PODSpecBuilder.create().entry("id", "int").spec()) + .pod("pod2", PODSpecBuilder.create().entry("other_id", "int").spec()) + .equalsEntry("pod1.id", "pod2.other_id") + .spec(); + + const result = db.queryByGroupSpec(podSpec); + expect(result.length).toBe(2); + // We have two valid combinations. In both cases, the pod1 slot is filled + // by pod1, and the pod2 slot is filled by pod2 and pod3 respectively. + expect(result[0]!.pod1.contentID).toBe(pod1.contentID); + expect(result[0]!.pod2.contentID).toBe(pod2.contentID); + expect(result[1]!.pod1.contentID).toBe(pod1.contentID); + expect(result[1]!.pod2.contentID).toBe(pod3.contentID); + }); +}); diff --git a/packages/podspec/test/processors/proofRequest/proofRequest.ts b/packages/podspec/test/processors/proofRequest/proofRequest.ts new file mode 100644 index 0000000..e69de29 diff --git a/packages/podspec/test/processors/validator/EntrySource.spec.ts b/packages/podspec/test/processors/validator/EntrySource.spec.ts new file mode 100644 index 0000000..ad8126f --- /dev/null +++ b/packages/podspec/test/processors/validator/EntrySource.spec.ts @@ -0,0 +1,103 @@ +import { POD, type PODEntries } from "@pcd/pod"; +import { describe, expect, it } from "vitest"; +import { PODGroupSpecBuilder, PODSpecBuilder } from "../../../src/index.js"; +import { + EntrySourcePodGroupSpec, + EntrySourcePodSpec, +} from "../../../src/processors/validate/EntrySource.js"; +import { generateKeyPair } from "../../utils.js"; + +describe("EntrySource", () => { + const { privateKey, publicKey } = generateKeyPair(); + const podSign = (pod: PODEntries) => POD.sign(pod, privateKey); + + it("should retrieve entries for a PODSpec", () => { + const podSpec = PODSpecBuilder.create() + .entry("id", "int") + .entry("num", "int") + .spec(); + const pod = podSign({ + id: { type: "int", value: 1000n }, + num: { type: "int", value: 50n }, + }); + + const entrySource = new EntrySourcePodSpec(podSpec, pod); + + expect(entrySource.getEntry("id")!.value).toBe(1000n); + expect(entrySource.getEntryTypeFromSpec("id")).toBe("int"); + + expect(entrySource.getEntry("$signerPublicKey")!.value).toBe(publicKey); + expect(entrySource.getEntryTypeFromSpec("$signerPublicKey")).toBe( + "eddsa_pubkey" + ); + + expect(entrySource.getEntry("$contentID")!.value).toBe( + pod.content.contentID + ); + expect(entrySource.getEntryTypeFromSpec("$contentID")).toBe( + "cryptographic" + ); + + expect(entrySource.getEntry("foo")).toBeUndefined(); + expect(entrySource.getEntryTypeFromSpec("foo")).toBeUndefined(); + }); + + it("should retrieve entries for a PODGroupSpec", () => { + const podA = podSign({ + id: { type: "int", value: 1000n }, + num: { type: "int", value: 50n }, + }); + const podB = podSign({ + foo: { type: "string", value: "foo" }, + other_id: { type: "int", value: 1000n }, + }); + const podGroupSpec = PODGroupSpecBuilder.create() + .pod( + "podA", + PODSpecBuilder.create().entry("id", "int").entry("num", "int").spec() + ) + .pod( + "podB", + PODSpecBuilder.create() + .entry("foo", "string") + .entry("other_id", "int") + .spec() + ) + .spec(); + + const entrySource = new EntrySourcePodGroupSpec(podGroupSpec, { + podA: podA, + podB: podB, + }); + + expect(entrySource.getEntry("podA.id")!.value).toBe(1000n); + expect(entrySource.getEntry("podA.num")!.value).toBe(50n); + expect(entrySource.getEntry("podB.foo")!.value).toBe("foo"); + expect(entrySource.getEntry("podB.other_id")!.value).toBe(1000n); + + expect(entrySource.getEntryTypeFromSpec("podA.id")).toBe("int"); + expect(entrySource.getEntryTypeFromSpec("podA.num")).toBe("int"); + expect(entrySource.getEntryTypeFromSpec("podB.foo")).toBe("string"); + expect(entrySource.getEntryTypeFromSpec("podB.other_id")).toBe("int"); + + expect(entrySource.getEntry("podA.$signerPublicKey")!.value).toBe( + publicKey + ); + expect(entrySource.getEntryTypeFromSpec("podA.$signerPublicKey")).toBe( + "eddsa_pubkey" + ); + + expect(entrySource.getEntry("podA.$contentID")!.value).toBe( + podA.content.contentID + ); + expect(entrySource.getEntryTypeFromSpec("podA.$contentID")).toBe( + "cryptographic" + ); + + expect(entrySource.getEntry("podC.id")).toBeUndefined(); + expect(entrySource.getEntryTypeFromSpec("podC.id")).toBeUndefined(); + + expect(entrySource.getEntry("podA.something")).toBeUndefined(); + expect(entrySource.getEntryTypeFromSpec("podA.something")).toBeUndefined(); + }); +}); diff --git a/packages/podspec/test/processors/validator/groupValidator.spec.ts b/packages/podspec/test/processors/validator/groupValidator.spec.ts new file mode 100644 index 0000000..13b5adb --- /dev/null +++ b/packages/podspec/test/processors/validator/groupValidator.spec.ts @@ -0,0 +1,91 @@ +import { POD } from "@pcd/pod"; +import type { PODEntries } from "@pcd/pod"; +import { assert, describe, expect, it } from "vitest"; +import { PODSpecBuilder } from "../../../src/index.js"; +import { PODGroupSpecBuilder } from "../../../src/index.js"; +import { groupValidator } from "../../../src/processors/validate/groupValidator.js"; +import { IssueCode } from "../../../src/processors/validate/issues.js"; +import { generateKeyPair } from "../../utils.js"; + +describe("groupValidator", () => { + const { privateKey } = generateKeyPair(); + + const signPOD = (entries: PODEntries) => POD.sign(entries, privateKey); + + const podA = signPOD({ + id: { type: "int", value: 1000n }, + num: { type: "int", value: 50n }, + }); + + const podB = signPOD({ + foo: { type: "string", value: "foo" }, + other_id: { type: "int", value: 1000n }, + }); + + const groupSpecBuilder = PODGroupSpecBuilder.create() + .pod( + "podA", + PODSpecBuilder.create().entry("id", "int").entry("num", "int").spec() + ) + .pod( + "podB", + PODSpecBuilder.create() + .entry("foo", "string") + .entry("other_id", "int") + .spec() + ); + + it("should validate unrelated pods in a group", () => { + const validator = groupValidator(groupSpecBuilder.spec()); + const result = validator.validate({ + podA, + podB, + }); + + assert(result.isValid); + expect(result.value.podA.content.asEntries().id.value).toBe(1000n); + expect(result.value.podA.content.asEntries().num.value).toBe(50n); + expect(result.value.podB.content.asEntries().foo.value).toBe("foo"); + expect(result.value.podB.content.asEntries().other_id.value).toBe(1000n); + }); + + it("should validate related pods in a group", () => { + const specWithRelations = groupSpecBuilder.equalsEntry( + "podA.id", + "podB.other_id" + ); + + const validator = groupValidator(specWithRelations.spec()); + const result = validator.validate({ + podA, + podB, + }); + + // The statement is true because the values are equal + assert(podA.content.getValue("id")?.value === 1000n); + assert(podB.content.getValue("other_id")?.value === 1000n); + assert(result.isValid); + + const validator2 = groupValidator( + // This statement should produce a negative result + groupSpecBuilder + .equalsEntry("podA.$contentID", "podB.$contentID", "shouldFail") + .spec() + ); + const result2 = validator2.validate({ + podA, + podB, + }); + + assert(!result2.isValid); + assert(result2.issues.length === 1); + const issue = result2.issues[0]; + assert(issue); + assert(issue.code === IssueCode.statement_negative_result); + assert(issue.statementType === "equalsEntry"); + assert(issue.statementName === "shouldFail"); + assert(issue.entries.length === 2); + assert(issue.entries[0] === "podA.$contentID"); + assert(issue.entries[1] === "podB.$contentID"); + }); +}); diff --git a/packages/podspec/test/processors/validator/podValidator.spec.ts b/packages/podspec/test/processors/validator/podValidator.spec.ts new file mode 100644 index 0000000..11157da --- /dev/null +++ b/packages/podspec/test/processors/validator/podValidator.spec.ts @@ -0,0 +1,74 @@ +import { + POD, + type PODEntries, + type PODIntValue, + type PODStringValue, + type PODValue, +} from "@pcd/pod"; +import { assert, describe, expect, it } from "vitest"; +import { PODSpecBuilder } from "../../../src/index.js"; +import { podValidator } from "../../../src/processors/validate/podValidator.js"; +import { generateKeyPair } from "../../utils.js"; + +describe("validator", () => { + it("should be a test", () => { + expect(true).toBe(true); + }); + + const { privateKey } = generateKeyPair(); + + const signPOD = (entries: PODEntries) => POD.sign(entries, privateKey); + + it("validatePOD", () => { + const myPOD = signPOD({ + foo: { type: "string", value: "foo" }, + num: { type: "int", value: 50n }, + }); + const myPodSpecBuilder = PODSpecBuilder.create() + .entry("foo", "string") + .isMemberOf(["foo"], ["foo", "bar"]); + + // This should pass because the entry "foo" is in the list ["foo", "bar"] + expect(podValidator(myPodSpecBuilder.spec()).check(myPOD)).toBe(true); + + const result = podValidator(myPodSpecBuilder.spec()).validate(myPOD); + if (result.isValid) { + const pod = result.value; + // After validation, the entries are strongly typed + pod.content.asEntries().bar?.value satisfies + | PODValue["value"] + | undefined; + pod.content.asEntries().foo.value satisfies string; + pod.content.getValue("bar")?.value satisfies + | PODValue["value"] + | undefined; + pod.content.getRawValue("bar") satisfies PODValue["value"] | undefined; + pod.content.getValue("foo") satisfies PODStringValue; + pod.content.getRawValue("foo") satisfies string; + } + + // This should fail because the entry "foo" is not in the list ["baz", "quux"] + const secondBuilder = myPodSpecBuilder.isMemberOf(["foo"], ["baz", "quux"]); + expect(podValidator(secondBuilder.spec()).check(myPOD)).toBe(false); + + // If we omit the new statement, it should pass + expect( + podValidator( + secondBuilder.omitStatements(["foo_isMemberOf_1"]).spec() + ).check(myPOD) + ).toBe(true); + + { + const result = podValidator( + secondBuilder + .omitStatements(["foo_isMemberOf_1"]) + .entry("num", "int") + .inRange("num", { min: 0n, max: 100n }) + .spec() + ).validate(myPOD); + assert(result.isValid); + const pod = result.value; + pod.content.asEntries().num satisfies PODIntValue; + } + }); +}); diff --git a/packages/podspec/test/serialization.spec.ts b/packages/podspec/test/serialization.spec.ts deleted file mode 100644 index fb8c0dd..0000000 --- a/packages/podspec/test/serialization.spec.ts +++ /dev/null @@ -1,83 +0,0 @@ -import { - type GPCBoundConfig, - boundConfigToJSON, - gpcProve, - podMembershipListsToJSON, - proofConfigToJSON, - revealedClaimsToJSON -} from "@pcd/gpc"; -import { POD } from "@pcd/pod"; -import { describe, expect, it } from "vitest"; -import * as p from "../src/index.js"; -import { GPC_NPM_ARTIFACTS_PATH } from "./constants.js"; -import { generateKeyPair } from "./utils.js"; - -describe("should be able to serialize outputs", function () { - it("should serialize proof request outputs", async function () { - const { publicKey, privateKey } = generateKeyPair(); - - const pod1 = p.pod({ - entries: { - foo: { type: "string" }, - bar: { - type: "int", - inRange: { min: 0n, max: 10n } - } - }, - signerPublicKey: { - isMemberOf: [publicKey] - }, - tuples: [ - { - entries: ["$signerPublicKey", "foo", "bar"], - isMemberOf: [ - [ - { type: "eddsa_pubkey", value: publicKey }, - { type: "string", value: "test" }, - { type: "int", value: 5n } - ] - ] - } - ] - }); - const prs = p.proofRequest({ - pods: { - pod1: pod1.proofConfig({ revealed: { foo: true, bar: true } }) - }, - watermark: { type: "string", value: "1" }, - externalNullifier: { type: "string", value: "1" } - }); - - const pr = prs.getProofRequest(); - console.dir(pr, { depth: null }); - - expect(() => proofConfigToJSON(pr.proofConfig)).to.not.throw; - expect(() => podMembershipListsToJSON(pr.membershipLists)).to.not.throw; - - const pod = POD.sign( - { - foo: { type: "string", value: "test" }, - bar: { type: "int", value: 5n } - }, - privateKey - ); - - const proof = await gpcProve( - pr.proofConfig, - { - membershipLists: pr.membershipLists, - pods: { pod1: pod } - }, - GPC_NPM_ARTIFACTS_PATH - ); - - pr.proofConfig.circuitIdentifier = proof.boundConfig.circuitIdentifier; - const boundConfig: GPCBoundConfig = { - ...pr.proofConfig, - circuitIdentifier: proof.boundConfig.circuitIdentifier - }; - - expect(() => boundConfigToJSON(boundConfig)).to.not.throw; - expect(() => revealedClaimsToJSON(proof.revealedClaims)).to.not.throw; - }); -}); diff --git a/packages/podspec/test/ticket-example.spec.ts b/packages/podspec/test/ticket-example.spec.ts deleted file mode 100644 index 51bee30..0000000 --- a/packages/podspec/test/ticket-example.spec.ts +++ /dev/null @@ -1,326 +0,0 @@ -import { gpcProve } from "@pcd/gpc"; -import { POD } from "@pcd/pod"; -import { Identity } from "@semaphore-protocol/identity"; -import { v4 as uuidv4 } from "uuid"; -import { assert, describe, expect, it } from "vitest"; -import * as p from "../src/index.js"; -import { $s } from "../src/pod_value_utils.js"; -import { GPC_NPM_ARTIFACTS_PATH } from "./constants.js"; -import { generateKeyPair } from "./utils.js"; - -/** - * A specification for tickets, based on the existing example in the Zupass codebase. - */ -const TicketEntries = p.entries({ - ticketId: { type: "string" }, - eventId: { type: "string" }, - productId: { type: "string" }, - ticketName: { type: "string" }, - eventName: { type: "string" }, - checkerEmail: { type: "optional", innerType: { type: "string" } }, - imageUrl: { type: "optional", innerType: { type: "string" } }, - imageAltText: { type: "optional", innerType: { type: "string" } }, - ticketSecret: { type: "optional", innerType: { type: "string" } }, - timestampConsumed: { type: "int" }, - timestampSigned: { type: "int" }, - attendeeSemaphoreId: { type: "cryptographic" }, - isConsumed: { type: "int" }, - isRevoked: { type: "int" }, - ticketCategory: { type: "string" }, - attendeeName: { type: "string" }, - attendeeEmail: { type: "string" } -}); - -// An event ID for an event -const MY_EVENT_ID = uuidv4(); -// Possible product IDs for the event -const MY_EVENT_PRODUCT_IDS = { - VISITOR: uuidv4(), - RESIDENT: uuidv4(), - ORGANIZER: uuidv4() -}; - -// Example data for a ticket -const VALID_TICKET_DATA = { - ticketId: uuidv4(), - eventId: MY_EVENT_ID, - productId: MY_EVENT_PRODUCT_IDS.VISITOR, - ticketName: "Ticket 1", - eventName: "Event 1", - checkerEmail: "checker@example.com", - imageUrl: "https://example.com/image.jpg", - imageAltText: "Image 1", - ticketSecret: "secret123", - timestampConsumed: 1714857600, - timestampSigned: 1714857600, - attendeeSemaphoreId: 1234567890, - isConsumed: 0, - isRevoked: 0, - ticketCategory: "Category 1", - attendeeName: "John Doe", - attendeeEmail: "john.doe@example.com" -}; - -describe.concurrent("podspec ticket example", function () { - // Given a ticket with valid data, it should be recognized as valid - it("should validate ticket entries", async function () { - const result = TicketEntries.safeParse(VALID_TICKET_DATA, { coerce: true }); - expect(result.isValid).toBe(true); - }); - - // Validation of a POD should work - it("should validate ticket PODs", async function () { - const { privateKey } = generateKeyPair(); - const entries = TicketEntries.parse(VALID_TICKET_DATA, { coerce: true }); - const pod = POD.sign(entries, privateKey); - - const podSpec = p.pod({ entries: TicketEntries.schema }); - - const result = podSpec.safeParse(pod); - expect(result.isValid).toBe(true); - assert(result.isValid); - }); - - // Given that we have a schema for tickets, we should be able to create - // narrower schemas for specific events or products, re-using our existing - // specification. - it("should support narrowing of ticket criteria", async function () { - const baseTicketSchema = TicketEntries.schema; - - // Override the event ID to be specific to our event - const EventSpecificTicketEntries = p.entries({ - ...baseTicketSchema, - eventId: { ...baseTicketSchema.eventId, isMemberOf: $s([MY_EVENT_ID]) } - }); - - // This will succeed because the ticket is for the specified event - const result = EventSpecificTicketEntries.safeParse(VALID_TICKET_DATA, { - coerce: true - }); - - expect(result.isValid).toBe(true); - }); - - it("should reject tickets which do not meet criteria", async function () { - const baseTicketSchema = TicketEntries.schema; - - // Now let's create a specification for a ticket which only matches tickets - // with two specific product IDs - const EventAndProductSpecificTicketEntries = p.entries({ - ...baseTicketSchema, - eventId: { ...baseTicketSchema.eventId, isMemberOf: $s([MY_EVENT_ID]) }, - productId: { - ...baseTicketSchema.productId, - isMemberOf: $s([ - // Ticket is of type "VISITOR", so neither of these match - MY_EVENT_PRODUCT_IDS.RESIDENT, - MY_EVENT_PRODUCT_IDS.ORGANIZER - ]) - } - }); - - // This will fail because the ticket is of type "VISITOR", which is not in - // the list of allowed product IDs - const result = EventAndProductSpecificTicketEntries.safeParse( - VALID_TICKET_DATA, - { - coerce: true - } - ); - - assert(result.isValid === false); - expect(result.issues).toHaveLength(1); - expect(result.issues[0]?.code).toBe("not_in_list"); - expect(result.issues[0]?.path).toEqual(["productId"]); - }); - - // Given a collection of tickets, we should be able to query for tickets which - // match a given criteria - it("should be able to find matching tickets from a collection", async function () { - const { privateKey } = generateKeyPair(); - const entries = TicketEntries.parse(VALID_TICKET_DATA, { coerce: true }); - const visitorPod = POD.sign(entries, privateKey); - const organizerPod = POD.sign( - { ...entries, productId: $s(MY_EVENT_PRODUCT_IDS.ORGANIZER) }, - privateKey - ); - const residentPod = POD.sign( - { ...entries, productId: $s(MY_EVENT_PRODUCT_IDS.RESIDENT) }, - privateKey - ); - const differentEventPod = POD.sign( - { ...entries, eventId: $s(uuidv4()) }, - privateKey - ); - const otherPod = POD.sign({ test: $s("I'm not a ticket") }, privateKey); - const pods = [ - visitorPod, - organizerPod, - residentPod, - differentEventPod, - otherPod - ]; - - // Create a podspec which matches all tickets - const allTicketsPodSpec = p.pod({ entries: TicketEntries.schema }); - { - const { matches, matchingIndexes } = allTicketsPodSpec.query(pods); - expect(matches).toHaveLength(4); - // We match the tickets, but not `otherPod` - expect(matchingIndexes).to.eql([0, 1, 2, 3]); - } - - // Create a new podspec which only matches tickets for our event - const eventTicketsPodSpec = allTicketsPodSpec.extend((s, f) => - f({ - ...s, - entries: { - ...s.entries, - eventId: { ...s.entries.eventId, isMemberOf: $s([MY_EVENT_ID]) } - } - }) - ); - - { - const { matches, matchingIndexes } = eventTicketsPodSpec.query(pods); - expect(matches).toHaveLength(3); - // We match the tickets for our event, but not the other tickets or `otherPod` - expect(matchingIndexes).to.eql([0, 1, 2]); - } - - // Extend our event-specific ticket spec to only match tickets of type "VISITOR" - const visitorTicketsPodSpec = eventTicketsPodSpec.extend((s, f) => - f({ - ...s, - entries: { - ...s.entries, - productId: { - ...s.entries.productId, - isMemberOf: $s([MY_EVENT_PRODUCT_IDS.VISITOR]) - } - } - }) - ); - - { - const { matches, matchingIndexes } = visitorTicketsPodSpec.query(pods); - expect(matches).toHaveLength(1); - // We only match the ticket of type "VISITOR" - expect(matchingIndexes).to.eql([0]); - } - - const organizerTicketsPodSpec = eventTicketsPodSpec.extend((s, f) => - f({ - ...s, - entries: { - ...s.entries, - productId: { - ...s.entries.productId, - isMemberOf: $s([MY_EVENT_PRODUCT_IDS.ORGANIZER]) - } - } - }) - ); - - { - const { matches, matchingIndexes } = organizerTicketsPodSpec.query(pods); - expect(matches).toHaveLength(1); - // We only match the ticket of type "ORGANIZER" - expect(matchingIndexes).to.eql([1]); - } - }); - - it("should be able to generate a proof request for a podspec", async function () { - // Create a podspec which matches all tickets - const allTicketsPodSpec = p.pod({ entries: TicketEntries.schema }); - - // Create a new podspec which only matches tickets for our event - const eventTicketsPodSpec = allTicketsPodSpec.extend((s, f) => - f({ - ...s, - entries: { - ...s.entries, - eventId: { ...s.entries.eventId, isMemberOf: $s([MY_EVENT_ID]) } - } - }) - ); - - const identity = new Identity(); - - // Turn our JavaScript data into PODEntries - const ticketEntries = TicketEntries.parse( - { - ...VALID_TICKET_DATA, - attendeeSemaphoreId: identity.commitment - }, - { coerce: true } - ); - - // Create the POD - const ticketPod = POD.sign(ticketEntries, generateKeyPair().privateKey); - - const proofRequestSpec = p.proofRequest({ - pods: { - // Create proof config for the ticket POD, specific to our event - ticketPod: eventTicketsPodSpec.proofConfig({ - revealed: { ticketId: true }, - owner: { entry: "attendeeSemaphoreId", protocol: "SemaphoreV3" } - }) - } - }); - - // Get a proof request - const req = proofRequestSpec.getProofRequest(); - - // There's a membership list check on event ID, so the proof request should - // have a membership list for it - expect(req.membershipLists.allowlist_ticketPod_eventId).toHaveLength(1); - // The membership list should contain the event ID - expect(req.membershipLists.allowlist_ticketPod_eventId?.[0]).toEqual( - $s(MY_EVENT_ID) - ); - // The ticket ID is revealed - expect(req.proofConfig.pods.ticketPod?.entries.ticketId?.isRevealed).toBe( - true - ); - // The event ID is not revealed - expect(req.proofConfig.pods.ticketPod?.entries.eventId?.isRevealed).toBe( - false - ); - // The event ID does have a membership list - expect(req.proofConfig.pods.ticketPod?.entries.eventId?.isMemberOf).toBe( - "allowlist_ticketPod_eventId" - ); - // The product ID is not defined, because it has no GPC checks and is not - // revealed - expect(req.proofConfig.pods.ticketPod?.entries.productId).toBeUndefined(); - // There are three configured entries, event ID, ticket ID, and the - // attendee's Semaphore ID (which is the owner) - expect( - Object.keys(req.proofConfig.pods.ticketPod?.entries ?? {}) - ).toHaveLength(3); - - const result = await gpcProve( - req.proofConfig, - { - pods: { - ticketPod - }, - membershipLists: req.membershipLists, - watermark: req.watermark, - owner: { - semaphoreV3: identity - } - }, - GPC_NPM_ARTIFACTS_PATH - ); - - expect(result).toBeDefined(); - expect(result.proof).toBeDefined(); - expect(result.boundConfig).toBeDefined(); - expect(result.revealedClaims).toBeDefined(); - expect( - result.revealedClaims.pods.ticketPod?.entries?.ticketId - ).toBeDefined(); - }); -}); diff --git a/packages/podspec/tsconfig.json b/packages/podspec/tsconfig.json index bb19bc3..d3222a6 100644 --- a/packages/podspec/tsconfig.json +++ b/packages/podspec/tsconfig.json @@ -1,7 +1,8 @@ { "extends": "@parcnet-js/typescript-config/base.json", "compilerOptions": { - "outDir": "dist" + "outDir": "dist", + "types": ["vitest/importMeta"] }, "include": ["src", "test"], "exclude": ["node_modules", "dist"] diff --git a/packages/podspec/tsup.config.ts b/packages/podspec/tsup.config.ts new file mode 100644 index 0000000..dc8dc4e --- /dev/null +++ b/packages/podspec/tsup.config.ts @@ -0,0 +1,13 @@ +import { defineConfig } from "tsup"; + +export default defineConfig({ + entry: ["src/**/*.ts", "src/**/*.tsx"], + format: ["cjs", "esm"], + clean: true, + sourcemap: true, + splitting: false, + minify: true, + define: { + "import.meta.vitest": "undefined", + }, +}); diff --git a/packages/podspec/vitest.config.ts b/packages/podspec/vitest.config.ts new file mode 100644 index 0000000..47de263 --- /dev/null +++ b/packages/podspec/vitest.config.ts @@ -0,0 +1,7 @@ +import { defineConfig } from "vitest/config"; + +export default defineConfig({ + test: { + includeSource: ["src/**/*.{js,ts}"], + }, +}); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index a081a93..318cc37 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -23,16 +23,16 @@ importers: version: 9.1.6 knip: specifier: ^5.29.2 - version: 5.29.2(@types/node@22.5.5)(typescript@5.5.4) + version: 5.29.2(@types/node@22.5.5)(typescript@5.7.2) turbo: specifier: ^2.2.1 version: 2.2.1 typescript: - specifier: ^5.4.5 - version: 5.5.4 + specifier: ^5.7 + version: 5.7.2 vitest: - specifier: ^2.0.5 - version: 2.0.5(@types/node@22.5.5)(sass@1.80.3)(terser@5.34.1) + specifier: ^3.0.0 + version: 3.0.4(@types/debug@4.1.12)(@types/node@22.5.5)(sass@1.80.3)(terser@5.34.1) apps/client-web: dependencies: @@ -429,7 +429,19 @@ importers: '@pcd/pod': specifier: ^0.5.0 version: 0.5.0 + base64-js: + specifier: ^1.5.1 + version: 1.5.1 + canonicalize: + specifier: ^2.0.0 + version: 2.0.0 + typia: + specifier: ^7.6.0 + version: 7.6.0(@samchon/openapi@2.4.1)(typescript@5.5.4) devDependencies: + '@fast-check/vitest': + specifier: ^0.1.5 + version: 0.1.5(vitest@3.0.5(@types/debug@4.1.12)(@types/node@22.5.5)(sass@1.80.3)(terser@5.34.1)) '@parcnet-js/eslint-config': specifier: workspace:* version: link:../eslint-config @@ -458,8 +470,8 @@ importers: specifier: ^9.0.0 version: 9.0.1 vitest: - specifier: ^2.1.1 - version: 2.1.1(@types/node@22.5.5)(@vitest/ui@2.1.1)(sass@1.80.3)(terser@5.34.1) + specifier: ^3.0.5 + version: 3.0.5(@types/debug@4.1.12)(@types/node@22.5.5)(sass@1.80.3)(terser@5.34.1) packages/ticket-spec: dependencies: @@ -1178,6 +1190,11 @@ packages: '@expressive-code/plugin-text-markers@0.35.6': resolution: {integrity: sha512-/k9eWVZSCs+uEKHR++22Uu6eIbHWEciVHbIuD8frT8DlqTtHYaaiwHPncO6KFWnGDz5i/gL7oyl6XmOi/E6GVg==} + '@fast-check/vitest@0.1.5': + resolution: {integrity: sha512-TxtdmzHFzHu1zLDc08SjJHH3jxxtf8oO/fZagsH3MXwkLu2bl0CSo6DWq03kNOxzd5uGT+jSUvZqFajy08+9sg==} + peerDependencies: + vitest: '>=0.28.1 <1.0.0 || ^1 || ^2 || ^3' + '@humanwhocodes/module-importer@1.0.1': resolution: {integrity: sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==} engines: {node: '>=12.22'} @@ -1761,6 +1778,9 @@ packages: '@rtsao/scc@1.1.0': resolution: {integrity: sha512-zt6OdqaDoOnJ1ZYsCYGt9YmWzDXl4vQdKTyJev62gFhRGKdx7mcT54V9KIjg+d2wi9EXsPvAPKe7i7WjfVWB8g==} + '@samchon/openapi@2.4.1': + resolution: {integrity: sha512-xynHEJJGAzmHDhc46NzROOs1qCVu0i02tqe5JxMRsiiq1fjeFnOltZomrHfKYoX6HPsfPtEOuWmbZAdprc5A9g==} + '@semaphore-protocol/core@4.0.3': resolution: {integrity: sha512-XwFSV8B8JYOS7nXDPNSQ8Uy/Yw2MPyAhcMLToAJsu6HG8rwIG5I6F6/MKgDm4IhUJnE1h22fipoteekjBMVqSg==} @@ -2010,6 +2030,12 @@ packages: '@vitest/expect@2.1.2': resolution: {integrity: sha512-FEgtlN8mIUSEAAnlvn7mP8vzaWhEaAEvhSXCqrsijM7K6QqjB11qoRZYEd4AKSCDz8p0/+yH5LzhZ47qt+EyPg==} + '@vitest/expect@3.0.4': + resolution: {integrity: sha512-Nm5kJmYw6P2BxhJPkO3eKKhGYKRsnqJqf+r0yOGRKpEP+bSCBDsjXgiu1/5QFrnPMEgzfC38ZEjvCFgaNBC0Eg==} + + '@vitest/expect@3.0.5': + resolution: {integrity: sha512-nNIOqupgZ4v5jWuQx2DSlHLEs7Q4Oh/7AYwNyE+k0UQzG7tSmjPXShUikn1mpNGzYEN2jJbTvLejwShMitovBA==} + '@vitest/mocker@2.1.1': resolution: {integrity: sha512-LNN5VwOEdJqCmJ/2XJBywB11DLlkbY0ooDJW3uRX5cZyYCrc4PI/ePX0iQhE3BiEGiQmK4GE7Q/PqCkkaiPnrA==} peerDependencies: @@ -2034,6 +2060,28 @@ packages: vite: optional: true + '@vitest/mocker@3.0.4': + resolution: {integrity: sha512-gEef35vKafJlfQbnyOXZ0Gcr9IBUsMTyTLXsEQwuyYAerpHqvXhzdBnDFuHLpFqth3F7b6BaFr4qV/Cs1ULx5A==} + peerDependencies: + msw: ^2.4.9 + vite: ^5.0.0 || ^6.0.0 + peerDependenciesMeta: + msw: + optional: true + vite: + optional: true + + '@vitest/mocker@3.0.5': + resolution: {integrity: sha512-CLPNBFBIE7x6aEGbIjaQAX03ZZlBMaWwAjBdMkIf/cAn6xzLTiM3zYqO/WAbieEjsAZir6tO71mzeHZoodThvw==} + peerDependencies: + msw: ^2.4.9 + vite: ^5.0.0 || ^6.0.0 + peerDependenciesMeta: + msw: + optional: true + vite: + optional: true + '@vitest/pretty-format@2.0.5': resolution: {integrity: sha512-h8k+1oWHfwTkyTkb9egzwNMfJAEx4veaPSnMeKbVSjp4euqGSbQlm5+6VHwTr7u4FJslVVsUG5nopCaAYdOmSQ==} @@ -2043,6 +2091,12 @@ packages: '@vitest/pretty-format@2.1.2': resolution: {integrity: sha512-FIoglbHrSUlOJPDGIrh2bjX1sNars5HbxlcsFKCtKzu4+5lpsRhOCVcuzp0fEhAGHkPZRIXVNzPcpSlkoZ3LuA==} + '@vitest/pretty-format@3.0.4': + resolution: {integrity: sha512-ts0fba+dEhK2aC9PFuZ9LTpULHpY/nd6jhAQ5IMU7Gaj7crPCTdCFfgvXxruRBLFS+MLraicCuFXxISEq8C93g==} + + '@vitest/pretty-format@3.0.5': + resolution: {integrity: sha512-CjUtdmpOcm4RVtB+up8r2vVDLR16Mgm/bYdkGFe3Yj/scRfCpbSi2W/BDSDcFK7ohw8UXvjMbOp9H4fByd/cOA==} + '@vitest/runner@2.0.5': resolution: {integrity: sha512-TfRfZa6Bkk9ky4tW0z20WKXFEwwvWhRY+84CnSEtq4+3ZvDlJyY32oNTJtM7AW9ihW90tX/1Q78cb6FjoAs+ig==} @@ -2052,6 +2106,12 @@ packages: '@vitest/runner@2.1.2': resolution: {integrity: sha512-UCsPtvluHO3u7jdoONGjOSil+uON5SSvU9buQh3lP7GgUXHp78guN1wRmZDX4wGK6J10f9NUtP6pO+SFquoMlw==} + '@vitest/runner@3.0.4': + resolution: {integrity: sha512-dKHzTQ7n9sExAcWH/0sh1elVgwc7OJ2lMOBrAm73J7AH6Pf9T12Zh3lNE1TETZaqrWFXtLlx3NVrLRb5hCK+iw==} + + '@vitest/runner@3.0.5': + resolution: {integrity: sha512-BAiZFityFexZQi2yN4OX3OkJC6scwRo8EhRB0Z5HIGGgd2q+Nq29LgHU/+ovCtd0fOfXj5ZI6pwdlUmC5bpi8A==} + '@vitest/snapshot@2.0.5': resolution: {integrity: sha512-SgCPUeDFLaM0mIUHfaArq8fD2WbaXG/zVXjRupthYfYGzc8ztbFbu6dUNOblBG7XLMR1kEhS/DNnfCZ2IhdDew==} @@ -2061,6 +2121,12 @@ packages: '@vitest/snapshot@2.1.2': resolution: {integrity: sha512-xtAeNsZ++aRIYIUsek7VHzry/9AcxeULlegBvsdLncLmNCR6tR8SRjn8BbDP4naxtccvzTqZ+L1ltZlRCfBZFA==} + '@vitest/snapshot@3.0.4': + resolution: {integrity: sha512-+p5knMLwIk7lTQkM3NonZ9zBewzVp9EVkVpvNta0/PlFWpiqLaRcF4+33L1it3uRUCh0BGLOaXPPGEjNKfWb4w==} + + '@vitest/snapshot@3.0.5': + resolution: {integrity: sha512-GJPZYcd7v8QNUJ7vRvLDmRwl+a1fGg4T/54lZXe+UOGy47F9yUfE18hRCtXL5aHN/AONu29NGzIXSVFh9K0feA==} + '@vitest/spy@2.0.5': resolution: {integrity: sha512-c/jdthAhvJdpfVuaexSrnawxZz6pywlTPe84LUB2m/4t3rl2fTo9NFGBG4oWgaD+FTgDDV8hJ/nibT7IfH3JfA==} @@ -2070,6 +2136,12 @@ packages: '@vitest/spy@2.1.2': resolution: {integrity: sha512-GSUi5zoy+abNRJwmFhBDC0yRuVUn8WMlQscvnbbXdKLXX9dE59YbfwXxuJ/mth6eeqIzofU8BB5XDo/Ns/qK2A==} + '@vitest/spy@3.0.4': + resolution: {integrity: sha512-sXIMF0oauYyUy2hN49VFTYodzEAu744MmGcPR3ZBsPM20G+1/cSW/n1U+3Yu/zHxX2bIDe1oJASOkml+osTU6Q==} + + '@vitest/spy@3.0.5': + resolution: {integrity: sha512-5fOzHj0WbUNqPK6blI/8VzZdkBlQLnT25knX0r4dbZI9qoZDf3qAdjoMmDcLG5A83W6oUUFJgUd0EYBc2P5xqg==} + '@vitest/ui@2.1.1': resolution: {integrity: sha512-IIxo2LkQDA+1TZdPLYPclzsXukBWd5dX2CKpGqH8CCt8Wh0ZuDn4+vuQ9qlppEju6/igDGzjWF/zyorfsf+nHg==} peerDependencies: @@ -2084,6 +2156,12 @@ packages: '@vitest/utils@2.1.2': resolution: {integrity: sha512-zMO2KdYy6mx56btx9JvAqAZ6EyS3g49krMPPrgOp1yxGZiA93HumGk+bZ5jIZtOg5/VBYl5eBmGRQHqq4FG6uQ==} + '@vitest/utils@3.0.4': + resolution: {integrity: sha512-8BqC1ksYsHtbWH+DfpOAKrFw3jl3Uf9J7yeFh85Pz52IWuh1hBBtyfEbRNNZNjl8H8A5yMLH9/t+k7HIKzQcZQ==} + + '@vitest/utils@3.0.5': + resolution: {integrity: sha512-N9AX0NUoUtVwKwy21JtwzaqR5L5R5A99GAbrHfCCXK1lp593i/3AZAXhSP43wRQuxYsflrdzEfXZFo1reR1Nkg==} + '@volar/kit@2.4.5': resolution: {integrity: sha512-ZzyErW5UiDfiIuJ/lpqc2Kx5PHDGDZ/bPlPJYpRcxlrn8Z8aDhRlsLHkNKcNiH65TmNahk2kbLaiejiqu6BD3A==} peerDependencies: @@ -2182,6 +2260,10 @@ packages: resolution: {integrity: sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw==} engines: {node: '>=6'} + ansi-escapes@4.3.2: + resolution: {integrity: sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==} + engines: {node: '>=8'} + ansi-regex@5.0.1: resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==} engines: {node: '>=8'} @@ -2240,6 +2322,9 @@ packages: array-iterate@2.0.1: resolution: {integrity: sha512-I1jXZMjAgCMmxT4qxXfPXa6SthSoE8h6gkSI9BGGNv8mP8G/v0blc+qFnZu6K42vTOiuME596QaLO0TP3Lk0xg==} + array-timsort@1.0.3: + resolution: {integrity: sha512-/+3GRL7dDAGEfM6TseQk/U+mi18TU2Ms9I3UlLdUMhz2hbvGNTKdj9xniwXfUqgYhHxRx0+8UnKkvlNwVU+cWQ==} + array-union@2.1.0: resolution: {integrity: sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==} engines: {node: '>=8'} @@ -2459,6 +2544,9 @@ packages: caniuse-lite@1.0.30001655: resolution: {integrity: sha512-jRGVy3iSGO5Uutn2owlb5gR6qsGngTw9ZTb4ali9f3glshcNmJ2noam4Mo9zia5P9Dk3jNNydy7vQjuE5dQmfg==} + canonicalize@2.0.0: + resolution: {integrity: sha512-ulDEYPv7asdKvqahuAY35c1selLdzDwHqugK92hfkzvlDCwXRRelDkR+Er33md/PtnpqHemgkuDPanZ4fiYZ8w==} + ccount@2.0.1: resolution: {integrity: sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg==} @@ -2466,6 +2554,10 @@ packages: resolution: {integrity: sha512-pT1ZgP8rPNqUgieVaEY+ryQr6Q4HXNg8Ei9UnLUrjN4IA7dvQC5JB+/kxVcPNDHyBcc/26CXPkbNzq3qwrOEKA==} engines: {node: '>=12'} + chai@5.1.2: + resolution: {integrity: sha512-aGtmf24DW6MLHHG5gCx4zaI3uBq3KRtxeVs0DjFH6Z0rDNbsvTxFASFvdj79pxjxZ8/5u3PIiN3IwEIQkiiuPw==} + engines: {node: '>=12'} + chalk@2.4.2: resolution: {integrity: sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==} engines: {node: '>=4'} @@ -2537,6 +2629,10 @@ packages: resolution: {integrity: sha512-/lzGpEWL/8PfI0BmBOPRwp0c/wFNX1RdUML3jK/RcSBA9T8mZDdQpqYBKtCFTOfQbwPqWEOpjqW+Fnayc0969g==} engines: {node: '>=10'} + cli-cursor@3.1.0: + resolution: {integrity: sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw==} + engines: {node: '>=8'} + cli-cursor@5.0.0: resolution: {integrity: sha512-aCj4O5wKyszjMmDT4tZj93kxyydN/K5zPWSCe6/0AV/AA1pqe5ZBIw0a2ZfPQV7lL5/yb5HsUreJ6UFAF1tEQw==} engines: {node: '>=18'} @@ -2545,6 +2641,10 @@ packages: resolution: {integrity: sha512-ywqV+5MmyL4E7ybXgKys4DugZbX0FC6LnwrhjuykIjnK9k8OQacQ7axGKnjDXWNhns0xot3bZI5h55H8yo9cJg==} engines: {node: '>=6'} + cli-width@3.0.0: + resolution: {integrity: sha512-FxqpkPPwu1HjuN93Omfm4h8uIanXofW0RxVEW3k5RKx+mJJYSthzNhp32Kzxxy3YAEZ/Dc/EWN1vZRY0+kOhbw==} + engines: {node: '>= 10'} + cliui@8.0.1: resolution: {integrity: sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==} engines: {node: '>=12'} @@ -2587,6 +2687,10 @@ packages: comma-separated-tokens@2.0.3: resolution: {integrity: sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg==} + commander@10.0.1: + resolution: {integrity: sha512-y4Mg2tXshplEbSGzx7amzPwKKOCGuoSRP/CjEdwwk0FOGlUbq6lKuoyDZTNZkmxHdJtp54hdfY/JUrdL7Xfdug==} + engines: {node: '>=14'} + commander@2.20.3: resolution: {integrity: sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==} @@ -2594,6 +2698,10 @@ packages: resolution: {integrity: sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==} engines: {node: '>= 6'} + comment-json@4.2.5: + resolution: {integrity: sha512-bKw/r35jR3HGt5PEPm1ljsQQGyCrR8sFGNiN5L+ykDHdpO8Smxkrkla9Yi6NkQyUrb8V54PGhfMs6NrIwtxtdw==} + engines: {node: '>= 6'} + common-ancestor-path@1.0.1: resolution: {integrity: sha512-L3sHRo1pXXEqX8VU28kfgUY+YGsk09hPqZiZmLacNib6XNTCM8ubYeT7ryXQw8asB1sKgcU5lkB7ONug08aB8w==} @@ -2702,6 +2810,15 @@ packages: supports-color: optional: true + debug@4.4.0: + resolution: {integrity: sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==} + engines: {node: '>=6.0'} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + decode-named-character-reference@1.0.2: resolution: {integrity: sha512-O8x12RzrUF8xyVcY0KJowWsmaJxQbmy0/EtnNtHRpsOcT7dFk5W598coHqBVpmWo1oQQfsCqfCmkZN5DJrZVdg==} @@ -2801,6 +2918,10 @@ packages: resolution: {integrity: sha512-7GO6HghkA5fYG9TYnNxi14/7K9f5occMlp3zXAuSxn7CKCxt9xbNWG7yF8hTCSUchlfWSe3uLmlPfigevRItzQ==} engines: {node: '>=12'} + drange@1.1.1: + resolution: {integrity: sha512-pYxfDYpued//QpnLIm4Avk7rsNtAtQkUES2cwAYSvD/wd2pKD71gN2Ebj3e7klzXwjocvE8c5vx/1fxwpqmSxA==} + engines: {node: '>=4'} + dset@3.1.4: resolution: {integrity: sha512-2QF/g9/zTaPDc3BjNcVTGoBbXBgYfMTTceLaYcFJ/W9kggFUkhxD/hMEeuLKbugyef9SqAx8cpgwlIP/jinUTA==} engines: {node: '>=4'} @@ -2868,6 +2989,9 @@ packages: es-module-lexer@1.5.4: resolution: {integrity: sha512-MVNK56NiMrOwitFB7cqDwq0CQutbw+0BvLshJSse0MUNU+y1FC3bUS/AQg7oUng+/wKrrki7JfmwtVHkVfPLlw==} + es-module-lexer@1.6.0: + resolution: {integrity: sha512-qqnD1yMU6tk/jnaMosogGySTZP8YtUgAffA9nMN+E/rjxcfRQ6IEk7IiozUjgxKoFHBGjTLnrHB/YC45r/59EQ==} + es-object-atoms@1.0.0: resolution: {integrity: sha512-MZ4iQ6JwHOBQjahnjwaC1ZtIBH+2ohjamzAO3oaHcXYup7qxjF2fixyH+Q71voWHeOkI2q/TnJao/KfXYIZWbw==} engines: {node: '>= 0.4'} @@ -3085,6 +3209,10 @@ packages: resolution: {integrity: sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==} engines: {node: '>=6'} + expect-type@1.1.0: + resolution: {integrity: sha512-bFi65yM+xZgk+u/KRIpekdSYkTB5W1pEf0Lt8Q8Msh7b+eQ7LXVtIB1Bkm4fvclDEL1b2CZkMhv2mOeF8tMdkA==} + engines: {node: '>=12.0.0'} + expressive-code@0.35.6: resolution: {integrity: sha512-+mx+TPTbMqgo0mL92Xh9QgjW0kSQIsEivMgEcOnaqKqL7qCw8Vkqc5Rg/di7ZYw4aMUSr74VTc+w8GQWu05j1g==} @@ -3102,6 +3230,10 @@ packages: resolution: {integrity: sha512-hMQ4CX1p1izmuLYyZqLMO/qGNw10wSv9QDCPfzXfyFrOaCSSoRfqE1Kf1s5an66J5JZC62NewG+mK49jOCtQew==} engines: {node: '>=4'} + fast-check@3.23.2: + resolution: {integrity: sha512-h5+1OzzfCC3Ef7VbtKdcv7zsstUQwUDlYpUTvjeUsJAssPgLn7QzbboPtL5ro04Mq0rPOsMzl7q5hIbRs2wD1A==} + engines: {node: '>=8.0.0'} + fast-deep-equal@3.1.3: resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} @@ -3144,6 +3276,10 @@ packages: fflate@0.8.2: resolution: {integrity: sha512-cPJU47OaAoCbg0pBvzsgpTPhmhqI5eJjh/JIu8tPj5q+T7iLvW/JAYUqmE7KOB4R1ZyEhzBaIQpQpardBF5z8A==} + figures@3.2.0: + resolution: {integrity: sha512-yaduQFRKLXYOGgEn6AZau90j3ggSOyiqXU0F9JZfeXYhNa+Jk4X+s45A2zg5jns87GAFa34BBm2kXw4XpNcbdg==} + engines: {node: '>=8'} + file-entry-cache@8.0.0: resolution: {integrity: sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==} engines: {node: '>=16.0.0'} @@ -3317,6 +3453,10 @@ packages: resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==} engines: {node: '>=8'} + has-own-prop@2.0.0: + resolution: {integrity: sha512-Pq0h+hvsVm6dDEa8x82GnLSYHOzNDt7f0ddFa3FqcQlgzEiptPqL+XrOJNavjOzSYiYWIrgeVYYgGlLmnxwilQ==} + engines: {node: '>=8'} + has-property-descriptors@1.0.2: resolution: {integrity: sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==} @@ -3486,6 +3626,10 @@ packages: inline-style-parser@0.2.4: resolution: {integrity: sha512-0aO8FkhNZlj/ZIbNi7Lxxr12obT7cL1moPfE4tg1LkX7LlLfC6DeX4l2ZEud1ukP9jNQyNnfzQVqwbwmAATY4Q==} + inquirer@8.2.6: + resolution: {integrity: sha512-M1WuAmb7pn9zdFRtQYk26ZBoY043Sse0wVDdk4Bppr+JOXyQYybdtvK+l9wUibhtjdjvtoiNy8tk+EgsYIUqKg==} + engines: {node: '>=12.0.0'} + internal-slot@1.0.7: resolution: {integrity: sha512-NGnrKwXzSms2qUUih/ILZ5JBqNTSa1+ZmP6flaIp6KmSElgE9qdndzS3cqjrDovwFdmwsGsLdeFgB6suw+1e9g==} engines: {node: '>= 0.4'} @@ -3573,6 +3717,10 @@ packages: engines: {node: '>=14.16'} hasBin: true + is-interactive@1.0.0: + resolution: {integrity: sha512-2HvIEKRoqS62guEC+qBjpvRubdX910WCMuJTZ+I9yvqKU2/12eSL549HMwtabb4oupdj2sMP50k+XJfB/8JE6w==} + engines: {node: '>=8'} + is-interactive@2.0.0: resolution: {integrity: sha512-qP1vozQRI+BMOPcjFzrjXuQvdak2pHNUMZoeG2eRbiSqyvbEf/wQtEOTOX1guk6E3t36RkaqiSt8A/6YElNxLQ==} engines: {node: '>=12'} @@ -3636,6 +3784,10 @@ packages: resolution: {integrity: sha512-uZ25/bUAlUY5fR4OKT4rZQEBrzQWYV9ZJYGGsUmEJ6thodVJ1HX64ePQ6Z0qPWP+m+Uq6e9UugrE38jeYsDSMw==} engines: {node: '>= 0.4'} + is-unicode-supported@0.1.0: + resolution: {integrity: sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==} + engines: {node: '>=10'} + is-unicode-supported@1.3.0: resolution: {integrity: sha512-43r2mRvz+8JRIKnWJ+3j8JtjRKZ6GmjzfaE/qiBJnikNnYv/6bagRJ1kUhNk8R5EX/GkobD+r+sfxCPJsiKBLQ==} engines: {node: '>=12'} @@ -3824,6 +3976,10 @@ packages: lodash@4.17.21: resolution: {integrity: sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==} + log-symbols@4.1.0: + resolution: {integrity: sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==} + engines: {node: '>=10'} + log-symbols@6.0.0: resolution: {integrity: sha512-i24m8rpwhmPIS4zscNzK6MSEhk0DUWa/8iYQWxhffV8jkI4Phvs3F+quL5xvS0gdQR0FyTCMMH33Y78dDTzzIw==} engines: {node: '>=18'} @@ -3841,6 +3997,9 @@ packages: loupe@3.1.1: resolution: {integrity: sha512-edNu/8D5MKVfGVFRhFf8aAxiTM6Wumfz5XsaatSxlD3w4R1d/WEKUTydCdPGbl9K7QG/Ca3GnDV2sIKIpXRQcw==} + loupe@3.1.2: + resolution: {integrity: sha512-23I4pFZHmAemUnz8WZXbYRSKYj801VDaNv9ETuMh7IrMc7VuVVSo+Z9iLE3ni30+U48iDWfi30d3twAXBYmnCg==} + lru-cache@10.4.3: resolution: {integrity: sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==} @@ -3861,6 +4020,9 @@ packages: magic-string@0.30.11: resolution: {integrity: sha512-+Wri9p0QHMy+545hKww7YAu5NyzF8iomPL/RQazugQ9+Ez4Ic3mERMd8ZTX5rfK944j+560ZJi8iAwgak1Ac7A==} + magic-string@0.30.17: + resolution: {integrity: sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA==} + magicast@0.3.5: resolution: {integrity: sha512-L0WhttDl+2BOsybvEOLK7fW3UA0OQ0IQ2d6Zl2x/a6vVRs3bAY0ECOSHHeL5jD+SbOpOCUEi0y1DgHEn9Qn1AQ==} @@ -4130,6 +4292,9 @@ packages: muggle-string@0.4.1: resolution: {integrity: sha512-VNTrAak/KhO2i8dqqnqnAHOa3cYBwXEZe9h+D5h/1ZqFSTEFHdM65lR7RoIqq3tBBYavsOXV84NoHXZ0AkPyqQ==} + mute-stream@0.0.8: + resolution: {integrity: sha512-nnbWWOkoWyUsTjKrhgD0dcz22mdkSnpYqbEjIm2nhwhuxlSkpywJmBo8h0ZqJdkp73mb90SssHkN4rsRaBAfAA==} + mz@2.7.0: resolution: {integrity: sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==} @@ -4259,6 +4424,10 @@ packages: resolution: {integrity: sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==} engines: {node: '>= 0.8.0'} + ora@5.4.1: + resolution: {integrity: sha512-5b6Y85tPxZZ7QytO+BQzysW31HJku27cRIlkbAXaNx+BdcVi+LlRFmVXzeF6a7JCwJpyw5c4b+YSVImQIrBpuQ==} + engines: {node: '>=10'} + ora@8.1.0: resolution: {integrity: sha512-GQEkNkH/GHOhPFXcqZs3IDahXEQcQxsSjEkK4KvEEST4t7eNzoMjxTzef+EZ+JluDEV+Raoi3WQ2CflnRdSVnQ==} engines: {node: '>=18'} @@ -4383,6 +4552,9 @@ packages: pathe@1.1.2: resolution: {integrity: sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ==} + pathe@2.0.2: + resolution: {integrity: sha512-15Ztpk+nov8DR524R4BF7uEuzESgzUEAV4Ah7CUMNGXdE5ELuvxElxGXndBl32vMSsWa1jpNf22Z+Er3sKwq+w==} + pathval@2.0.0: resolution: {integrity: sha512-vE7JKRyES09KiunauX7nd2Q9/L7lhok4smP9RZTDeD4MVs72Dp2qNFVz39Nz5a0FVEW0BJR6C0DYrq6unoziZA==} engines: {node: '>= 14.16'} @@ -4570,6 +4742,9 @@ packages: resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} engines: {node: '>=6'} + pure-rand@6.1.0: + resolution: {integrity: sha512-bVWawvoZoBYpp6yIoQtQXHZjmz35RSVHnUOTefl8Vcjr8snTPY1wnpSPMWekcFwbxI6gtmT7rSYPFvz71ldiOA==} + qs@6.13.0: resolution: {integrity: sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==} engines: {node: '>=0.6'} @@ -4587,6 +4762,10 @@ packages: r1csfile@0.0.48: resolution: {integrity: sha512-kHRkKUJNaor31l05f2+RFzvcH5XSa7OfEfd/l4hzjte6NL6fjRkSMfZ4BjySW9wmfdwPOtq3mXurzPvPGEf5Tw==} + randexp@0.5.3: + resolution: {integrity: sha512-U+5l2KrcMNOUPYvazA3h5ekF80FHTUG+87SEAmHZmolh1M+i/WyTCxVzmi+tidIa1tM4BSe8g2Y/D3loWDjj+w==} + engines: {node: '>=4'} + randombytes@2.1.0: resolution: {integrity: sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==} @@ -4743,6 +4922,10 @@ packages: remark-stringify@11.0.0: resolution: {integrity: sha512-1OSmLd3awB/t8qdoEOMazZkNsfVTeY4fTsgzcQFdXNq8ToTN4ZGwrMnlda4K6smTFKD+GRV6O48i6Z4iKgPPpw==} + repeat-string@1.6.1: + resolution: {integrity: sha512-PV0dzCYDNfRi1jCDbJzpW7jNNDRuCOG/jI5ctQcGKt/clZD+YcPS3yIlWuTJMmESC8aevCFmWJy5wjAFgNqN6w==} + engines: {node: '>=0.10'} + request-light@0.5.8: resolution: {integrity: sha512-3Zjgh+8b5fhRJBQZoy+zbVKpAQGLyka0MPgW3zruTF4dFFJ8Fqcfu9YsAvi/rvdcaTeWG3MkbZv4WKxAn/84Lg==} @@ -4772,10 +4955,18 @@ packages: resolution: {integrity: sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw==} hasBin: true + restore-cursor@3.1.0: + resolution: {integrity: sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA==} + engines: {node: '>=8'} + restore-cursor@5.1.0: resolution: {integrity: sha512-oMA2dcrw6u0YfxJQXm342bFKX/E4sG9rbTzO9ptUcR/e8A33cHuvStiYOwH7fszkZlZ1z/ta9AAoPk2F4qIOHA==} engines: {node: '>=18'} + ret@0.2.2: + resolution: {integrity: sha512-M0b3YWQs7R3Z917WRQy1HHA7Ba7D8hvZg6UE5mLykJxQVE2ju0IXbGlaHPPlkY+WN7wFP+wUMXmBFA0aV6vYGQ==} + engines: {node: '>=4'} + retext-latin@4.0.0: resolution: {integrity: sha512-hv9woG7Fy0M9IlRQloq/N6atV82NxLGveq+3H2WOi79dtIYWN8OaxogDm77f8YnVXJL2VD3bbqowu5E3EMhBYA==} @@ -4800,9 +4991,16 @@ packages: engines: {node: '>=18.0.0', npm: '>=8.0.0'} hasBin: true + run-async@2.4.1: + resolution: {integrity: sha512-tvVnVv01b8c1RrA6Ep7JkStj85Guv/YrMcwqYQnwjsAS2cTmmPGBBjAjpCW7RrSodNSoE2/qg9O4bceNvUuDgQ==} + engines: {node: '>=0.12.0'} + run-parallel@1.2.0: resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==} + rxjs@7.8.1: + resolution: {integrity: sha512-AA3TVj+0A2iuIoQkWEK/tqFjBq2j+6PO6Y0zJcvzLAFhEFIO3HL0vls9hWLncZbAAbK0mar7oZ4V079I/qPMxg==} + safe-array-concat@1.1.2: resolution: {integrity: sha512-vj6RsCsWBCf19jIeHEfkRMw8DPiBb+DMXklQ/1SGDHOMlHdPUkZXFQ2YdplS23zESTijAcurb1aSgJA3AgMu1Q==} engines: {node: '>=0.4'} @@ -4989,6 +5187,9 @@ packages: std-env@3.7.0: resolution: {integrity: sha512-JPbdCEQLj1w5GilpiHAx3qJvFndqybBysA3qUOnznweH4QbNYUsW/ea8QzSrnh0vNsezMMw5bcVool8lM0gwzg==} + std-env@3.8.0: + resolution: {integrity: sha512-Bc3YwwCB+OzldMxOXJIIvC6cPRWr/LxOp48CdQTOkPyk/t4JWWJbrilwBd7RJzKV8QW7tJkcgAmeuLLJugl5/w==} + stdin-discarder@0.2.2: resolution: {integrity: sha512-UhDfHmA92YAlNnCfhmq0VeNL5bDbiZGg7sZ2IvPsXubGkiNa9EC+tUTsjBRsYUAz87btI6/1wf4XoVvQ3uRnmQ==} engines: {node: '>=18'} @@ -5151,6 +5352,9 @@ packages: thenify@3.3.1: resolution: {integrity: sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==} + through@2.3.8: + resolution: {integrity: sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==} + timers-browserify@2.0.12: resolution: {integrity: sha512-9phl76Cqm6FhSX9Xe1ZUAMLtm1BLkKj2Qd5ApyWkXzsMRaA7dgr81kf4wJmQf/hAvg8EEyJxDo3du/0KlhPiKQ==} engines: {node: '>=0.6.0'} @@ -5161,6 +5365,9 @@ packages: tinyexec@0.3.0: resolution: {integrity: sha512-tVGE0mVJPGb0chKhqmsoosjsS+qUnJVGJpZgsHYQcGoPlG3B51R3PouqTgEGH2Dc9jjFyOqOpix6ZHNMXp1FZg==} + tinyexec@0.3.2: + resolution: {integrity: sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==} + tinyglobby@0.2.9: resolution: {integrity: sha512-8or1+BGEdk1Zkkw2ii16qSS7uVrQJPre5A9o/XkWPATkk23FZh/15BKFxPnlTy6vkljZxLqYCzzBMj30ZrSvjw==} engines: {node: '>=12.0.0'} @@ -5169,10 +5376,18 @@ packages: resolution: {integrity: sha512-URZYihUbRPcGv95En+sz6MfghfIc2OJ1sv/RmhWZLouPY0/8Vo80viwPvg3dlaS9fuq7fQMEfgRRK7BBZThBEA==} engines: {node: ^18.0.0 || >=20.0.0} + tinypool@1.0.2: + resolution: {integrity: sha512-al6n+QEANGFOMf/dmUMsuS5/r9B06uwlyNjZZql/zv8J7ybHCgoihBNORZCY2mzUuAnomQa2JdhyHKzZxPCrFA==} + engines: {node: ^18.0.0 || >=20.0.0} + tinyrainbow@1.2.0: resolution: {integrity: sha512-weEDEq7Z5eTHPDh4xjX789+fHfF+P8boiFB+0vbWzpbnbsEr/GRaohi/uMKxg8RZMXnl1ItAi/IUHWMsjDV7kQ==} engines: {node: '>=14.0.0'} + tinyrainbow@2.0.0: + resolution: {integrity: sha512-op4nsTR47R6p0vMUUoYl/a+ljLFVtlfaXkLQmqfLR1qHma1h/ysYk4hEXZ880bf2CYgTskvTa/e196Vd5dDQXw==} + engines: {node: '>=14.0.0'} + tinyspy@3.0.2: resolution: {integrity: sha512-n1cw8k1k0x4pgA2+9XrOkFydTerNcJ1zWCO5Nn9scWHTD+5tp8dghT2x1uduQePZTZgd3Tupf+x9BxJjeJi77Q==} engines: {node: '>=14.0.0'} @@ -5323,6 +5538,10 @@ packages: resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==} engines: {node: '>= 0.8.0'} + type-fest@0.21.3: + resolution: {integrity: sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==} + engines: {node: '>=10'} + type-fest@2.19.0: resolution: {integrity: sha512-RAH822pAdBgcNMAfWnCBU3CFZcfZ/i1eZjwFU/dsLKumyuuP3niueg2UAukXYF0E2AAoc82ZSSf9J0WQBinzHA==} engines: {node: '>=12.20'} @@ -5381,6 +5600,18 @@ packages: engines: {node: '>=14.17'} hasBin: true + typescript@5.7.2: + resolution: {integrity: sha512-i5t66RHxDvVN40HfDd1PsEThGNnlMCMT3jMUuoh9/0TaqWevNontacunWyN02LA9/fIbEWlcHZcgTKb9QoaLfg==} + engines: {node: '>=14.17'} + hasBin: true + + typia@7.6.0: + resolution: {integrity: sha512-iuxkRcZltbxEA87CvW6vIvNaQerEcLZbRmJugMraZFAMTwVf3AKvQEh4pSq2n1VMfomINnWFhy0nxEWvpEzHbQ==} + hasBin: true + peerDependencies: + '@samchon/openapi': '>=2.4.0 <3.0.0' + typescript: '>=4.8.0 <5.8.0' + uc.micro@2.1.0: resolution: {integrity: sha512-ARDJmphmdvUk6Glw7y9DQ2bFkKBHwQHLi2lsaH6PPmz/Ka9sFOBsBluozhDltWmnv9u/cF6Rt87znRTPV+yp/A==} @@ -5519,6 +5750,16 @@ packages: engines: {node: ^18.0.0 || >=20.0.0} hasBin: true + vite-node@3.0.4: + resolution: {integrity: sha512-7JZKEzcYV2Nx3u6rlvN8qdo3QV7Fxyt6hx+CCKz9fbWxdX5IvUOmTWEAxMrWxaiSf7CKGLJQ5rFu8prb/jBjOA==} + engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0} + hasBin: true + + vite-node@3.0.5: + resolution: {integrity: sha512-02JEJl7SbtwSDJdYS537nU6l+ktdvcREfLksk/NDAqtdKWGqHl+joXzEubHROmS3E6pip+Xgu2tFezMu75jH7A==} + engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0} + hasBin: true + vite-plugin-node-polyfills@0.22.0: resolution: {integrity: sha512-F+G3LjiGbG8QpbH9bZ//GSBr9i1InSTkaulfUHFa9jkLqVGORFBoqc2A/Yu5Mmh1kNAbiAeKeK+6aaQUf3x0JA==} peerDependencies: @@ -5638,6 +5879,62 @@ packages: jsdom: optional: true + vitest@3.0.4: + resolution: {integrity: sha512-6XG8oTKy2gnJIFTHP6LD7ExFeNLxiTkK3CfMvT7IfR8IN+BYICCf0lXUQmX7i7JoxUP8QmeP4mTnWXgflu4yjw==} + engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0} + hasBin: true + peerDependencies: + '@edge-runtime/vm': '*' + '@types/debug': ^4.1.12 + '@types/node': ^18.0.0 || ^20.0.0 || >=22.0.0 + '@vitest/browser': 3.0.4 + '@vitest/ui': 3.0.4 + happy-dom: '*' + jsdom: '*' + peerDependenciesMeta: + '@edge-runtime/vm': + optional: true + '@types/debug': + optional: true + '@types/node': + optional: true + '@vitest/browser': + optional: true + '@vitest/ui': + optional: true + happy-dom: + optional: true + jsdom: + optional: true + + vitest@3.0.5: + resolution: {integrity: sha512-4dof+HvqONw9bvsYxtkfUp2uHsTN9bV2CZIi1pWgoFpL1Lld8LA1ka9q/ONSsoScAKG7NVGf2stJTI7XRkXb2Q==} + engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0} + hasBin: true + peerDependencies: + '@edge-runtime/vm': '*' + '@types/debug': ^4.1.12 + '@types/node': ^18.0.0 || ^20.0.0 || >=22.0.0 + '@vitest/browser': 3.0.5 + '@vitest/ui': 3.0.5 + happy-dom: '*' + jsdom: '*' + peerDependenciesMeta: + '@edge-runtime/vm': + optional: true + '@types/debug': + optional: true + '@types/node': + optional: true + '@vitest/browser': + optional: true + '@vitest/ui': + optional: true + happy-dom: + optional: true + jsdom: + optional: true + vm-browserify@1.1.2: resolution: {integrity: sha512-2ham8XPWTONajOR0ohOKOHXkm3+gaBmGut3SRuu75xLd/RRaY6vqgh8NBYYk7+RW3u5AtzPQZG8F10LHkl0lAQ==} @@ -5808,6 +6105,10 @@ packages: resolution: {integrity: sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==} engines: {node: '>=0.10.0'} + wrap-ansi@6.2.0: + resolution: {integrity: sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==} + engines: {node: '>=8'} + wrap-ansi@7.0.0: resolution: {integrity: sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==} engines: {node: '>=10'} @@ -6679,6 +6980,11 @@ snapshots: dependencies: '@expressive-code/core': 0.35.6 + '@fast-check/vitest@0.1.5(vitest@3.0.5(@types/debug@4.1.12)(@types/node@22.5.5)(sass@1.80.3)(terser@5.34.1))': + dependencies: + fast-check: 3.23.2 + vitest: 3.0.5(@types/debug@4.1.12)(@types/node@22.5.5)(sass@1.80.3)(terser@5.34.1) + '@humanwhocodes/module-importer@1.0.1': {} '@humanwhocodes/retry@0.3.0': {} @@ -7233,6 +7539,8 @@ snapshots: '@rtsao/scc@1.1.0': {} + '@samchon/openapi@2.4.1': {} + '@semaphore-protocol/core@4.0.3': dependencies: '@semaphore-protocol/group': 4.0.3 @@ -7516,7 +7824,7 @@ snapshots: dependencies: '@typescript-eslint/typescript-estree': 8.4.0(typescript@5.5.4) '@typescript-eslint/utils': 8.4.0(eslint@9.9.1(jiti@1.21.6))(typescript@5.5.4) - debug: 4.3.7 + debug: 4.4.0 ts-api-utils: 1.3.0(typescript@5.5.4) optionalDependencies: typescript: 5.5.4 @@ -7530,7 +7838,7 @@ snapshots: dependencies: '@typescript-eslint/types': 8.4.0 '@typescript-eslint/visitor-keys': 8.4.0 - debug: 4.3.7 + debug: 4.4.0 fast-glob: 3.3.2 is-glob: 4.0.3 minimatch: 9.0.5 @@ -7602,6 +7910,20 @@ snapshots: chai: 5.1.1 tinyrainbow: 1.2.0 + '@vitest/expect@3.0.4': + dependencies: + '@vitest/spy': 3.0.4 + '@vitest/utils': 3.0.4 + chai: 5.1.2 + tinyrainbow: 2.0.0 + + '@vitest/expect@3.0.5': + dependencies: + '@vitest/spy': 3.0.5 + '@vitest/utils': 3.0.5 + chai: 5.1.2 + tinyrainbow: 2.0.0 + '@vitest/mocker@2.1.1(@vitest/spy@2.1.1)(vite@5.4.4(@types/node@22.5.5)(sass@1.80.3)(terser@5.34.1))': dependencies: '@vitest/spy': 2.1.1 @@ -7618,6 +7940,22 @@ snapshots: optionalDependencies: vite: 5.4.4(@types/node@22.5.5)(sass@1.80.3)(terser@5.34.1) + '@vitest/mocker@3.0.4(vite@5.4.4(@types/node@22.5.5)(sass@1.80.3)(terser@5.34.1))': + dependencies: + '@vitest/spy': 3.0.4 + estree-walker: 3.0.3 + magic-string: 0.30.17 + optionalDependencies: + vite: 5.4.4(@types/node@22.5.5)(sass@1.80.3)(terser@5.34.1) + + '@vitest/mocker@3.0.5(vite@5.4.4(@types/node@22.5.5)(sass@1.80.3)(terser@5.34.1))': + dependencies: + '@vitest/spy': 3.0.5 + estree-walker: 3.0.3 + magic-string: 0.30.17 + optionalDependencies: + vite: 5.4.4(@types/node@22.5.5)(sass@1.80.3)(terser@5.34.1) + '@vitest/pretty-format@2.0.5': dependencies: tinyrainbow: 1.2.0 @@ -7630,6 +7968,14 @@ snapshots: dependencies: tinyrainbow: 1.2.0 + '@vitest/pretty-format@3.0.4': + dependencies: + tinyrainbow: 2.0.0 + + '@vitest/pretty-format@3.0.5': + dependencies: + tinyrainbow: 2.0.0 + '@vitest/runner@2.0.5': dependencies: '@vitest/utils': 2.0.5 @@ -7645,6 +7991,16 @@ snapshots: '@vitest/utils': 2.1.2 pathe: 1.1.2 + '@vitest/runner@3.0.4': + dependencies: + '@vitest/utils': 3.0.4 + pathe: 2.0.2 + + '@vitest/runner@3.0.5': + dependencies: + '@vitest/utils': 3.0.5 + pathe: 2.0.2 + '@vitest/snapshot@2.0.5': dependencies: '@vitest/pretty-format': 2.0.5 @@ -7663,6 +8019,18 @@ snapshots: magic-string: 0.30.11 pathe: 1.1.2 + '@vitest/snapshot@3.0.4': + dependencies: + '@vitest/pretty-format': 3.0.4 + magic-string: 0.30.17 + pathe: 2.0.2 + + '@vitest/snapshot@3.0.5': + dependencies: + '@vitest/pretty-format': 3.0.5 + magic-string: 0.30.17 + pathe: 2.0.2 + '@vitest/spy@2.0.5': dependencies: tinyspy: 3.0.2 @@ -7675,6 +8043,14 @@ snapshots: dependencies: tinyspy: 3.0.2 + '@vitest/spy@3.0.4': + dependencies: + tinyspy: 3.0.2 + + '@vitest/spy@3.0.5': + dependencies: + tinyspy: 3.0.2 + '@vitest/ui@2.1.1(vitest@2.1.1)': dependencies: '@vitest/utils': 2.1.1 @@ -7706,6 +8082,18 @@ snapshots: loupe: 3.1.1 tinyrainbow: 1.2.0 + '@vitest/utils@3.0.4': + dependencies: + '@vitest/pretty-format': 3.0.4 + loupe: 3.1.2 + tinyrainbow: 2.0.0 + + '@vitest/utils@3.0.5': + dependencies: + '@vitest/pretty-format': 3.0.5 + loupe: 3.1.2 + tinyrainbow: 2.0.0 + '@volar/kit@2.4.5(typescript@5.6.2)': dependencies: '@volar/language-service': 2.4.5 @@ -7846,6 +8234,10 @@ snapshots: ansi-colors@4.1.3: {} + ansi-escapes@4.3.2: + dependencies: + type-fest: 0.21.3 + ansi-regex@5.0.1: {} ansi-regex@6.0.1: {} @@ -7900,6 +8292,8 @@ snapshots: array-iterate@2.0.1: {} + array-timsort@1.0.3: {} + array-union@2.1.0: {} array.prototype.findlastindex@1.2.5: @@ -8258,6 +8652,8 @@ snapshots: caniuse-lite@1.0.30001655: {} + canonicalize@2.0.0: {} + ccount@2.0.1: {} chai@5.1.1: @@ -8268,6 +8664,14 @@ snapshots: loupe: 3.1.1 pathval: 2.0.0 + chai@5.1.2: + dependencies: + assertion-error: 2.0.1 + check-error: 2.1.1 + deep-eql: 5.0.2 + loupe: 3.1.2 + pathval: 2.0.0 + chalk@2.4.2: dependencies: ansi-styles: 3.2.1 @@ -8335,20 +8739,25 @@ snapshots: cli-boxes@3.0.0: {} + cli-cursor@3.1.0: + dependencies: + restore-cursor: 3.1.0 + cli-cursor@5.0.0: dependencies: restore-cursor: 5.1.0 cli-spinners@2.9.2: {} + cli-width@3.0.0: {} + cliui@8.0.1: dependencies: string-width: 4.2.3 strip-ansi: 6.0.1 wrap-ansi: 7.0.0 - clone@1.0.4: - optional: true + clone@1.0.4: {} clsx@2.0.0: {} @@ -8380,11 +8789,21 @@ snapshots: comma-separated-tokens@2.0.3: {} + commander@10.0.1: {} + commander@2.20.3: optional: true commander@4.1.1: {} + comment-json@4.2.5: + dependencies: + array-timsort: 1.0.3 + core-util-is: 1.0.3 + esprima: 4.0.1 + has-own-prop: 2.0.0 + repeat-string: 1.6.1 + common-ancestor-path@1.0.1: {} concat-map@0.0.1: {} @@ -8503,6 +8922,10 @@ snapshots: dependencies: ms: 2.1.3 + debug@4.4.0: + dependencies: + ms: 2.1.3 + decode-named-character-reference@1.0.2: dependencies: character-entities: 2.0.2 @@ -8520,7 +8943,6 @@ snapshots: defaults@1.0.4: dependencies: clone: 1.0.4 - optional: true define-data-property@1.1.4: dependencies: @@ -8589,6 +9011,8 @@ snapshots: dotenv@16.0.3: {} + drange@1.1.1: {} + dset@3.1.4: {} eastasianwidth@0.2.0: {} @@ -8701,6 +9125,8 @@ snapshots: es-module-lexer@1.5.4: {} + es-module-lexer@1.6.0: {} + es-object-atoms@1.0.0: dependencies: es-errors: 1.3.0 @@ -9024,6 +9450,8 @@ snapshots: expand-template@2.0.3: {} + expect-type@1.1.0: {} + expressive-code@0.35.6: dependencies: '@expressive-code/core': 0.35.6 @@ -9045,6 +9473,10 @@ snapshots: iconv-lite: 0.4.24 tmp: 0.0.33 + fast-check@3.23.2: + dependencies: + pure-rand: 6.1.0 + fast-deep-equal@3.1.3: {} fast-fifo@1.3.2: {} @@ -9085,6 +9517,10 @@ snapshots: fflate@0.8.2: optional: true + figures@3.2.0: + dependencies: + escape-string-regexp: 1.0.5 + file-entry-cache@8.0.0: dependencies: flat-cache: 4.0.1 @@ -9257,6 +9693,8 @@ snapshots: has-flag@4.0.0: {} + has-own-prop@2.0.0: {} + has-property-descriptors@1.0.2: dependencies: es-define-property: 1.0.0 @@ -9527,6 +9965,24 @@ snapshots: inline-style-parser@0.2.4: {} + inquirer@8.2.6: + dependencies: + ansi-escapes: 4.3.2 + chalk: 4.1.2 + cli-cursor: 3.1.0 + cli-width: 3.0.0 + external-editor: 3.1.0 + figures: 3.2.0 + lodash: 4.17.21 + mute-stream: 0.0.8 + ora: 5.4.1 + run-async: 2.4.1 + rxjs: 7.8.1 + string-width: 4.2.3 + strip-ansi: 6.0.1 + through: 2.3.8 + wrap-ansi: 6.2.0 + internal-slot@1.0.7: dependencies: es-errors: 1.3.0 @@ -9607,6 +10063,8 @@ snapshots: dependencies: is-docker: 3.0.0 + is-interactive@1.0.0: {} + is-interactive@2.0.0: {} is-nan@1.3.2: @@ -9659,6 +10117,8 @@ snapshots: dependencies: which-typed-array: 1.1.15 + is-unicode-supported@0.1.0: {} + is-unicode-supported@1.3.0: {} is-unicode-supported@2.1.0: {} @@ -9759,7 +10219,7 @@ snapshots: kleur@4.1.5: {} - knip@5.29.2(@types/node@22.5.5)(typescript@5.5.4): + knip@5.29.2(@types/node@22.5.5)(typescript@5.7.2): dependencies: '@nodelib/fs.walk': 1.2.8 '@snyk/github-codeowners': 1.1.0 @@ -9776,7 +10236,7 @@ snapshots: smol-toml: 1.3.0 strip-json-comments: 5.0.1 summary: 2.1.0 - typescript: 5.5.4 + typescript: 5.7.2 zod: 3.23.8 zod-validation-error: 3.3.1(zod@3.23.8) @@ -9829,6 +10289,11 @@ snapshots: lodash@4.17.21: {} + log-symbols@4.1.0: + dependencies: + chalk: 4.1.2 + is-unicode-supported: 0.1.0 + log-symbols@6.0.0: dependencies: chalk: 5.3.0 @@ -9846,6 +10311,8 @@ snapshots: dependencies: get-func-name: 2.0.2 + loupe@3.1.2: {} + lru-cache@10.4.3: {} lru-cache@4.1.5: @@ -9867,6 +10334,10 @@ snapshots: dependencies: '@jridgewell/sourcemap-codec': 1.5.0 + magic-string@0.30.17: + dependencies: + '@jridgewell/sourcemap-codec': 1.5.0 + magicast@0.3.5: dependencies: '@babel/parser': 7.25.6 @@ -10339,7 +10810,7 @@ snapshots: micromark@4.0.0: dependencies: '@types/debug': 4.1.12 - debug: 4.3.7 + debug: 4.4.0 decode-named-character-reference: 1.0.2 devlop: 1.1.0 micromark-core-commonmark: 2.0.1 @@ -10412,6 +10883,8 @@ snapshots: muggle-string@0.4.1: {} + mute-stream@0.0.8: {} + mz@2.7.0: dependencies: any-promise: 1.3.0 @@ -10568,6 +11041,18 @@ snapshots: type-check: 0.4.0 word-wrap: 1.2.5 + ora@5.4.1: + dependencies: + bl: 4.1.0 + chalk: 4.1.2 + cli-cursor: 3.1.0 + cli-spinners: 2.9.2 + is-interactive: 1.0.0 + is-unicode-supported: 0.1.0 + log-symbols: 4.1.0 + strip-ansi: 6.0.1 + wcwidth: 1.0.1 + ora@8.1.0: dependencies: chalk: 5.3.0 @@ -10699,6 +11184,8 @@ snapshots: pathe@1.1.2: {} + pathe@2.0.2: {} + pathval@2.0.0: {} pbkdf2@3.1.2: @@ -10884,6 +11371,8 @@ snapshots: punycode@2.3.1: {} + pure-rand@6.1.0: {} + qs@6.13.0: dependencies: side-channel: 1.0.6 @@ -10901,6 +11390,11 @@ snapshots: fastfile: 0.0.20 ffjavascript: 0.3.0 + randexp@0.5.3: + dependencies: + drange: 1.1.1 + ret: 0.2.2 + randombytes@2.1.0: dependencies: safe-buffer: 5.2.1 @@ -11132,6 +11626,8 @@ snapshots: mdast-util-to-markdown: 2.1.0 unified: 11.0.5 + repeat-string@1.6.1: {} + request-light@0.5.8: {} request-light@0.7.0: {} @@ -11153,11 +11649,18 @@ snapshots: path-parse: 1.0.7 supports-preserve-symlinks-flag: 1.0.0 + restore-cursor@3.1.0: + dependencies: + onetime: 5.1.2 + signal-exit: 3.0.7 + restore-cursor@5.1.0: dependencies: onetime: 7.0.0 signal-exit: 4.1.0 + ret@0.2.2: {} + retext-latin@4.0.0: dependencies: '@types/nlcst': 2.0.3 @@ -11212,10 +11715,16 @@ snapshots: '@rollup/rollup-win32-x64-msvc': 4.21.2 fsevents: 2.3.3 + run-async@2.4.1: {} + run-parallel@1.2.0: dependencies: queue-microtask: 1.2.3 + rxjs@7.8.1: + dependencies: + tslib: 2.7.0 + safe-array-concat@1.1.2: dependencies: call-bind: 1.0.7 @@ -11444,6 +11953,8 @@ snapshots: std-env@3.7.0: {} + std-env@3.8.0: {} + stdin-discarder@0.2.2: {} stream-browserify@3.0.0: @@ -11712,6 +12223,8 @@ snapshots: dependencies: any-promise: 1.3.0 + through@2.3.8: {} + timers-browserify@2.0.12: dependencies: setimmediate: 1.0.5 @@ -11720,6 +12233,8 @@ snapshots: tinyexec@0.3.0: {} + tinyexec@0.3.2: {} + tinyglobby@0.2.9: dependencies: fdir: 6.4.2(picomatch@4.0.2) @@ -11728,8 +12243,12 @@ snapshots: tinypool@1.0.1: {} + tinypool@1.0.2: {} + tinyrainbow@1.2.0: {} + tinyrainbow@2.0.0: {} + tinyspy@3.0.2: {} tmp@0.0.33: @@ -11938,6 +12457,8 @@ snapshots: dependencies: prelude-ls: 1.2.1 + type-fest@0.21.3: {} + type-fest@2.19.0: {} typed-array-buffer@1.0.2: @@ -12006,6 +12527,18 @@ snapshots: typescript@5.6.2: {} + typescript@5.7.2: {} + + typia@7.6.0(@samchon/openapi@2.4.1)(typescript@5.5.4): + dependencies: + '@samchon/openapi': 2.4.1 + commander: 10.0.1 + comment-json: 4.2.5 + inquirer: 8.2.6 + package-manager-detector: 0.2.0 + randexp: 0.5.3 + typescript: 5.5.4 + uc.micro@2.1.0: {} unbox-primitive@1.0.2: @@ -12203,6 +12736,42 @@ snapshots: - supports-color - terser + vite-node@3.0.4(@types/node@22.5.5)(sass@1.80.3)(terser@5.34.1): + dependencies: + cac: 6.7.14 + debug: 4.4.0 + es-module-lexer: 1.6.0 + pathe: 2.0.2 + vite: 5.4.4(@types/node@22.5.5)(sass@1.80.3)(terser@5.34.1) + transitivePeerDependencies: + - '@types/node' + - less + - lightningcss + - sass + - sass-embedded + - stylus + - sugarss + - supports-color + - terser + + vite-node@3.0.5(@types/node@22.5.5)(sass@1.80.3)(terser@5.34.1): + dependencies: + cac: 6.7.14 + debug: 4.4.0 + es-module-lexer: 1.6.0 + pathe: 2.0.2 + vite: 5.4.4(@types/node@22.5.5)(sass@1.80.3)(terser@5.34.1) + transitivePeerDependencies: + - '@types/node' + - less + - lightningcss + - sass + - sass-embedded + - stylus + - sugarss + - supports-color + - terser + vite-plugin-node-polyfills@0.22.0(rollup@4.21.2)(vite@5.4.4(@types/node@20.16.3)(sass@1.80.3)(terser@5.34.1)): dependencies: '@rollup/plugin-inject': 5.0.5(rollup@4.21.2) @@ -12347,6 +12916,78 @@ snapshots: - supports-color - terser + vitest@3.0.4(@types/debug@4.1.12)(@types/node@22.5.5)(sass@1.80.3)(terser@5.34.1): + dependencies: + '@vitest/expect': 3.0.4 + '@vitest/mocker': 3.0.4(vite@5.4.4(@types/node@22.5.5)(sass@1.80.3)(terser@5.34.1)) + '@vitest/pretty-format': 3.0.4 + '@vitest/runner': 3.0.4 + '@vitest/snapshot': 3.0.4 + '@vitest/spy': 3.0.4 + '@vitest/utils': 3.0.4 + chai: 5.1.2 + debug: 4.4.0 + expect-type: 1.1.0 + magic-string: 0.30.17 + pathe: 2.0.2 + std-env: 3.8.0 + tinybench: 2.9.0 + tinyexec: 0.3.2 + tinypool: 1.0.2 + tinyrainbow: 2.0.0 + vite: 5.4.4(@types/node@22.5.5)(sass@1.80.3)(terser@5.34.1) + vite-node: 3.0.4(@types/node@22.5.5)(sass@1.80.3)(terser@5.34.1) + why-is-node-running: 2.3.0 + optionalDependencies: + '@types/debug': 4.1.12 + '@types/node': 22.5.5 + transitivePeerDependencies: + - less + - lightningcss + - msw + - sass + - sass-embedded + - stylus + - sugarss + - supports-color + - terser + + vitest@3.0.5(@types/debug@4.1.12)(@types/node@22.5.5)(sass@1.80.3)(terser@5.34.1): + dependencies: + '@vitest/expect': 3.0.5 + '@vitest/mocker': 3.0.5(vite@5.4.4(@types/node@22.5.5)(sass@1.80.3)(terser@5.34.1)) + '@vitest/pretty-format': 3.0.5 + '@vitest/runner': 3.0.5 + '@vitest/snapshot': 3.0.5 + '@vitest/spy': 3.0.5 + '@vitest/utils': 3.0.5 + chai: 5.1.2 + debug: 4.4.0 + expect-type: 1.1.0 + magic-string: 0.30.17 + pathe: 2.0.2 + std-env: 3.8.0 + tinybench: 2.9.0 + tinyexec: 0.3.2 + tinypool: 1.0.2 + tinyrainbow: 2.0.0 + vite: 5.4.4(@types/node@22.5.5)(sass@1.80.3)(terser@5.34.1) + vite-node: 3.0.5(@types/node@22.5.5)(sass@1.80.3)(terser@5.34.1) + why-is-node-running: 2.3.0 + optionalDependencies: + '@types/debug': 4.1.12 + '@types/node': 22.5.5 + transitivePeerDependencies: + - less + - lightningcss + - msw + - sass + - sass-embedded + - stylus + - sugarss + - supports-color + - terser + vm-browserify@1.1.2: {} volar-service-css@0.0.61(@volar/language-service@2.4.5): @@ -12469,7 +13110,6 @@ snapshots: wcwidth@1.0.1: dependencies: defaults: 1.0.4 - optional: true web-namespaces@2.0.1: {} @@ -12524,6 +13164,12 @@ snapshots: word-wrap@1.2.5: {} + wrap-ansi@6.2.0: + dependencies: + ansi-styles: 4.3.0 + string-width: 4.2.3 + strip-ansi: 6.0.1 + wrap-ansi@7.0.0: dependencies: ansi-styles: 4.3.0 diff --git a/vitest.workspace.ts b/vitest.workspace.ts new file mode 100644 index 0000000..c1efc9a --- /dev/null +++ b/vitest.workspace.ts @@ -0,0 +1 @@ +export default ["packages/*"];