From cd02b5a326480560c98d41620f3bd30ff250e186 Mon Sep 17 00:00:00 2001 From: Rob Knight Date: Mon, 27 Jan 2025 07:50:53 +0100 Subject: [PATCH 01/20] WIP --- .vscode/settings.json | 5 +- package.json | 4 +- packages/eslint-config/eslint.base.config.mjs | 4 +- packages/podspec/package.json | 6 +- packages/podspec/src/builders/entries.ts | 174 ++++ packages/podspec/src/builders/entry.ts | 54 ++ packages/podspec/src/builders/group.ts | 86 ++ packages/podspec/src/builders/groupv2.ts | 103 +++ packages/podspec/src/builders/pod.ts | 168 ++++ packages/podspec/src/builders/podv2.ts | 769 ++++++++++++++++++ packages/podspec/src/builders/shared.ts | 13 + packages/podspec/src/group.ts | 118 +++ packages/podspec/src/index.ts | 3 +- packages/podspec/src/parse/pod.ts | 64 ++ packages/podspec/src/processors/proof.ts | 0 packages/podspec/src/processors/query.ts | 0 packages/podspec/src/processors/validate.ts | 0 packages/podspec/src/schemas/entry.ts | 100 ++- packages/podspec/src/schemas/string.ts | 2 +- packages/podspec/src/types/entries.ts | 98 +++ packages/podspec/src/types/group.ts | 116 +++ packages/podspec/src/types/pod.ts | 62 ++ packages/podspec/src/types/utils.ts | 3 + packages/podspec/test/podspec.spec.ts | 79 ++ packages/podspec/test/scratch.ts | 0 packages/podspec/test/serialization.spec.ts | 1 - packages/podspec/tsconfig.json | 3 +- packages/podspec/tsup.config.ts | 13 + packages/podspec/vitest.config.ts | 7 + pnpm-lock.yaml | 294 ++++++- vitest.workspace.ts | 1 + 31 files changed, 2322 insertions(+), 28 deletions(-) create mode 100644 packages/podspec/src/builders/entries.ts create mode 100644 packages/podspec/src/builders/entry.ts create mode 100644 packages/podspec/src/builders/group.ts create mode 100644 packages/podspec/src/builders/groupv2.ts create mode 100644 packages/podspec/src/builders/pod.ts create mode 100644 packages/podspec/src/builders/podv2.ts create mode 100644 packages/podspec/src/builders/shared.ts create mode 100644 packages/podspec/src/group.ts create mode 100644 packages/podspec/src/processors/proof.ts create mode 100644 packages/podspec/src/processors/query.ts create mode 100644 packages/podspec/src/processors/validate.ts create mode 100644 packages/podspec/src/types/entries.ts create mode 100644 packages/podspec/src/types/group.ts create mode 100644 packages/podspec/src/types/pod.ts create mode 100644 packages/podspec/src/types/utils.ts create mode 100644 packages/podspec/test/scratch.ts create mode 100644 packages/podspec/tsup.config.ts create mode 100644 packages/podspec/vitest.config.ts create mode 100644 vitest.workspace.ts diff --git a/.vscode/settings.json b/.vscode/settings.json index 44f3c3d..6248b6a 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -12,7 +12,7 @@ "ts-library.json": "jsonc" }, "[typescript]": { - "editor.defaultFormatter": "biomejs.biome", + "editor.defaultFormatter": "esbenp.prettier-vscode", "editor.codeActionsOnSave": { "quickfix.biome": "explicit", "source.organizeImports.biome": "explicit" @@ -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/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/eslint-config/eslint.base.config.mjs b/packages/eslint-config/eslint.base.config.mjs index 202fff0..d543764 100644 --- a/packages/eslint-config/eslint.base.config.mjs +++ b/packages/eslint-config/eslint.base.config.mjs @@ -16,7 +16,9 @@ export default tseslint.config( "**/dist/", "**/vitest.config.ts", "**/vite.config.ts", - "**/tailwind.config.ts" + "**/vitest.workspace.ts", + "**/tailwind.config.ts", + "**/tsup.config.ts" ] // global ignore with single ignore key }, { diff --git a/packages/podspec/package.json b/packages/podspec/package.json index 693f1fb..7d81d45 100644 --- a/packages/podspec/package.json +++ b/packages/podspec/package.json @@ -30,8 +30,8 @@ }, "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"], @@ -49,7 +49,7 @@ "tsup": "^8.2.4", "typescript": "^5.5", "uuid": "^9.0.0", - "vitest": "^2.1.1" + "vitest": "^3.0.0" }, "publishConfig": { "access": "public", diff --git a/packages/podspec/src/builders/entries.ts b/packages/podspec/src/builders/entries.ts new file mode 100644 index 0000000..10d3630 --- /dev/null +++ b/packages/podspec/src/builders/entries.ts @@ -0,0 +1,174 @@ +import type { + EntriesSpec, + EntryListSpec, + EntrySpec +} from "../types/entries.js"; + +/** + * Converts a complex type expression into its concrete, simplified form. + * For example, converts Pick<{a: string}, 'a'> into {a: string} + */ +type Concrete = T extends object ? { [K in keyof T]: T[K] } : T; + +/** + * @todo add some type parameter to keep track of tuples + */ +export class EntriesSpecBuilder { + readonly #spec: EntriesSpec; + + private constructor(spec: EntriesSpec) { + this.#spec = spec; + } + + public static create(entries: E) { + return new EntriesSpecBuilder({ entries }); + } + + public add( + key: Exclude, + type: T + ): EntriesSpecBuilder> { + if (key in this.#spec.entries) { + throw new ReferenceError( + `Key ${key.toString()} already exists in entries: ${Object.keys( + this.#spec.entries + ).join(", ")}` + ); + } + return new EntriesSpecBuilder>({ + entries: { + ...this.#spec.entries, + [key]: type + } as Concrete + }); + } + + public pick( + keys: K + ): EntriesSpecBuilder>> { + return new EntriesSpecBuilder>>({ + entries: keys.reduce( + (acc, key) => { + if (!(key in this.#spec.entries)) { + throw new ReferenceError( + `Key ${key.toString()} not found in entries: ${Object.keys( + this.#spec.entries + ).join(", ")}` + ); + } + return { + ...acc, + [key]: this.#spec.entries[key] + }; + }, + {} as Concrete> + ) + }); + } + + public omit( + keys: K, + { strict = true }: { strict?: boolean } = {} + ): EntriesSpecBuilder>> { + if (strict) { + for (const key of keys) { + if (!(key in this.#spec.entries)) { + throw new ReferenceError( + `Key ${key.toString()} not found in entries: ${Object.keys( + this.#spec.entries + ).join(", ")}` + ); + } + } + } + return new EntriesSpecBuilder>>({ + entries: Object.fromEntries( + Object.entries(this.#spec.entries).filter( + ([key]) => !keys.includes(key) + ) + ) as Concrete> + }); + } + + public merge( + other: EntriesSpecBuilder + ): EntriesSpecBuilder> { + return new EntriesSpecBuilder>({ + entries: { + ...this.#spec.entries, + ...other.#spec.entries + } as Concrete + }); + } + + public build(): EntriesSpec { + return structuredClone(this.#spec); + } +} + +if (import.meta.vitest) { + const { describe, it, expect } = import.meta.vitest; + + describe("EntriesSpecBuilder", () => { + it("should add entries correctly", () => { + const nameAndAgeBuilder = EntriesSpecBuilder.create({ + name: { + type: "string" + } + }).add("age", { type: "int" }); + + expect(nameAndAgeBuilder.build()).toEqual({ + name: { type: "string" }, + age: { type: "int" } + }); + + nameAndAgeBuilder satisfies EntriesSpecBuilder<{ + name: { type: "string" }; + age: { type: "int" }; + }>; + + const nameBuilder = nameAndAgeBuilder.pick(["name"]); + nameBuilder satisfies EntriesSpecBuilder<{ + name: { type: "string" }; + }>; + // @ts-expect-error age is not in the builder + nameBuilder satisfies EntriesSpecBuilder<{ + age: { type: "int" }; + }>; + + expect(nameBuilder.build()).toEqual({ + name: { type: "string" } + }); + + // @ts-expect-error nonExistingKey will not type-check, but we want to + // test the error for cases where the caller is not using TypeScript + expect(() => nameAndAgeBuilder.pick(["nonExistingKey"])).to.throw( + ReferenceError + ); + + expect(nameAndAgeBuilder.omit(["name"]).build()).toEqual({ + age: { type: "int" } + }); + + expect( + nameAndAgeBuilder.omit(["name"], { strict: false }).build() + ).toEqual({ + age: { type: "int" } + }); + + nameAndAgeBuilder.omit(["name"]) satisfies EntriesSpecBuilder<{ + age: { type: "int" }; + }>; + + // @ts-expect-error nonExistingKey will not type-check, but we want to + // test the error for cases where the caller is not using TypeScript + expect(() => nameAndAgeBuilder.omit(["nonExistingKey"])).to.throw( + ReferenceError + ); + + expect(() => + nameAndAgeBuilder.omit(["name"], { strict: false }) + ).not.to.throw(ReferenceError); + }); + }); +} diff --git a/packages/podspec/src/builders/entry.ts b/packages/podspec/src/builders/entry.ts new file mode 100644 index 0000000..eb64b3e --- /dev/null +++ b/packages/podspec/src/builders/entry.ts @@ -0,0 +1,54 @@ +import { POD_INT_MAX, POD_INT_MIN, type PODIntValue } from "@pcd/pod/podTypes"; +import type { IntEntrySpec } from "../types/entries.js"; +import { checkPODValue } from "@pcd/pod/podChecks"; + +function validateRange( + min: bigint, + max: bigint, + allowedMin: bigint, + allowedMax: bigint +) { + 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"); + } +} + +export class IntEntrySpecBuilder { + private readonly spec: T; + + constructor(spec: T) { + this.spec = spec; + } + + public inRange(min: bigint, max: bigint): IntEntrySpecBuilder { + validateRange(min, max, POD_INT_MIN, POD_INT_MAX); + return new IntEntrySpecBuilder({ + ...this.spec, + inRange: { min, max } + }); + } + + public isMemberOf(values: PODIntValue[]): IntEntrySpecBuilder { + for (const value of values) { + checkPODValue("", value); + } + return new IntEntrySpecBuilder({ + ...this.spec, + isMemberOf: values + }); + } + + public isNotMemberOf(values: PODIntValue[]): IntEntrySpecBuilder { + return new IntEntrySpecBuilder({ + ...this.spec, + isNotMemberOf: values + }); + } + + public build(): T { + return this.spec; + } +} diff --git a/packages/podspec/src/builders/group.ts b/packages/podspec/src/builders/group.ts new file mode 100644 index 0000000..4f3a179 --- /dev/null +++ b/packages/podspec/src/builders/group.ts @@ -0,0 +1,86 @@ +import type { + HasEntries, + PodEntryStringsOfType, + PodGroupSpec, + PodGroupSpecPods, + TupleSpec +} from "../types/group.js"; + +/** + * @todo + * - Add other constraints (equalsEntry, etc.) + * - Add the ability to split out a single POD + * - Add the ability to create a sub-group + * - Merge groups + * - Add some type parameter to keep track of tuples + */ + +type Concrete = T extends object ? { [K in keyof T]: T[K] } : T; + +export class PodGroupBuilder

{ + private readonly tuples: TupleSpec

[] = []; + private readonly constraints: unknown[] = []; + private readonly spec: PodGroupSpec

; + + constructor(private readonly pods: P) { + this.spec = { + pods: pods + }; + } + + public add( + key: K, + pod: T + ): PodGroupBuilder> { + return new PodGroupBuilder({ ...this.pods, [key]: pod } as Concrete< + P & { [PK in K]: T } + >); + } + + public tuple(tuple: TupleSpec

) { + this.tuples.push(tuple); + return this; + } + + public lessThan( + entryPair1: PodEntryStringsOfType, + entryPair2: PodEntryStringsOfType + ) { + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const [pod1, entry1] = entryPair1.split("."); + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const [pod2, entry2] = entryPair2.split("."); + return this; + } + + public build(): PodGroupSpec

{ + return this.spec; + } +} + +if (import.meta.vitest) { + const { it, expect } = import.meta.vitest; + it("can add a POD to the group", () => { + const group = new PodGroupBuilder({ + pod1: { + entries: { + foo: { type: "int" }, + bar: { type: "string" } + } + } + }) + .add("pod2", { + entries: { + foo: { type: "int" } + } + }) + .build(); + + expect(group.pods.pod1).toEqual({ + entries: { + foo: { type: "int" }, + bar: { type: "string" } + } + }); + }); +} diff --git a/packages/podspec/src/builders/groupv2.ts b/packages/podspec/src/builders/groupv2.ts new file mode 100644 index 0000000..723e5db --- /dev/null +++ b/packages/podspec/src/builders/groupv2.ts @@ -0,0 +1,103 @@ +import type { PODName } from "@pcd/pod"; +import type { EntryListSpec } from "../types/entries.js"; + +import { + PODSpecBuilderV2, + type ConstraintMap, + type PODSpecV2 +} from "./podv2.js"; + +type PODGroupPODs = Record>; + +// @TODO add group constraints, where instead of extending EntryListSpec, +// we have some kind of group entry list, with each entry name prefixed +// by the name of the POD it belongs to. + +// type GroupIsMemberOf = { +// entry: N[number]; +// type: "isMemberOf"; +// isMemberOf: N[number]; +// }; + +type PODGroupSpec< + PODs extends PODGroupPODs, + Constraints extends ConstraintMap +> = { + pods: PODs; + constraints: Constraints; +}; + +// type AddEntry< +// E extends EntryListSpec, +// K extends keyof E, +// V extends PODValueType +// > = Concrete; + +type Evaluate = T extends infer O ? { [K in keyof O]: O[K] } : never; + +type AddPOD< + PODs extends PODGroupPODs, + N extends PODName, + Spec extends PODSpecV2 +> = Evaluate<{ + [K in keyof PODs | N]: K extends N ? Spec : PODs[K & keyof PODs]; +}>; + +class PODGroupSpecBuilder< + PODs extends PODGroupPODs, + Constraints extends ConstraintMap +> { + #spec: PODGroupSpec; + + constructor(pods: PODs, constraints: Constraints) { + this.#spec = { + pods, + constraints + }; + } + + public static create(pods: PODs) { + return new PODGroupSpecBuilder(pods, {}); + } + + public spec(): PODGroupSpec { + return this.#spec; + } + + public pod< + N extends PODName, + Spec extends PODSpecV2, + NewPods extends AddPOD + >(name: N, spec: Spec): PODGroupSpecBuilder { + return new PODGroupSpecBuilder( + { ...this.#spec.pods, [name]: spec } as unknown as NewPods, + this.#spec.constraints + ); + } + + public isMemberOf( + name: N + ): PODGroupSpecBuilder { + return new PODGroupSpecBuilder(this.#spec.pods, { + ...this.#spec.constraints, + [name]: { type: "isMemberOf", isMemberOf: name } + }); + } +} + +if (import.meta.vitest) { + const { it, expect } = import.meta.vitest; + + it("PODGroupSpecBuilder", () => { + const group = PODGroupSpecBuilder.create({}); + const podBuilder = PODSpecBuilderV2.create().entry("my_string", "string"); + const group2 = group.pod("foo", podBuilder.spec()); + const spec = group2.spec(); + expect(group2.spec()).toEqual({ + pods: { + foo: podBuilder.spec() + }, + constraints: {} + }); + }); +} diff --git a/packages/podspec/src/builders/pod.ts b/packages/podspec/src/builders/pod.ts new file mode 100644 index 0000000..129f0a5 --- /dev/null +++ b/packages/podspec/src/builders/pod.ts @@ -0,0 +1,168 @@ +import type { EntryListSpec } from "../types/entries.js"; +import type { PODSpec, PODTupleSpec, PODTuplesSpec } from "../types/pod.js"; + +export class PODSpecBuilder { + readonly #spec: PODSpec; + + private constructor(spec: PODSpec) { + this.#spec = spec; + } + + public static create(entries: E): PODSpecBuilder { + return new PODSpecBuilder({ entries, tuples: {} as PODTuplesSpec }); + } + + /** + * Add a tuple constraint to the schema + */ + tuple( + name: N, + tuple: { entries: [...K] } & Omit, "entries"> + ): PODSpecBuilder { + if (name in this.#spec.tuples) { + throw new ReferenceError( + `Tuple ${name.toString()} already exists: ${Object.keys( + this.#spec.tuples + ).join(", ")}` + ); + } + return new PODSpecBuilder({ + ...this.#spec, + tuples: { + ...this.#spec.tuples, + [name]: tuple + } as PODTuplesSpec + }); + } + + /** + * Pick tuples by name + * + * @todo Make the names type-safe for better DX + */ + pickTuples(names: string[]) { + return new PODSpecBuilder({ + ...this.#spec, + tuples: Object.fromEntries( + Object.entries(this.#spec.tuples).filter(([name]) => + names.includes(name) + ) + ) + }); + } + + /** + * Omit tuples by name + * + * @todo Make the names type-safe for better DX + */ + omitTuples(names: string[]) { + return new PODSpecBuilder({ + ...this.#spec, + tuples: Object.fromEntries( + Object.entries(this.#spec.tuples).filter( + ([name]) => !names.includes(name) + ) + ) + }); + } + + /** + * Configure signer public key constraints + */ + signerPublicKey(config: { + isRevealed?: boolean; + isMemberOf?: string[]; + isNotMemberOf?: string[]; + }): PODSpecBuilder { + return new PODSpecBuilder({ + ...this.#spec, + signerPublicKey: config + }); + } + + /** + * Configure signature constraints + */ + signature(config: { + isMemberOf?: string[]; + isNotMemberOf?: string[]; + }): PODSpecBuilder { + return new PODSpecBuilder({ + ...this.#spec, + signature: config + }); + } + + /** + * Set metadata for the schema + */ + meta(config: { labelEntry: keyof E & string }): PODSpecBuilder { + return new PODSpecBuilder({ + ...this.#spec, + meta: config + }); + } + + /** + * Build and return the final POD schema + */ + build(): PODSpec { + return structuredClone(this.#spec); + } + + /** + * Pick entries by key + */ + pick(keys: K[]): PODSpecBuilder> { + // Remove tuples whose keys are not picked + const tuples = Object.fromEntries( + Object.entries(this.#spec.tuples).filter(([_key, tuple]) => + tuple.entries.every( + (entry) => entry === "$signerPublicKey" || keys.includes(entry as K) + ) + ) + ); + + // If the labelEntry is picked, keep it, otherwise remove it + const meta = this.#spec.meta?.labelEntry + ? keys.includes(this.#spec.meta.labelEntry as K) + ? this.#spec.meta + : undefined + : undefined; + + return new PODSpecBuilder({ + ...this.#spec, + entries: Object.fromEntries( + keys.map((k) => [k, this.#spec.entries[k]]) + ) as Pick, + tuples, + meta + } as PODSpec>); + } +} + +if (import.meta.vitest) { + const { it, expect } = import.meta.vitest; + it("add", () => { + const pod = PODSpecBuilder.create({ + name: { type: "string" }, + age: { type: "int" } + }).tuple("foo", { + entries: ["name"], + isMemberOf: [[{ type: "string", value: "foo" }]] + }); + + const output = pod.build(); + + expect(output).toEqual({ + entries: { name: { type: "string" }, age: { type: "int" } }, + tuples: { + foo: { + entries: ["name"], + isMemberOf: [[{ type: "string", value: "foo" }]] + } + } + }); + }); +} diff --git a/packages/podspec/src/builders/podv2.ts b/packages/podspec/src/builders/podv2.ts new file mode 100644 index 0000000..df7abed --- /dev/null +++ b/packages/podspec/src/builders/podv2.ts @@ -0,0 +1,769 @@ +import { + POD_CRYPTOGRAPHIC_MAX, + POD_CRYPTOGRAPHIC_MIN, + POD_DATE_MAX, + POD_DATE_MIN, + POD_INT_MAX, + POD_INT_MIN, + type PODName, + type PODValue +} from "@pcd/pod"; +import type { EntryListSpec, EntrySpec } from "../types/entries.js"; +import type { PODValueType } from "../types/utils.js"; +import { validateRange } from "./shared.js"; + +/** + @todo + - [ ] add lessThan, greaterThan, lessThanEq, greaterThanEq + - [ ] add omit + - [ ] maybe add pick/omit for constraints? + - [x] add signerPublicKey support + - [ ] add constraints on signature + - [x] add contentID virtual entry + - [ ] refactor types + - [ ] rename away from v2 suffix + - [ ] validate entry names + - [ ] validate constraint parameters + - [ ] switch to using value types rather than PODValues (everywhere?) + - [ ] change `create` parameter to not take a PODEntryListSpec (maybe it should take nothing) + */ + +const virtualEntryNames = new Set([ + "$contentID", + "$signature", + "$signerPublicKey" +]); + +type VirtualEntries = { + $contentID: { type: "string" }; + $signature: { type: "string" }; + $signerPublicKey: { type: "eddsa_pubkey" }; +}; + +export type PODSpecV2 = { + entries: E; + constraints: C; +}; + +type EntryTypes = Record; + +type EntryKeys = (keyof E & string)[]; + +type PODValueTupleForNamedEntries< + E extends EntryListSpec, + Names extends EntryKeys +> = { + [K in keyof Names]: PODValueTypeForEntry; +}; + +type PODValueTypeFromTypeName = Extract< + PODValue, + { type: T } +>; + +type PODValueTypeForEntry = E["type"] extends PODValueType + ? PODValueTypeFromTypeName + : never; + +// type EntryType = E["type"]; +// type NamedEntry = E[N]; +// type EntryOfType> = E extends { +// [P in keyof E]: { type: T }; +// } +// ? E +// : never; + +type EntriesOfType = { + [P in keyof E as E[P] extends { type: T } ? P & string : never]: E[P]; +}; + +type IsMemberOf> = { + entries: N; + type: "isMemberOf"; + isMemberOf: PODValueTupleForNamedEntries[]; +}; + +type IsNotMemberOf> = { + entries: N; + type: "isNotMemberOf"; + isNotMemberOf: PODValueTupleForNamedEntries[]; +}; + +type SupportsRangeChecks = "int" | "cryptographic" | "date"; + +type InRange< + E extends EntryListSpec, + N extends keyof EntriesOfType +> = { + entry: N; + type: "inRange"; + inRange: { + min: E[N]["type"] extends "date" ? Date : bigint; + max: E[N]["type"] extends "date" ? Date : bigint; + }; +}; + +type NotInRange< + E extends EntryListSpec, + N extends keyof EntriesOfType +> = { + entry: N; + type: "notInRange"; + notInRange: { + min: E[N]["type"] extends "date" ? Date : bigint; + max: E[N]["type"] extends "date" ? Date : bigint; + }; +}; + +type EqualsEntry< + E extends EntryListSpec, + N1 extends keyof E, + N2 extends keyof E +> = E[N2]["type"] extends E[N1]["type"] + ? { + entry: N1; + type: "equalsEntry"; + equalsEntry: N2; + } + : never; + +type NotEqualsEntry< + E extends EntryListSpec, + N1 extends keyof E, + N2 extends keyof E +> = E[N2]["type"] extends E[N1]["type"] + ? { + entry: N1; + type: "notEqualsEntry"; + notEqualsEntry: N2; + } + : never; + +type Constraints = + | IsMemberOf + | IsNotMemberOf + | InRange + | NotInRange + | EqualsEntry + | NotEqualsEntry; + +/** + * Given a list of entry names, return the names of the entries that are not in the list + */ +type OmittedEntryNames = Exclude< + keyof E, + N[number] +>; + +type NonOverlappingConstraints = { + [K in keyof C as C[K] extends + | IsMemberOf + | IsNotMemberOf + ? Entries[number] extends N[number] + ? K + : never + : C[K] extends InRange + ? Entry extends N[number] + ? K + : never + : C[K] extends NotInRange + ? Entry extends N[number] + ? K + : never + : C[K] extends EqualsEntry + ? [Entry1, Entry2][number] extends N[number] + ? K + : never + : C[K] extends NotEqualsEntry + ? [Entry1, Entry2][number] extends N[number] + ? K + : never + : never]: C[K]; +}; + +type Concrete = T extends object ? { [K in keyof T]: T[K] } : T; + +type AddEntry< + E extends EntryListSpec, + K extends keyof E, + V extends PODValueType +> = Concrete; + +// Utility types for constraint naming +type JoinWithUnderscore = T extends readonly [ + infer F extends string, + ...infer R extends string[] +] + ? R["length"] extends 0 + ? F + : `${F}_${JoinWithUnderscore}` + : never; + +type BaseConstraintName< + N extends readonly string[], + C extends Constraints["type"] +> = `${JoinWithUnderscore}_${C}`; + +type NextAvailableSuffix< + Base extends string, + C extends ConstraintMap +> = Base extends keyof C + ? `${Base}_1` extends keyof C + ? `${Base}_2` extends keyof C + ? `${Base}_3` + : `${Base}_2` + : `${Base}_1` + : Base; + +type ConstraintName< + N extends readonly string[], + C extends Constraints["type"], + Map extends ConstraintMap +> = NextAvailableSuffix, Map>; + +// Base constraint map +export type ConstraintMap = Record; + +export class PODSpecBuilderV2< + E extends EntryListSpec, + // eslint-disable-next-line @typescript-eslint/no-empty-object-type + C extends ConstraintMap = {} +> { + readonly #spec: PODSpecV2; + + private constructor(spec: PODSpecV2) { + this.#spec = spec; + } + + public static create() { + return new PODSpecBuilderV2({ + entries: {}, + constraints: {} + }); + } + + public spec(): PODSpecV2 { + return structuredClone(this.#spec); + } + + public entry< + K extends string, + V extends PODValueType, + NewEntries extends AddEntry + >(key: Exclude, type: V): PODSpecBuilderV2 { + // @todo handle existing entries? + return new PODSpecBuilderV2({ + ...this.#spec, + entries: { + ...this.#spec.entries, + [key]: { type } + } as NewEntries, + constraints: this.#spec.constraints + }); + } + + /** + * Pick entries by key + */ + public pick( + keys: K[] + ): PODSpecBuilderV2, Concrete>> { + return new PODSpecBuilderV2({ + entries: Object.fromEntries( + Object.entries(this.#spec.entries).filter(([key]) => + keys.includes(key as K) + ) + ) as Pick, + constraints: Object.fromEntries( + Object.entries(this.#spec.constraints).filter(([_key, constraint]) => { + if (constraint.type === "isMemberOf") { + return (constraint.entries as EntryKeys).every((entry) => + keys.includes(entry as K) + ); + } else if (constraint.type === "inRange") { + return keys.includes(constraint.entry as K); + } + return false; + }) + ) as Concrete> + }); + } + + /** + * 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 PODSpecBuilderV2 with the constraint added + */ + public isMemberOf>( + names: [...N], + values: N["length"] extends 1 + ? PODValueTypeForEntry< + (E & VirtualEntries)[N[0] & keyof (E & VirtualEntries)] + >[] + : PODValueTupleForNamedEntries[] + ): PODSpecBuilderV2< + E, + C & { [K in ConstraintName]: IsMemberOf } + > { + // Check that all names exist in entries + for (const name of names) { + if (!(name in this.#spec.entries) && !virtualEntryNames.has(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 constraint: IsMemberOf = { + entries: names, + type: "isMemberOf", + // Wrap single values in arrays to match the expected tuple format + isMemberOf: (names.length === 1 + ? // @todo handle virtual entries + (values as PODValueTypeForEntry[]).map((v) => [v]) + : values) as PODValueTupleForNamedEntries[] + }; + + const baseName = `${names.join("_")}_isMemberOf`; + let constraintName = baseName; + let suffix = 1; + + while (constraintName in this.#spec.constraints) { + constraintName = `${baseName}_${suffix++}`; + } + + return new PODSpecBuilderV2({ + ...this.#spec, + constraints: { + ...this.#spec.constraints, + [constraintName]: constraint + } + }); + } + + /** + * 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 PODSpecBuilderV2 with the constraint added + */ + public isNotMemberOf>( + names: [...N], + values: N["length"] extends 1 + ? PODValueTypeForEntry[] + : PODValueTupleForNamedEntries[] + ): PODSpecBuilderV2< + E, + C & { [K in ConstraintName]: IsNotMemberOf } + > { + // Check that all names exist in entries + for (const name of names) { + if (!(name in this.#spec.entries)) { + 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 constraint: IsNotMemberOf = { + entries: names, + type: "isNotMemberOf", + // Wrap single values in arrays to match the expected tuple format + isNotMemberOf: (names.length === 1 + ? (values as PODValueTypeForEntry[]).map((v) => [v]) + : values) as PODValueTupleForNamedEntries[] + }; + + const baseName = `${names.join("_")}_isNotMemberOf`; + let constraintName = baseName; + let suffix = 1; + + while (constraintName in this.#spec.constraints) { + constraintName = `${baseName}_${suffix++}`; + } + + return new PODSpecBuilderV2({ + ...this.#spec, + constraints: { + ...this.#spec.constraints, + [constraintName]: constraint + } + }); + } + + /** + * 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 PODSpecBuilderV2 with the constraint added + */ + public inRange< + N extends keyof EntriesOfType & string + >( + name: N, + range: { + min: E[N]["type"] extends "date" ? Date : bigint; + max: E[N]["type"] extends "date" ? Date : bigint; + } + ): PODSpecBuilderV2< + E, + C & { [K in ConstraintName<[N & string], "inRange", C>]: InRange } + > { + // Check that the entry exists + if (!(name in this.#spec.entries) && !virtualEntryNames.has(name)) { + throw new Error(`Entry "${name}" does not exist`); + } + + const entryType = this.#spec.entries[name]?.type; + + if (entryType === "int") { + validateRange( + range.min as bigint, + range.max as bigint, + POD_INT_MIN, + POD_INT_MAX + ); + } else if (entryType === "cryptographic") { + validateRange( + range.min as bigint, + range.max as bigint, + POD_CRYPTOGRAPHIC_MIN, + POD_CRYPTOGRAPHIC_MAX + ); + } else if (entryType === "date") { + validateRange( + range.min as Date, + range.max as Date, + POD_DATE_MIN, + POD_DATE_MAX + ); + } + + const constraint: InRange = { + entry: name, + type: "inRange", + inRange: range + }; + + const baseName = `${name}_inRange`; + let constraintName = baseName; + let suffix = 1; + + while (constraintName in this.#spec.constraints) { + constraintName = `${baseName}_${suffix++}`; + } + + return new PODSpecBuilderV2({ + ...this.#spec, + constraints: { + ...this.#spec.constraints, + [constraintName]: constraint + } + }); + } + + /** + * 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 PODSpecBuilderV2 with the constraint added + */ + public notInRange< + N extends keyof EntriesOfType & string + >( + name: N, + range: { + min: E[N]["type"] extends "date" ? Date : bigint; + max: E[N]["type"] extends "date" ? Date : bigint; + } + ): PODSpecBuilderV2< + E, + C & { + [K in ConstraintName<[N & string], "notInRange", C>]: NotInRange; + } + > { + // Check that the entry exists + if (!(name in this.#spec.entries)) { + throw new Error(`Entry "${name}" does not exist`); + } + + const entryType = this.#spec.entries[name]?.type; + + if (entryType === "int") { + validateRange( + range.min as bigint, + range.max as bigint, + POD_INT_MIN, + POD_INT_MAX + ); + } else if (entryType === "cryptographic") { + validateRange( + range.min as bigint, + range.max as bigint, + POD_CRYPTOGRAPHIC_MIN, + POD_CRYPTOGRAPHIC_MAX + ); + } else if (entryType === "date") { + validateRange( + range.min as Date, + range.max as Date, + POD_DATE_MIN, + POD_DATE_MAX + ); + } + + const constraint: NotInRange = { + entry: name, + type: "notInRange", + notInRange: range + }; + + const baseName = `${name}_notInRange`; + let constraintName = baseName; + let suffix = 1; + + while (constraintName in this.#spec.constraints) { + constraintName = `${baseName}_${suffix++}`; + } + + return new PODSpecBuilderV2({ + ...this.#spec, + constraints: { + ...this.#spec.constraints, + [constraintName]: constraint + } + }); + } + + public equalsEntry( + name1: N1, + name2: E[N2]["type"] extends E[N1]["type"] ? N2 : never + ): PODSpecBuilderV2< + E, + C & { + [K in ConstraintName<[N1, N2], "equalsEntry", C>]: EqualsEntry; + } + > { + // Check that both names exist in entries + if (!(name1 in this.#spec.entries)) { + throw new Error(`Entry "${name1}" does not exist`); + } + if (!(name2 in this.#spec.entries)) { + throw new Error(`Entry "${name2}" does not exist`); + } + if ((name1 as string) === (name2 as string)) { + throw new Error("Entry names must be different"); + } + if (this.#spec.entries[name1]?.type !== this.#spec.entries[name2]?.type) { + throw new Error("Entry types must be the same"); + } + + const constraint = { + entry: name1, + type: "equalsEntry", + equalsEntry: name2 + // We know that the types are compatible, so we can cast to the correct type + } as unknown as EqualsEntry; + + const baseName = `${name1}_${name2}_equalsEntry`; + let constraintName = baseName; + let suffix = 1; + + while (constraintName in this.#spec.constraints) { + constraintName = `${baseName}_${suffix++}`; + } + + return new PODSpecBuilderV2({ + ...this.#spec, + constraints: { + ...this.#spec.constraints, + [constraintName]: constraint + } + }); + } + + public notEqualsEntry< + N1 extends keyof E & string, + N2 extends keyof E & string + >( + name1: N1, + name2: E[N2]["type"] extends E[N1]["type"] ? N2 : never + ): PODSpecBuilderV2< + E, + C & { + [K in ConstraintName<[N1, N2], "notEqualsEntry", C>]: NotEqualsEntry< + E, + N1, + N2 + >; + } + > { + // Check that both names exist in entries + if (!(name1 in this.#spec.entries)) { + throw new Error(`Entry "${name1}" does not exist`); + } + if (!(name2 in this.#spec.entries)) { + throw new Error(`Entry "${name2}" does not exist`); + } + if ((name1 as string) === (name2 as string)) { + throw new Error("Entry names must be different"); + } + if (this.#spec.entries[name1]?.type !== this.#spec.entries[name2]?.type) { + throw new Error("Entry types must be the same"); + } + + const constraint = { + entry: name1, + type: "notEqualsEntry", + notEqualsEntry: name2 + // We know that the types are compatible, so we can cast to the correct type + } as unknown as NotEqualsEntry; + + const baseName = `${name1}_${name2}_notEqualsEntry`; + let constraintName = baseName; + let suffix = 1; + + while (constraintName in this.#spec.constraints) { + constraintName = `${baseName}_${suffix++}`; + } + + return new PODSpecBuilderV2({ + ...this.#spec, + constraints: { + ...this.#spec.constraints, + [constraintName]: constraint + } + }); + } +} + +if (import.meta.vitest) { + const { it, expect } = import.meta.vitest; + it("PODSpecBuilderV2", () => { + const a = PODSpecBuilderV2.create(); + const b = a.entry("a", "string").entry("b", "int"); + expect(b.spec().entries).toEqual({ + a: { type: "string" }, + b: { type: "int" } + }); + + const c = b.isMemberOf(["a"], [{ type: "string", value: "foo" }]); + expect(c.spec().constraints).toEqual({ + a_isMemberOf: { + entries: ["a"], + type: "isMemberOf", + isMemberOf: [[{ type: "string", value: "foo" }]] + } + }); + + const d = c.inRange("b", { min: 10n, max: 100n }); + expect(d.spec().constraints).toEqual({ + a_isMemberOf: { + entries: ["a"], + type: "isMemberOf", + isMemberOf: [[{ type: "string", value: "foo" }]] + }, + b_inRange: { + entry: "b", + type: "inRange", + inRange: { min: 10n, max: 100n } + } + }); + + const e = d.isMemberOf( + ["a", "b"], + [ + [ + { type: "string", value: "foo" }, + { type: "int", value: 10n } + ] + ] + ); + expect(e.spec().constraints.a_b_isMemberOf.entries).toEqual(["a", "b"]); + + const f = e.pick(["b"]); + expect(f.spec().constraints).toEqual({ + b_inRange: { + entry: "b", + type: "inRange", + inRange: { min: 10n, max: 100n } + } + }); + + const g = e.entry("new", "string").equalsEntry("a", "new"); + const _GEntries = g.spec().entries; + type EntriesType = typeof _GEntries; + g.spec().constraints.a_new_equalsEntry satisfies EqualsEntry< + EntriesType, + "a", + "new" + >; + + expect(g.spec().constraints).toMatchObject({ + a_new_equalsEntry: { + entry: "a", + type: "equalsEntry", + equalsEntry: "new" + } + }); + }); +} + +// Example entry list spec +type TestEntries = { + a: { type: "string" }; + b: { type: "int" }; + c: { type: "int" }; +}; + +// Example constraint map +type TestConstraints = { + a_isMemberOf: IsMemberOf; + b_inRange: InRange; + ac_isMemberOf: IsMemberOf; +}; + +// Let's test picking just 'a' and 'b' +type PickedKeys = ["b"]; + +// First, let's see what OmittedEntryNames gives us +type TestOmitted = OmittedEntryNames; +// Should be: "c" + +// Now let's test NonOverlapping +type TestNonOverlapping = NonOverlappingConstraints< + TestConstraints, + PickedKeys +>; + +// Let's see what we get when picking just 'a' +type TestPickA = NonOverlappingConstraints; diff --git a/packages/podspec/src/builders/shared.ts b/packages/podspec/src/builders/shared.ts new file mode 100644 index 0000000..54b4244 --- /dev/null +++ b/packages/podspec/src/builders/shared.ts @@ -0,0 +1,13 @@ +export function validateRange( + min: bigint | Date, + max: bigint | Date, + allowedMin: bigint | Date, + allowedMax: bigint | Date +) { + 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"); + } +} diff --git a/packages/podspec/src/group.ts b/packages/podspec/src/group.ts new file mode 100644 index 0000000..a789be1 --- /dev/null +++ b/packages/podspec/src/group.ts @@ -0,0 +1,118 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ +/* eslint-disable @typescript-eslint/array-type */ +import type { PODValue } from "@pcd/pod"; +import type { PodSpec } from "./parse/pod.js"; + +/* +@todo +- [ ] rebuild group builder along the same line as podv2 +*/ + +// This describes the minimal shape we need +type HasSchemaEntries = { + schema: { + entries: T; + }; +}; + +// Get the keys from a type, excluding any index signature +type LiteralKeys = keyof { + [K in keyof T as string extends K ? never : K]: T[K]; +}; + +// Create a union type that maintains the relationship between pod and its entries +type PodEntryPair

= { + [K in keyof P]: P[K] extends { schema: { entries: infer E } } + ? `${K & string}.${LiteralKeys & string}` + : never; +}[keyof P]; + +// Helper to create fixed-length array type +type FixedLengthArray< + T, + N extends number, + R extends T[] = [] +> = R["length"] extends N ? R : FixedLengthArray; + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +type Length = T["length"]; + +type TupleSpec[]]> = { + entries: E; + isMemberOf?: { [K in keyof E]: PODValue }[]; + isNotMemberOf?: { [K in keyof E]: PODValue }[]; +}; + +export type PodSpecGroupSchema< + P, + T extends ReadonlyArray[]]>> +> = { + pods: P; + tuples: T; +}; + +// Debug types +type TestPodSpec1 = PodSpec<{ foo: { type: "string" } }>; +type TestPodSpec2 = PodSpec<{ bar: { type: "string" } }>; +type TestPods = { + p1: TestPodSpec1; + p2: TestPodSpec2; +}; + +/// Expand a type recursively +type ExpandRecursively = T extends object + ? T extends infer O + ? { [K in keyof O]: ExpandRecursively } + : never + : T; + +// The class inherits the same constraint +export class PodSpecGroup< + P extends Record, + T extends readonly TupleSpec[]]>[] +> { + readonly specs: PodSpecGroupSchema; + + constructor(schema: PodSpecGroupSchema) { + this.specs = schema; + } + + get(key: K): P[K] { + return this.specs.pods[key]; + } +} +// Debug concrete PodEntryPair +type DebugPodEntryPair = ExpandRecursively>; +// This will show: "p1.foo" | "p2.bar" + +// Debug concrete PodSpecGroupSchema +type DebugPodSpecGroupSchema = ExpandRecursively< + PodSpecGroupSchema]> +>; +type DebugPodSpecTuples = ExpandRecursively; +// This will show the full structure: +// { +// pods: TestPods; +// tuples?: { +// entries: PodEntryPair[]; +// isMemberOf?: PODValue[][]; +// isNotMemberOf?: PODValue[][]; +// }[]; +// } + +type DebugTupleSpec = TupleSpec< + TestPods, + readonly [PodEntryPair, PodEntryPair] +>; + +// Debug types to understand length inference +type TestEntries = ["p1.foo", "p2.bar"]; +type TestEntriesLength = Length; // should be 2 +type TestFixedArray = FixedLengthArray; + +// Debug what happens in PodSpecGroupSchema +type TestGroupSchema = PodSpecGroupSchema< + TestPods, + [TupleSpec] +>; +type TestGroupSchemaTuples = ExpandRecursively; diff --git a/packages/podspec/src/index.ts b/packages/podspec/src/index.ts index 187f4d4..aab6be7 100644 --- a/packages/podspec/src/index.ts +++ b/packages/podspec/src/index.ts @@ -9,7 +9,7 @@ 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 { pod, merge } from "./parse/pod.js"; import type { EntriesSchema } from "./schemas/entries.js"; import type { PODSchema } from "./schemas/pod.js"; import type { @@ -24,6 +24,7 @@ export { pod, proofRequest, podToPODData, + merge, type EntriesOutputType, type EntriesSchema, type EntriesSpec, diff --git a/packages/podspec/src/parse/pod.ts b/packages/podspec/src/parse/pod.ts index e3b79f0..acb7ba6 100644 --- a/packages/podspec/src/parse/pod.ts +++ b/packages/podspec/src/parse/pod.ts @@ -22,6 +22,9 @@ import { import type { ParseResult } from "./parse_utils.js"; import { FAILURE, SUCCESS, safeCheckTuple } from "./parse_utils.js"; +const invalid = Symbol("invalid"); +type Invalid = { [invalid]: T }; + export interface PODData { entries: PODEntries; signature: string; @@ -231,6 +234,58 @@ export class PodSpec { } } +/** + * Merges two PodSpecs, combining their schemas. + * The resulting PodSpec will only accept PODs that satisfy both specs. + * + * @param spec1 The first PodSpec to merge + * @param spec2 The second PodSpec to merge + * @returns A new PodSpec that combines both schemas + * @throws {Error} If the specs have overlapping entries or conflicting constraints + */ +export function merge< + const E extends EntriesSchema, + const F extends EntriesSchema +>( + spec1: NoOverlappingEntries extends never + ? PodSpec & Invalid<"Cannot merge PodSpecs with overlapping entries"> + : PodSpec, + spec2: PodSpec +): PodSpec { + // Runtime checks for constraints that complement the type checks + const entriesOverlap = Object.keys(spec1.schema.entries).some( + (key) => key in spec2.schema.entries + ); + if (entriesOverlap) { + throw new Error("Cannot merge PodSpecs with overlapping entries"); + } + + if (spec1.schema.signature && spec2.schema.signature) { + throw new Error( + "Cannot merge PodSpecs that both have signature constraints" + ); + } + + if (spec1.schema.signerPublicKey && spec2.schema.signerPublicKey) { + throw new Error( + "Cannot merge PodSpecs that both have signerPublicKey constraints" + ); + } + + const mergedSchema: PODSchema = { + entries: { + ...spec1.schema.entries, + ...spec2.schema.entries + }, + signature: spec1.schema.signature ?? spec2.schema.signature, + signerPublicKey: + spec1.schema.signerPublicKey ?? spec2.schema.signerPublicKey, + tuples: [...(spec1.schema.tuples ?? []), ...(spec2.schema.tuples ?? [])] + }; + + return PodSpec.create(mergedSchema); +} + /** * Exported version of static create method, for convenience. */ @@ -368,3 +423,12 @@ export function safeParsePod( data as StrongPOD> ); } + +/** Check if two types share any keys */ +type HasOverlap = keyof T & keyof U extends never ? false : true; + +/** Ensure two schemas don't have overlapping entries */ +type NoOverlappingEntries< + E1 extends EntriesSchema, + E2 extends EntriesSchema +> = HasOverlap extends true ? never : E1 & E2; 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/query.ts b/packages/podspec/src/processors/query.ts new file mode 100644 index 0000000..e69de29 diff --git a/packages/podspec/src/processors/validate.ts b/packages/podspec/src/processors/validate.ts new file mode 100644 index 0000000..e69de29 diff --git a/packages/podspec/src/schemas/entry.ts b/packages/podspec/src/schemas/entry.ts index a8a98d2..c536b2c 100644 --- a/packages/podspec/src/schemas/entry.ts +++ b/packages/podspec/src/schemas/entry.ts @@ -1,11 +1,95 @@ -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"; +import type { + PODName, + PODBytesValue, + PODCryptographicValue, + PODDateValue, + PODEdDSAPublicKeyValue, + PODIntValue, + PODBooleanValue, + PODNullValue, + PODStringValue +} from "@pcd/pod"; + +/** + * Schema for a PODStringValue. + */ +export interface StringSchema { + type: "string"; + isMemberOf?: PODStringValue[]; + isNotMemberOf?: PODStringValue[]; + equalsEntry?: PODName; +} + +/** + * Schema for a PODBytesValue. + */ +export interface BytesSchema { + type: "bytes"; + isMemberOf?: PODBytesValue[]; + isNotMemberOf?: PODBytesValue[]; + equalsEntry?: PODName; +} + +/** + * Schema for a PODIntValue. + */ +export interface IntSchema { + type: "int"; + isMemberOf?: PODIntValue[]; + isNotMemberOf?: PODIntValue[]; + equalsEntry?: PODName; + inRange?: { min: bigint; max: bigint }; +} + +/** + * Schema for a PODCryptographicValue. + */ +export interface CryptographicSchema { + type: "cryptographic"; + isMemberOf?: PODCryptographicValue[]; + isNotMemberOf?: PODCryptographicValue[]; + equalsEntry?: PODName; + inRange?: { min: bigint; max: bigint }; + isOwnerID?: boolean; +} + +/** + * Schema for a PODEdDSAPublicKeyValue. + */ +export interface EdDSAPublicKeySchema { + type: "eddsa_pubkey"; + isMemberOf?: PODEdDSAPublicKeyValue[]; + isNotMemberOf?: PODEdDSAPublicKeyValue[]; + equalsEntry?: PODName; +} + +/** + * Schema for a PODBooleanValue. + */ +export interface BooleanSchema { + type: "boolean"; + isMemberOf?: PODBooleanValue[]; + isNotMemberOf?: PODBooleanValue[]; +} + +/** + * Schema for a PODDateValue. + */ +export interface DateSchema { + type: "date"; + isMemberOf?: PODDateValue[]; + isNotMemberOf?: PODDateValue[]; + equalsEntry?: PODName; +} + +/** + * Schema for a PODNullValue. + */ +export interface NullSchema { + type: "null"; + isMemberOf?: PODNullValue[]; + isNotMemberOf?: PODNullValue[]; +} /** * Union of schemas for non-optional entries. diff --git a/packages/podspec/src/schemas/string.ts b/packages/podspec/src/schemas/string.ts index d9364b1..3bf9ed7 100644 --- a/packages/podspec/src/schemas/string.ts +++ b/packages/podspec/src/schemas/string.ts @@ -15,7 +15,7 @@ export interface StringSchema { } /** - * Checks if the given input is a PODEdDSAPublicKeyValue. + * Checks if the given input is a PODStringValue. * @param data - The input to check. * @returns A ParseResult wrapping the value */ diff --git a/packages/podspec/src/types/entries.ts b/packages/podspec/src/types/entries.ts new file mode 100644 index 0000000..06391b5 --- /dev/null +++ b/packages/podspec/src/types/entries.ts @@ -0,0 +1,98 @@ +import type { + PODBytesValue, + PODCryptographicValue, + PODDateValue, + PODEdDSAPublicKeyValue, + PODIntValue, + PODBooleanValue, + PODNullValue, + PODStringValue, + PODName +} from "@pcd/pod"; + +export interface StringEntrySpec { + type: "string"; + isMemberOf?: PODStringValue[]; + isNotMemberOf?: PODStringValue[]; +} + +export interface BytesEntrySpec { + type: "bytes"; + isMemberOf?: PODBytesValue[]; + isNotMemberOf?: PODBytesValue[]; +} + +export interface IntEntrySpec { + type: "int"; + isMemberOf?: PODIntValue[]; + isNotMemberOf?: PODIntValue[]; + inRange?: { min: bigint; max: bigint }; +} + +export interface CryptographicEntrySpec { + type: "cryptographic"; + isMemberOf?: PODCryptographicValue[]; + isNotMemberOf?: PODCryptographicValue[]; + inRange?: { min: bigint; max: bigint }; + isOwnerID?: "SemaphoreV3"; // @todo constant from GPC? +} + +export interface EdDSAPublicKeyEntrySpec { + type: "eddsa_pubkey"; + isMemberOf?: PODEdDSAPublicKeyValue[]; + isNotMemberOf?: PODEdDSAPublicKeyValue[]; + isOwnerID?: "SemaphoreV4"; // @todo constant from GPC? +} + +export interface BooleanEntrySpec { + type: "boolean"; + isMemberOf?: PODBooleanValue[]; + isNotMemberOf?: PODBooleanValue[]; +} + +export interface DateEntrySpec { + type: "date"; + isMemberOf?: PODDateValue[]; + isNotMemberOf?: PODDateValue[]; +} + +export interface NullEntrySpec { + type: "null"; + isMemberOf?: PODNullValue[]; + isNotMemberOf?: PODNullValue[]; +} + +/** + * Union of non-optional entries. + */ +export type DefinedEntrySpec = + | StringEntrySpec + | CryptographicEntrySpec + | IntEntrySpec + | EdDSAPublicKeyEntrySpec + | BooleanEntrySpec + | BytesEntrySpec + | DateEntrySpec + | NullEntrySpec; + +/** + * Optional entry wrapper. + */ +export interface OptionalEntrySpec { + type: "optional"; + innerType: DefinedEntrySpec; +} + +/** + * Union of all entry types. + */ +export type EntrySpec = DefinedEntrySpec | OptionalEntrySpec; + +/** + * Spec for a PODEntries object - simply a keyed collection of EntrySpecs. + */ +export type EntryListSpec = Readonly>; + +export type EntriesSpec = { + entries: E; +}; diff --git a/packages/podspec/src/types/group.ts b/packages/podspec/src/types/group.ts new file mode 100644 index 0000000..a60dd4b --- /dev/null +++ b/packages/podspec/src/types/group.ts @@ -0,0 +1,116 @@ +import type { PODName, PODValue } from "@pcd/pod"; +import type { EntryListSpec } from "./entries.js"; +import type { PODValueType } from "./utils.js"; + +export type HasEntries = { + entries: T; +}; + +export type PodEntryStrings

> = { + [K in keyof P]: { + [E in keyof P[K]["entries"]]: `${K & string}.${E & string}`; + }[keyof P[K]["entries"]]; +}[keyof P]; + +export type TupleSpec< + P extends Record, + E extends readonly PodEntryStrings

[] = readonly PodEntryStrings

[] +> = { + entries: E; + isMemberOf?: [{ [K in keyof E]: PODValue }[number]][]; + isNotMemberOf?: [{ [K in keyof E]: PODValue }[number]][]; +}; + +export type PodGroupSpec

> = { + pods: P & Record; +}; + +export type PodGroupSpecPods< + P extends PodGroupSpec> = PodGroupSpec< + Record + > +> = P["pods"]; + +// Get the keys from a type, excluding any index signature +type LiteralKeys = keyof { + [K in keyof T as string extends K ? never : K]: T[K]; +}; + +// Create a union type that maintains the relationship between pod and its entries +type PodEntryPair

= { + [K in keyof P]: P[K] extends { schema: { entries: infer E } } + ? `${K & string}.${LiteralKeys & string}` + : never; +}[keyof P]; + +// eslint-disable-next-line @typescript-eslint/no-unused-vars +type PodEntryNames

> = { + [K in keyof P]: (keyof P[K]["entries"] & string)[]; +}; + +export type PodEntryStringsOfType< + P extends Record, + T extends PODValueType +> = { + [K in keyof P]: { + [E in keyof P[K]["entries"]]: P[K]["entries"][E]["type"] extends T + ? `${K & string}.${E & string}` + : never; + }[keyof P[K]["entries"]]; +}[keyof P]; + +// Add this helper type +type AssertEqual = [T] extends [U] + ? [U] extends [T] + ? true + : false + : false; + +type _TestPodEntryStrings = PodEntryStrings<{ + pod1: { entries: { foo: { type: "string" } } }; + pod2: { entries: { foo: { type: "int" }; bar: { type: "string" } } }; +}>; + +// This creates a type error when the condition is false +type Assert = T; +type IsEqual = (() => T extends A ? 1 : 2) extends () => T extends B + ? 1 + : 2 + ? true + : false; + +// This should fail compilation if types don't match +type _Test = Assert< + IsEqual<_TestPodEntryStrings, "pod1.foo" | "pod2.foo" | "pod2.bar"> +>; + +// If you want to see an error when types don't match: +type _TestShouldFail = AssertEqual<_TestPodEntryStrings, "wrong_type">; + +type _TestPodEntryStringsOfType = PodEntryStringsOfType< + { + pod1: { entries: { foo: { type: "int" } } }; + pod2: { entries: { foo: { type: "int" }; bar: { type: "string" } } }; + }, + "int" +>; // "pod1.foo" | "pod2.foo" + +// Split a PodEntryPair into its pod and entry components +type SplitPodEntry = T extends `${infer Pod}.${infer Entry}` + ? { pod: Pod; entry: Entry } + : never; + +// Get the type of an entry given a pod and entry name +// eslint-disable-next-line @typescript-eslint/no-unused-vars +type EntryType< + P extends Record, + PE extends PodEntryPair

+> = SplitPodEntry extends { pod: infer Pod } + ? Pod extends keyof P + ? SplitPodEntry extends { entry: infer Entry } + ? Entry extends keyof P[Pod]["entries"] + ? P[Pod]["entries"][Entry]["type"] + : never + : never + : never + : never; diff --git a/packages/podspec/src/types/pod.ts b/packages/podspec/src/types/pod.ts new file mode 100644 index 0000000..26c11fc --- /dev/null +++ b/packages/podspec/src/types/pod.ts @@ -0,0 +1,62 @@ +import type { PODName, PODValue } from "@pcd/pod"; +import type { EntryListSpec } from "./entries.js"; + +type PODValueTypeForEntry< + E extends EntryListSpec, + K extends keyof E +> = E[K]["type"]; + +export type EntryNamesForPOD = readonly [ + keyof (E & { + $signerPublicKey: never; + }) & + PODName, + ...(keyof (E & { + $signerPublicKey: never; + }) & + PODName)[] +]; + +/** + * Schema for validating a POD. + */ +export type PODTupleSpec< + E extends EntryListSpec, + Names extends readonly (keyof E)[] = (keyof E)[] +> = { + entries: Names; + isMemberOf?: { + [I in keyof Names]: Extract< + PODValue, + { type: PODValueTypeForEntry } + >; + }[]; + isNotMemberOf?: { + [I in keyof Names]: Extract< + PODValue, + { type: PODValueTypeForEntry } + >; + }[]; +}; + +export type PODTuplesSpec = Record< + string, + PODTupleSpec> +>; + +export type PODSpec = { + entries: E; + tuples: PODTuplesSpec; + signerPublicKey?: { + isRevealed?: boolean; + isMemberOf?: string[]; + isNotMemberOf?: string[]; + }; + signature?: { + isMemberOf?: string[]; + isNotMemberOf?: string[]; + }; + meta?: { + labelEntry: keyof E & PODName; + }; +}; diff --git a/packages/podspec/src/types/utils.ts b/packages/podspec/src/types/utils.ts new file mode 100644 index 0000000..80a7e8d --- /dev/null +++ b/packages/podspec/src/types/utils.ts @@ -0,0 +1,3 @@ +import type { PODValue } from "@pcd/pod"; + +export type PODValueType = PODValue["type"]; diff --git a/packages/podspec/test/podspec.spec.ts b/packages/podspec/test/podspec.spec.ts index 9a3f0f7..f70d113 100644 --- a/packages/podspec/test/podspec.spec.ts +++ b/packages/podspec/test/podspec.spec.ts @@ -14,6 +14,8 @@ 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"; +import { merge } from "../src/index.js"; +import { PodSpecGroup } from "../src/group.js"; describe("podspec should work", function () { it("should validate POD entries", () => { @@ -814,4 +816,81 @@ describe("podspec should work", function () { ); assert(result); }); + + it("can merge pod specs", async function () { + const p1 = p.pod({ + entries: { + foo: { type: "string" }, + bar: { type: "int" } + } + }); + + const p2 = p.pod({ + entries: { + baz: { type: "string" }, + quux: { type: "int" } + } + }); + + const p3 = merge(p1, p2); + + assert(p3.schema.entries.foo === p1.schema.entries.foo); + assert(p3.schema.entries.bar === p1.schema.entries.bar); + assert(p3.schema.entries.baz === p2.schema.entries.baz); + assert(p3.schema.entries.quux === p2.schema.entries.quux); + + const result = p3.safeParseEntries( + { + foo: "test", + bar: 5n, + baz: "test", + quux: 10n + }, + { + coerce: true + } + ); + + assert(result.isValid); + assert(result.value.foo.type === "string"); + assert(result.value.foo.value === "test"); + assert(result.value.bar.type === "int"); + assert(result.value.bar.value === 5n); + assert(result.value.baz.type === "string"); + assert(result.value.baz.value === "test"); + assert(result.value.quux.type === "int"); + assert(result.value.quux.value === 10n); + }); + + it("group stuff", async function () { + const p1 = p.pod({ + entries: { foo: { type: "string" } } + }); + const p2 = p.pod({ + entries: { bar: { type: "string" } } + }); + const p3 = p.pod({ + entries: { baz: { type: "string" } } + }); + const group = new PodSpecGroup({ + pods: { p1, p2, p3 }, + tuples: [ + { + entries: ["p1.foo", "p2.bar"], + isMemberOf: [ + [ + { type: "string", value: "test" }, + { type: "string", value: "test" }, + { type: "string", value: "test" } + ] + ] + } + ] + }); + + const p1got = group.get("p1"); + assert(p1got !== undefined); + assert(p1got.schema.entries.foo !== undefined); + assert(p1got.schema.entries.foo.type === "string"); + }); }); diff --git a/packages/podspec/test/scratch.ts b/packages/podspec/test/scratch.ts new file mode 100644 index 0000000..e69de29 diff --git a/packages/podspec/test/serialization.spec.ts b/packages/podspec/test/serialization.spec.ts index fb8c0dd..a05bd34 100644 --- a/packages/podspec/test/serialization.spec.ts +++ b/packages/podspec/test/serialization.spec.ts @@ -49,7 +49,6 @@ describe("should be able to serialize outputs", function () { }); const pr = prs.getProofRequest(); - console.dir(pr, { depth: null }); expect(() => proofConfigToJSON(pr.proofConfig)).to.not.throw; expect(() => podMembershipListsToJSON(pr.membershipLists)).to.not.throw; 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..1d39906 --- /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..a6b5390 --- /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..7b92091 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: @@ -457,9 +457,49 @@ importers: uuid: specifier: ^9.0.0 version: 9.0.1 + vitest: + 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) + + packages/podspec3: + dependencies: + '@pcd/gpc': + specifier: ^0.4.0 + version: 0.4.0(typescript@5.6.2) + '@pcd/pod': + specifier: ^0.5.0 + version: 0.5.0 + devDependencies: + '@parcnet-js/eslint-config': + specifier: workspace:* + version: link:../eslint-config + '@parcnet-js/typescript-config': + specifier: workspace:* + version: link:../typescript-config + '@pcd/proto-pod-gpc-artifacts': + specifier: ^0.11.0 + version: 0.11.0 + '@semaphore-protocol/identity': + specifier: ^3.15.2 + version: 3.15.2 + '@types/uuid': + specifier: ^9.0.0 + version: 9.0.8 + '@zk-kit/eddsa-poseidon': + specifier: ^1.0.3 + version: 1.0.4 + tsup: + specifier: ^8.2.4 + version: 8.2.4(jiti@1.21.6)(postcss@8.4.44)(tsx@4.19.0)(typescript@5.6.2)(yaml@2.5.1) + typescript: + specifier: ^5.5 + version: 5.6.2 + uuid: + 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) + version: 2.1.2(@types/node@22.5.5)(sass@1.80.3)(terser@5.34.1) packages/ticket-spec: dependencies: @@ -2010,6 +2050,9 @@ 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/mocker@2.1.1': resolution: {integrity: sha512-LNN5VwOEdJqCmJ/2XJBywB11DLlkbY0ooDJW3uRX5cZyYCrc4PI/ePX0iQhE3BiEGiQmK4GE7Q/PqCkkaiPnrA==} peerDependencies: @@ -2034,6 +2077,17 @@ 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/pretty-format@2.0.5': resolution: {integrity: sha512-h8k+1oWHfwTkyTkb9egzwNMfJAEx4veaPSnMeKbVSjp4euqGSbQlm5+6VHwTr7u4FJslVVsUG5nopCaAYdOmSQ==} @@ -2043,6 +2097,9 @@ 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/runner@2.0.5': resolution: {integrity: sha512-TfRfZa6Bkk9ky4tW0z20WKXFEwwvWhRY+84CnSEtq4+3ZvDlJyY32oNTJtM7AW9ihW90tX/1Q78cb6FjoAs+ig==} @@ -2052,6 +2109,9 @@ packages: '@vitest/runner@2.1.2': resolution: {integrity: sha512-UCsPtvluHO3u7jdoONGjOSil+uON5SSvU9buQh3lP7GgUXHp78guN1wRmZDX4wGK6J10f9NUtP6pO+SFquoMlw==} + '@vitest/runner@3.0.4': + resolution: {integrity: sha512-dKHzTQ7n9sExAcWH/0sh1elVgwc7OJ2lMOBrAm73J7AH6Pf9T12Zh3lNE1TETZaqrWFXtLlx3NVrLRb5hCK+iw==} + '@vitest/snapshot@2.0.5': resolution: {integrity: sha512-SgCPUeDFLaM0mIUHfaArq8fD2WbaXG/zVXjRupthYfYGzc8ztbFbu6dUNOblBG7XLMR1kEhS/DNnfCZ2IhdDew==} @@ -2061,6 +2121,9 @@ 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/spy@2.0.5': resolution: {integrity: sha512-c/jdthAhvJdpfVuaexSrnawxZz6pywlTPe84LUB2m/4t3rl2fTo9NFGBG4oWgaD+FTgDDV8hJ/nibT7IfH3JfA==} @@ -2070,6 +2133,9 @@ 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/ui@2.1.1': resolution: {integrity: sha512-IIxo2LkQDA+1TZdPLYPclzsXukBWd5dX2CKpGqH8CCt8Wh0ZuDn4+vuQ9qlppEju6/igDGzjWF/zyorfsf+nHg==} peerDependencies: @@ -2084,6 +2150,9 @@ packages: '@vitest/utils@2.1.2': resolution: {integrity: sha512-zMO2KdYy6mx56btx9JvAqAZ6EyS3g49krMPPrgOp1yxGZiA93HumGk+bZ5jIZtOg5/VBYl5eBmGRQHqq4FG6uQ==} + '@vitest/utils@3.0.4': + resolution: {integrity: sha512-8BqC1ksYsHtbWH+DfpOAKrFw3jl3Uf9J7yeFh85Pz52IWuh1hBBtyfEbRNNZNjl8H8A5yMLH9/t+k7HIKzQcZQ==} + '@volar/kit@2.4.5': resolution: {integrity: sha512-ZzyErW5UiDfiIuJ/lpqc2Kx5PHDGDZ/bPlPJYpRcxlrn8Z8aDhRlsLHkNKcNiH65TmNahk2kbLaiejiqu6BD3A==} peerDependencies: @@ -2466,6 +2535,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'} @@ -2702,6 +2775,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==} @@ -2868,6 +2950,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 +3170,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==} @@ -3841,6 +3930,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 +3953,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==} @@ -4383,6 +4478,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'} @@ -4989,6 +5087,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'} @@ -5161,6 +5262,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 +5273,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'} @@ -5381,6 +5493,11 @@ packages: engines: {node: '>=14.17'} hasBin: true + typescript@5.7.2: + resolution: {integrity: sha512-i5t66RHxDvVN40HfDd1PsEThGNnlMCMT3jMUuoh9/0TaqWevNontacunWyN02LA9/fIbEWlcHZcgTKb9QoaLfg==} + engines: {node: '>=14.17'} + hasBin: true + uc.micro@2.1.0: resolution: {integrity: sha512-ARDJmphmdvUk6Glw7y9DQ2bFkKBHwQHLi2lsaH6PPmz/Ka9sFOBsBluozhDltWmnv9u/cF6Rt87znRTPV+yp/A==} @@ -5519,6 +5636,11 @@ 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-plugin-node-polyfills@0.22.0: resolution: {integrity: sha512-F+G3LjiGbG8QpbH9bZ//GSBr9i1InSTkaulfUHFa9jkLqVGORFBoqc2A/Yu5Mmh1kNAbiAeKeK+6aaQUf3x0JA==} peerDependencies: @@ -5638,6 +5760,34 @@ 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 + vm-browserify@1.1.2: resolution: {integrity: sha512-2ham8XPWTONajOR0ohOKOHXkm3+gaBmGut3SRuu75xLd/RRaY6vqgh8NBYYk7+RW3u5AtzPQZG8F10LHkl0lAQ==} @@ -7602,6 +7752,13 @@ 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/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 +7775,14 @@ 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/pretty-format@2.0.5': dependencies: tinyrainbow: 1.2.0 @@ -7630,6 +7795,10 @@ snapshots: dependencies: tinyrainbow: 1.2.0 + '@vitest/pretty-format@3.0.4': + dependencies: + tinyrainbow: 2.0.0 + '@vitest/runner@2.0.5': dependencies: '@vitest/utils': 2.0.5 @@ -7645,6 +7814,11 @@ 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/snapshot@2.0.5': dependencies: '@vitest/pretty-format': 2.0.5 @@ -7663,6 +7837,12 @@ 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/spy@2.0.5': dependencies: tinyspy: 3.0.2 @@ -7675,6 +7855,10 @@ snapshots: dependencies: tinyspy: 3.0.2 + '@vitest/spy@3.0.4': + dependencies: + tinyspy: 3.0.2 + '@vitest/ui@2.1.1(vitest@2.1.1)': dependencies: '@vitest/utils': 2.1.1 @@ -7706,6 +7890,12 @@ 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 + '@volar/kit@2.4.5(typescript@5.6.2)': dependencies: '@volar/language-service': 2.4.5 @@ -8268,6 +8458,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.1 + pathval: 2.0.0 + chalk@2.4.2: dependencies: ansi-styles: 3.2.1 @@ -8503,6 +8701,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 @@ -8701,6 +8903,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 +9228,8 @@ snapshots: expand-template@2.0.3: {} + expect-type@1.1.0: {} + expressive-code@0.35.6: dependencies: '@expressive-code/core': 0.35.6 @@ -9759,7 +9965,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 +9982,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) @@ -9846,6 +10052,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 +10075,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 @@ -10699,6 +10911,8 @@ snapshots: pathe@1.1.2: {} + pathe@2.0.2: {} + pathval@2.0.0: {} pbkdf2@3.1.2: @@ -11444,6 +11658,8 @@ snapshots: std-env@3.7.0: {} + std-env@3.8.0: {} + stdin-discarder@0.2.2: {} stream-browserify@3.0.0: @@ -11720,6 +11936,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 +11946,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: @@ -12006,6 +12228,8 @@ snapshots: typescript@5.6.2: {} + typescript@5.7.2: {} + uc.micro@2.1.0: {} unbox-primitive@1.0.2: @@ -12203,6 +12427,24 @@ 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-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 +12589,42 @@ 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 + vm-browserify@1.1.2: {} volar-service-css@0.0.61(@volar/language-service@2.4.5): 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/*"]; From c1dfb86d7b4eddeee5255c2693638cf0472539bc Mon Sep 17 00:00:00 2001 From: Rob Knight Date: Tue, 28 Jan 2025 08:02:59 +0100 Subject: [PATCH 02/20] Rename constraints -> statements --- packages/podspec/src/builders/groupv2.ts | 37 ++- packages/podspec/src/builders/podv2.ts | 320 +++++++++++------------ packages/podspec/src/builders/shared.ts | 38 +++ 3 files changed, 213 insertions(+), 182 deletions(-) diff --git a/packages/podspec/src/builders/groupv2.ts b/packages/podspec/src/builders/groupv2.ts index 723e5db..8476e63 100644 --- a/packages/podspec/src/builders/groupv2.ts +++ b/packages/podspec/src/builders/groupv2.ts @@ -1,13 +1,12 @@ import type { PODName } from "@pcd/pod"; -import type { EntryListSpec } from "../types/entries.js"; - import { PODSpecBuilderV2, - type ConstraintMap, - type PODSpecV2 + type StatementMap, + type PODSpecV2, + type EntryTypes } from "./podv2.js"; -type PODGroupPODs = Record>; +type PODGroupPODs = Record>; // @TODO add group constraints, where instead of extending EntryListSpec, // we have some kind of group entry list, with each entry name prefixed @@ -21,10 +20,10 @@ type PODGroupPODs = Record>; type PODGroupSpec< PODs extends PODGroupPODs, - Constraints extends ConstraintMap + Statements extends StatementMap > = { pods: PODs; - constraints: Constraints; + statements: Statements; }; // type AddEntry< @@ -38,21 +37,21 @@ type Evaluate = T extends infer O ? { [K in keyof O]: O[K] } : never; type AddPOD< PODs extends PODGroupPODs, N extends PODName, - Spec extends PODSpecV2 + Spec extends PODSpecV2 > = Evaluate<{ [K in keyof PODs | N]: K extends N ? Spec : PODs[K & keyof PODs]; }>; class PODGroupSpecBuilder< PODs extends PODGroupPODs, - Constraints extends ConstraintMap + Statements extends StatementMap > { - #spec: PODGroupSpec; + #spec: PODGroupSpec; - constructor(pods: PODs, constraints: Constraints) { + constructor(pods: PODs, statements: Statements) { this.#spec = { pods, - constraints + statements }; } @@ -60,26 +59,26 @@ class PODGroupSpecBuilder< return new PODGroupSpecBuilder(pods, {}); } - public spec(): PODGroupSpec { + public spec(): PODGroupSpec { return this.#spec; } public pod< N extends PODName, - Spec extends PODSpecV2, + Spec extends PODSpecV2, NewPods extends AddPOD - >(name: N, spec: Spec): PODGroupSpecBuilder { + >(name: N, spec: Spec): PODGroupSpecBuilder { return new PODGroupSpecBuilder( { ...this.#spec.pods, [name]: spec } as unknown as NewPods, - this.#spec.constraints + this.#spec.statements ); } public isMemberOf( name: N - ): PODGroupSpecBuilder { + ): PODGroupSpecBuilder { return new PODGroupSpecBuilder(this.#spec.pods, { - ...this.#spec.constraints, + ...this.#spec.statements, [name]: { type: "isMemberOf", isMemberOf: name } }); } @@ -97,7 +96,7 @@ if (import.meta.vitest) { pods: { foo: podBuilder.spec() }, - constraints: {} + statements: {} }); }); } diff --git a/packages/podspec/src/builders/podv2.ts b/packages/podspec/src/builders/podv2.ts index df7abed..0ae358b 100644 --- a/packages/podspec/src/builders/podv2.ts +++ b/packages/podspec/src/builders/podv2.ts @@ -8,7 +8,6 @@ import { type PODName, type PODValue } from "@pcd/pod"; -import type { EntryListSpec, EntrySpec } from "../types/entries.js"; import type { PODValueType } from "../types/utils.js"; import { validateRange } from "./shared.js"; @@ -24,15 +23,14 @@ import { validateRange } from "./shared.js"; - [ ] rename away from v2 suffix - [ ] validate entry names - [ ] validate constraint parameters - - [ ] switch to using value types rather than PODValues (everywhere?) - - [ ] change `create` parameter to not take a PODEntryListSpec (maybe it should take nothing) + - [ ] switch to using value types rather than PODValues (everywhere? maybe not membership lists) */ -const virtualEntryNames = new Set([ - "$contentID", - "$signature", - "$signerPublicKey" -]); +const virtualEntries: VirtualEntries = { + $contentID: { type: "string" }, + $signature: { type: "string" }, + $signerPublicKey: { type: "eddsa_pubkey" } +}; type VirtualEntries = { $contentID: { type: "string" }; @@ -40,20 +38,20 @@ type VirtualEntries = { $signerPublicKey: { type: "eddsa_pubkey" }; }; -export type PODSpecV2 = { +export type PODSpecV2 = { entries: E; - constraints: C; + statements: S; }; -type EntryTypes = Record; +export type EntryTypes = Record; -type EntryKeys = (keyof E & string)[]; +export type EntryKeys = (keyof E & string)[]; -type PODValueTupleForNamedEntries< - E extends EntryListSpec, +export type PODValueTupleForNamedEntries< + E extends EntryTypes, Names extends EntryKeys > = { - [K in keyof Names]: PODValueTypeForEntry; + [K in keyof Names]: PODValueTypeFromTypeName; }; type PODValueTypeFromTypeName = Extract< @@ -61,29 +59,24 @@ type PODValueTypeFromTypeName = Extract< { type: T } >; -type PODValueTypeForEntry = E["type"] extends PODValueType - ? PODValueTypeFromTypeName - : never; - -// type EntryType = E["type"]; -// type NamedEntry = E[N]; -// type EntryOfType> = E extends { -// [P in keyof E]: { type: T }; -// } -// ? E -// : never; - -type EntriesOfType = { - [P in keyof E as E[P] extends { type: T } ? P & string : never]: E[P]; +type EntriesOfType = { + [P in keyof E as E[P] extends T ? P & string : never]: E[P]; }; -type IsMemberOf> = { +/** + * @TODO Consider not having the E type parameter here. + * We can practically constrain the entry names using the constraint method + * signature, and then store a lighter-weight type that just lists the entry + * names used, without keeping a reference to the entry type list. + */ + +type IsMemberOf> = { entries: N; type: "isMemberOf"; isMemberOf: PODValueTupleForNamedEntries[]; }; -type IsNotMemberOf> = { +type IsNotMemberOf> = { entries: N; type: "isNotMemberOf"; isNotMemberOf: PODValueTupleForNamedEntries[]; @@ -92,34 +85,34 @@ type IsNotMemberOf> = { type SupportsRangeChecks = "int" | "cryptographic" | "date"; type InRange< - E extends EntryListSpec, + E extends EntryTypes, N extends keyof EntriesOfType > = { entry: N; type: "inRange"; inRange: { - min: E[N]["type"] extends "date" ? Date : bigint; - max: E[N]["type"] extends "date" ? Date : bigint; + min: E[N] extends "date" ? Date : bigint; + max: E[N] extends "date" ? Date : bigint; }; }; type NotInRange< - E extends EntryListSpec, + E extends EntryTypes, N extends keyof EntriesOfType > = { entry: N; type: "notInRange"; notInRange: { - min: E[N]["type"] extends "date" ? Date : bigint; - max: E[N]["type"] extends "date" ? Date : bigint; + min: E[N] extends "date" ? Date : bigint; + max: E[N] extends "date" ? Date : bigint; }; }; type EqualsEntry< - E extends EntryListSpec, + E extends EntryTypes, N1 extends keyof E, N2 extends keyof E -> = E[N2]["type"] extends E[N1]["type"] +> = E[N2] extends E[N1] ? { entry: N1; type: "equalsEntry"; @@ -128,10 +121,10 @@ type EqualsEntry< : never; type NotEqualsEntry< - E extends EntryListSpec, + E extends EntryTypes, N1 extends keyof E, N2 extends keyof E -> = E[N2]["type"] extends E[N1]["type"] +> = E[N2] extends E[N1] ? { entry: N1; type: "notEqualsEntry"; @@ -139,7 +132,7 @@ type NotEqualsEntry< } : never; -type Constraints = +type Statements = | IsMemberOf | IsNotMemberOf | InRange @@ -150,46 +143,46 @@ type Constraints = /** * Given a list of entry names, return the names of the entries that are not in the list */ -type OmittedEntryNames = Exclude< +type OmittedEntryNames = Exclude< keyof E, N[number] >; -type NonOverlappingConstraints = { - [K in keyof C as C[K] extends +type NonOverlappingStatements = { + [K in keyof S as S[K] extends | IsMemberOf | IsNotMemberOf ? Entries[number] extends N[number] ? K : never - : C[K] extends InRange + : S[K] extends InRange ? Entry extends N[number] ? K : never - : C[K] extends NotInRange + : S[K] extends NotInRange ? Entry extends N[number] ? K : never - : C[K] extends EqualsEntry + : S[K] extends EqualsEntry ? [Entry1, Entry2][number] extends N[number] ? K : never - : C[K] extends NotEqualsEntry + : S[K] extends NotEqualsEntry ? [Entry1, Entry2][number] extends N[number] ? K : never - : never]: C[K]; + : never]: S[K]; }; type Concrete = T extends object ? { [K in keyof T]: T[K] } : T; type AddEntry< - E extends EntryListSpec, + E extends EntryTypes, K extends keyof E, V extends PODValueType -> = Concrete; +> = Concrete; -// Utility types for constraint naming +// Utility types for statement naming type JoinWithUnderscore = T extends readonly [ infer F extends string, ...infer R extends string[] @@ -199,50 +192,50 @@ type JoinWithUnderscore = T extends readonly [ : `${F}_${JoinWithUnderscore}` : never; -type BaseConstraintName< +type BaseStatementName< N extends readonly string[], - C extends Constraints["type"] -> = `${JoinWithUnderscore}_${C}`; + S extends Statements["type"] +> = `${JoinWithUnderscore}_${S}`; type NextAvailableSuffix< Base extends string, - C extends ConstraintMap -> = Base extends keyof C - ? `${Base}_1` extends keyof C - ? `${Base}_2` extends keyof C + S extends StatementMap +> = Base extends keyof S + ? `${Base}_1` extends keyof S + ? `${Base}_2` extends keyof S ? `${Base}_3` : `${Base}_2` : `${Base}_1` : Base; -type ConstraintName< +type StatementName< N extends readonly string[], - C extends Constraints["type"], - Map extends ConstraintMap -> = NextAvailableSuffix, Map>; + S extends Statements["type"], + Map extends StatementMap +> = NextAvailableSuffix, Map>; // Base constraint map -export type ConstraintMap = Record; +export type StatementMap = Record; export class PODSpecBuilderV2< - E extends EntryListSpec, + E extends EntryTypes, // eslint-disable-next-line @typescript-eslint/no-empty-object-type - C extends ConstraintMap = {} + S extends StatementMap = {} > { - readonly #spec: PODSpecV2; + readonly #spec: PODSpecV2; - private constructor(spec: PODSpecV2) { + private constructor(spec: PODSpecV2) { this.#spec = spec; } public static create() { return new PODSpecBuilderV2({ entries: {}, - constraints: {} + statements: {} }); } - public spec(): PODSpecV2 { + public spec(): PODSpecV2 { return structuredClone(this.#spec); } @@ -250,15 +243,15 @@ export class PODSpecBuilderV2< K extends string, V extends PODValueType, NewEntries extends AddEntry - >(key: Exclude, type: V): PODSpecBuilderV2 { + >(key: Exclude, type: V): PODSpecBuilderV2 { // @todo handle existing entries? return new PODSpecBuilderV2({ ...this.#spec, entries: { ...this.#spec.entries, - [key]: { type } + [key]: type } as NewEntries, - constraints: this.#spec.constraints + statements: this.#spec.statements }); } @@ -267,25 +260,25 @@ export class PODSpecBuilderV2< */ public pick( keys: K[] - ): PODSpecBuilderV2, Concrete>> { + ): PODSpecBuilderV2, Concrete>> { return new PODSpecBuilderV2({ entries: Object.fromEntries( Object.entries(this.#spec.entries).filter(([key]) => keys.includes(key as K) ) ) as Pick, - constraints: Object.fromEntries( - Object.entries(this.#spec.constraints).filter(([_key, constraint]) => { - if (constraint.type === "isMemberOf") { - return (constraint.entries as EntryKeys).every((entry) => + statements: Object.fromEntries( + Object.entries(this.#spec.statements).filter(([_key, statement]) => { + if (statement.type === "isMemberOf") { + return (statement.entries as EntryKeys).every((entry) => keys.includes(entry as K) ); - } else if (constraint.type === "inRange") { - return keys.includes(constraint.entry as K); + } else if (statement.type === "inRange") { + return keys.includes(statement.entry as K); } return false; }) - ) as Concrete> + ) as Concrete> }); } @@ -307,17 +300,17 @@ export class PODSpecBuilderV2< public isMemberOf>( names: [...N], values: N["length"] extends 1 - ? PODValueTypeForEntry< + ? PODValueTypeFromTypeName< (E & VirtualEntries)[N[0] & keyof (E & VirtualEntries)] >[] : PODValueTupleForNamedEntries[] ): PODSpecBuilderV2< E, - C & { [K in ConstraintName]: IsMemberOf } + S & { [K in StatementName]: IsMemberOf } > { // Check that all names exist in entries for (const name of names) { - if (!(name in this.#spec.entries) && !virtualEntryNames.has(name)) { + if (!(name in this.#spec.entries) && !(name in virtualEntries)) { throw new Error(`Entry "${name}" does not exist`); } } @@ -328,29 +321,31 @@ export class PODSpecBuilderV2< throw new Error("Duplicate entry names are not allowed"); } - const constraint: IsMemberOf = { + const statement: IsMemberOf = { entries: names, type: "isMemberOf", // Wrap single values in arrays to match the expected tuple format isMemberOf: (names.length === 1 ? // @todo handle virtual entries - (values as PODValueTypeForEntry[]).map((v) => [v]) + (values as PODValueTypeFromTypeName[]).map((v) => [ + v + ]) : values) as PODValueTupleForNamedEntries[] }; const baseName = `${names.join("_")}_isMemberOf`; - let constraintName = baseName; + let statementName = baseName; let suffix = 1; - while (constraintName in this.#spec.constraints) { - constraintName = `${baseName}_${suffix++}`; + while (statementName in this.#spec.statements) { + statementName = `${baseName}_${suffix++}`; } return new PODSpecBuilderV2({ ...this.#spec, - constraints: { - ...this.#spec.constraints, - [constraintName]: constraint + statements: { + ...this.#spec.statements, + [statementName]: statement } }); } @@ -373,11 +368,11 @@ export class PODSpecBuilderV2< public isNotMemberOf>( names: [...N], values: N["length"] extends 1 - ? PODValueTypeForEntry[] + ? PODValueTypeFromTypeName[] : PODValueTupleForNamedEntries[] ): PODSpecBuilderV2< E, - C & { [K in ConstraintName]: IsNotMemberOf } + S & { [K in StatementName]: IsNotMemberOf } > { // Check that all names exist in entries for (const name of names) { @@ -392,28 +387,30 @@ export class PODSpecBuilderV2< throw new Error("Duplicate entry names are not allowed"); } - const constraint: IsNotMemberOf = { + const statement: IsNotMemberOf = { entries: names, type: "isNotMemberOf", // Wrap single values in arrays to match the expected tuple format isNotMemberOf: (names.length === 1 - ? (values as PODValueTypeForEntry[]).map((v) => [v]) + ? (values as PODValueTypeFromTypeName[]).map((v) => [ + v + ]) : values) as PODValueTupleForNamedEntries[] }; const baseName = `${names.join("_")}_isNotMemberOf`; - let constraintName = baseName; + let statementName = baseName; let suffix = 1; - while (constraintName in this.#spec.constraints) { - constraintName = `${baseName}_${suffix++}`; + while (statementName in this.#spec.statements) { + statementName = `${baseName}_${suffix++}`; } return new PODSpecBuilderV2({ ...this.#spec, - constraints: { - ...this.#spec.constraints, - [constraintName]: constraint + statements: { + ...this.#spec.statements, + [statementName]: statement } }); } @@ -430,19 +427,19 @@ export class PODSpecBuilderV2< >( name: N, range: { - min: E[N]["type"] extends "date" ? Date : bigint; - max: E[N]["type"] extends "date" ? Date : bigint; + min: E[N] extends "date" ? Date : bigint; + max: E[N] extends "date" ? Date : bigint; } ): PODSpecBuilderV2< E, - C & { [K in ConstraintName<[N & string], "inRange", C>]: InRange } + S & { [K in StatementName<[N & string], "inRange", S>]: InRange } > { // Check that the entry exists - if (!(name in this.#spec.entries) && !virtualEntryNames.has(name)) { + if (!(name in this.#spec.entries) && !(name in virtualEntries)) { throw new Error(`Entry "${name}" does not exist`); } - const entryType = this.#spec.entries[name]?.type; + const entryType = this.#spec.entries[name]; if (entryType === "int") { validateRange( @@ -467,25 +464,25 @@ export class PODSpecBuilderV2< ); } - const constraint: InRange = { + const statement: InRange = { entry: name, type: "inRange", inRange: range }; const baseName = `${name}_inRange`; - let constraintName = baseName; + let statementName = baseName; let suffix = 1; - while (constraintName in this.#spec.constraints) { - constraintName = `${baseName}_${suffix++}`; + while (statementName in this.#spec.statements) { + statementName = `${baseName}_${suffix++}`; } return new PODSpecBuilderV2({ ...this.#spec, - constraints: { - ...this.#spec.constraints, - [constraintName]: constraint + statements: { + ...this.#spec.statements, + [statementName]: statement } }); } @@ -502,21 +499,21 @@ export class PODSpecBuilderV2< >( name: N, range: { - min: E[N]["type"] extends "date" ? Date : bigint; - max: E[N]["type"] extends "date" ? Date : bigint; + min: E[N] extends "date" ? Date : bigint; + max: E[N] extends "date" ? Date : bigint; } ): PODSpecBuilderV2< E, - C & { - [K in ConstraintName<[N & string], "notInRange", C>]: NotInRange; + S & { + [K in StatementName<[N & string], "notInRange", S>]: NotInRange; } > { // Check that the entry exists - if (!(name in this.#spec.entries)) { + if (!(name in this.#spec.entries) && !(name in virtualEntries)) { throw new Error(`Entry "${name}" does not exist`); } - const entryType = this.#spec.entries[name]?.type; + const entryType = this.#spec.entries[name]; if (entryType === "int") { validateRange( @@ -541,36 +538,36 @@ export class PODSpecBuilderV2< ); } - const constraint: NotInRange = { + const statement: NotInRange = { entry: name, type: "notInRange", notInRange: range }; const baseName = `${name}_notInRange`; - let constraintName = baseName; + let statementName = baseName; let suffix = 1; - while (constraintName in this.#spec.constraints) { - constraintName = `${baseName}_${suffix++}`; + while (statementName in this.#spec.statements) { + statementName = `${baseName}_${suffix++}`; } return new PODSpecBuilderV2({ ...this.#spec, - constraints: { - ...this.#spec.constraints, - [constraintName]: constraint + statements: { + ...this.#spec.statements, + [statementName]: statement } }); } public equalsEntry( name1: N1, - name2: E[N2]["type"] extends E[N1]["type"] ? N2 : never + name2: E[N2] extends E[N1] ? N2 : never ): PODSpecBuilderV2< E, - C & { - [K in ConstraintName<[N1, N2], "equalsEntry", C>]: EqualsEntry; + S & { + [K in StatementName<[N1, N2], "equalsEntry", S>]: EqualsEntry; } > { // Check that both names exist in entries @@ -583,11 +580,11 @@ export class PODSpecBuilderV2< if ((name1 as string) === (name2 as string)) { throw new Error("Entry names must be different"); } - if (this.#spec.entries[name1]?.type !== this.#spec.entries[name2]?.type) { + if ((this.#spec.entries[name1] as string) !== this.#spec.entries[name2]) { throw new Error("Entry types must be the same"); } - const constraint = { + const statement = { entry: name1, type: "equalsEntry", equalsEntry: name2 @@ -595,18 +592,18 @@ export class PODSpecBuilderV2< } as unknown as EqualsEntry; const baseName = `${name1}_${name2}_equalsEntry`; - let constraintName = baseName; + let statementName = baseName; let suffix = 1; - while (constraintName in this.#spec.constraints) { - constraintName = `${baseName}_${suffix++}`; + while (statementName in this.#spec.statements) { + statementName = `${baseName}_${suffix++}`; } return new PODSpecBuilderV2({ ...this.#spec, - constraints: { - ...this.#spec.constraints, - [constraintName]: constraint + statements: { + ...this.#spec.statements, + [statementName]: statement } }); } @@ -616,11 +613,11 @@ export class PODSpecBuilderV2< N2 extends keyof E & string >( name1: N1, - name2: E[N2]["type"] extends E[N1]["type"] ? N2 : never + name2: E[N2] extends E[N1] ? N2 : never ): PODSpecBuilderV2< E, - C & { - [K in ConstraintName<[N1, N2], "notEqualsEntry", C>]: NotEqualsEntry< + S & { + [K in StatementName<[N1, N2], "notEqualsEntry", S>]: NotEqualsEntry< E, N1, N2 @@ -637,11 +634,11 @@ export class PODSpecBuilderV2< if ((name1 as string) === (name2 as string)) { throw new Error("Entry names must be different"); } - if (this.#spec.entries[name1]?.type !== this.#spec.entries[name2]?.type) { + if ((this.#spec.entries[name1] as string) !== this.#spec.entries[name2]) { throw new Error("Entry types must be the same"); } - const constraint = { + const statement = { entry: name1, type: "notEqualsEntry", notEqualsEntry: name2 @@ -649,18 +646,18 @@ export class PODSpecBuilderV2< } as unknown as NotEqualsEntry; const baseName = `${name1}_${name2}_notEqualsEntry`; - let constraintName = baseName; + let statementName = baseName; let suffix = 1; - while (constraintName in this.#spec.constraints) { - constraintName = `${baseName}_${suffix++}`; + while (statementName in this.#spec.statements) { + statementName = `${baseName}_${suffix++}`; } return new PODSpecBuilderV2({ ...this.#spec, - constraints: { - ...this.#spec.constraints, - [constraintName]: constraint + statements: { + ...this.#spec.statements, + [statementName]: statement } }); } @@ -677,7 +674,7 @@ if (import.meta.vitest) { }); const c = b.isMemberOf(["a"], [{ type: "string", value: "foo" }]); - expect(c.spec().constraints).toEqual({ + expect(c.spec().statements).toEqual({ a_isMemberOf: { entries: ["a"], type: "isMemberOf", @@ -686,7 +683,7 @@ if (import.meta.vitest) { }); const d = c.inRange("b", { min: 10n, max: 100n }); - expect(d.spec().constraints).toEqual({ + expect(d.spec().statements).toEqual({ a_isMemberOf: { entries: ["a"], type: "isMemberOf", @@ -708,10 +705,10 @@ if (import.meta.vitest) { ] ] ); - expect(e.spec().constraints.a_b_isMemberOf.entries).toEqual(["a", "b"]); + expect(e.spec().statements.a_b_isMemberOf.entries).toEqual(["a", "b"]); const f = e.pick(["b"]); - expect(f.spec().constraints).toEqual({ + expect(f.spec().statements).toEqual({ b_inRange: { entry: "b", type: "inRange", @@ -722,13 +719,13 @@ if (import.meta.vitest) { const g = e.entry("new", "string").equalsEntry("a", "new"); const _GEntries = g.spec().entries; type EntriesType = typeof _GEntries; - g.spec().constraints.a_new_equalsEntry satisfies EqualsEntry< + g.spec().statements.a_new_equalsEntry satisfies EqualsEntry< EntriesType, "a", "new" >; - expect(g.spec().constraints).toMatchObject({ + expect(g.spec().statements).toMatchObject({ a_new_equalsEntry: { entry: "a", type: "equalsEntry", @@ -745,8 +742,8 @@ type TestEntries = { c: { type: "int" }; }; -// Example constraint map -type TestConstraints = { +// Example statement map +type TestStatements = { a_isMemberOf: IsMemberOf; b_inRange: InRange; ac_isMemberOf: IsMemberOf; @@ -760,10 +757,7 @@ type TestOmitted = OmittedEntryNames; // Should be: "c" // Now let's test NonOverlapping -type TestNonOverlapping = NonOverlappingConstraints< - TestConstraints, - PickedKeys ->; +type TestNonOverlapping = NonOverlappingStatements; // Let's see what we get when picking just 'a' -type TestPickA = NonOverlappingConstraints; +type TestPickA = NonOverlappingStatements; diff --git a/packages/podspec/src/builders/shared.ts b/packages/podspec/src/builders/shared.ts index 54b4244..5da0689 100644 --- a/packages/podspec/src/builders/shared.ts +++ b/packages/podspec/src/builders/shared.ts @@ -1,3 +1,13 @@ +import type { + PODBooleanValue, + PODBytesValue, + PODCryptographicValue, + PODDateValue, + PODIntValue, + PODNullValue, + PODStringValue +} from "@pcd/pod"; + export function validateRange( min: bigint | Date, max: bigint | Date, @@ -11,3 +21,31 @@ export function validateRange( throw new RangeError("Value out of range"); } } + +export function toPODStringValue(value: string): PODStringValue { + return { type: "string", value }; +} + +export function toPODIntValue(value: bigint): PODIntValue { + return { type: "int", value }; +} + +export function toPODBooleanValue(value: boolean): PODBooleanValue { + return { type: "boolean", value }; +} + +export function toPODBytesValue(value: Uint8Array): PODBytesValue { + return { type: "bytes", value }; +} + +export function toPODCryptographicValue(value: bigint): PODCryptographicValue { + return { type: "cryptographic", value }; +} + +export function toPODDateValue(value: Date): PODDateValue { + return { type: "date", value }; +} + +export function toPODNullValue(): PODNullValue { + return { type: "null", value: null }; +} From f87c964482b1e6b76cc6c4b26e1c10da3e2a73ef Mon Sep 17 00:00:00 2001 From: Rob Knight Date: Tue, 28 Jan 2025 16:41:10 +0100 Subject: [PATCH 03/20] Try out validation --- packages/podspec/src/builders/group.ts | 174 ++-- packages/podspec/src/builders/groupv2.ts | 102 --- packages/podspec/src/builders/pod.ts | 895 +++++++++++++++++--- packages/podspec/src/builders/podv2.ts | 763 ----------------- packages/podspec/src/builders/shared.ts | 81 +- packages/podspec/src/processors/validate.ts | 143 ++++ 6 files changed, 1087 insertions(+), 1071 deletions(-) delete mode 100644 packages/podspec/src/builders/groupv2.ts delete mode 100644 packages/podspec/src/builders/podv2.ts diff --git a/packages/podspec/src/builders/group.ts b/packages/podspec/src/builders/group.ts index 4f3a179..f2a1e14 100644 --- a/packages/podspec/src/builders/group.ts +++ b/packages/podspec/src/builders/group.ts @@ -1,86 +1,120 @@ -import type { - HasEntries, - PodEntryStringsOfType, - PodGroupSpec, - PodGroupSpecPods, - TupleSpec -} from "../types/group.js"; - -/** - * @todo - * - Add other constraints (equalsEntry, etc.) - * - Add the ability to split out a single POD - * - Add the ability to create a sub-group - * - Merge groups - * - Add some type parameter to keep track of tuples - */ - -type Concrete = T extends object ? { [K in keyof T]: T[K] } : T; - -export class PodGroupBuilder

{ - private readonly tuples: TupleSpec

[] = []; - private readonly constraints: unknown[] = []; - private readonly spec: PodGroupSpec

; - - constructor(private readonly pods: P) { - this.spec = { - pods: pods - }; +import { checkPODName, type PODName } from "@pcd/pod"; +import { + PODSpecBuilder, + type StatementMap, + type PODSpec, + type EntryTypes +} from "./pod.js"; + +type PODGroupPODs = Record>; + +// @TODO add group constraints, where instead of extending EntryListSpec, +// we have some kind of group entry list, with each entry name prefixed +// by the name of the POD it belongs to. + +// type GroupIsMemberOf = { +// entry: N[number]; +// type: "isMemberOf"; +// isMemberOf: N[number]; +// }; + +type PODGroupSpec< + PODs extends PODGroupPODs, + Statements extends StatementMap +> = { + pods: PODs; + statements: Statements; +}; + +// type AddEntry< +// E extends EntryListSpec, +// K extends keyof E, +// V extends PODValueType +// > = Concrete; + +type Evaluate = T extends infer O ? { [K in keyof O]: O[K] } : never; + +type AddPOD< + PODs extends PODGroupPODs, + N extends PODName, + Spec extends PODSpec +> = Evaluate<{ + [K in keyof PODs | N]: K extends N ? Spec : PODs[K & keyof PODs]; +}>; + +class PODGroupSpecBuilder< + PODs extends PODGroupPODs, + Statements extends StatementMap +> { + readonly #spec: PODGroupSpec; + static #isInternalConstructing = false; + + private constructor(spec: PODGroupSpec) { + if (PODGroupSpecBuilder.#isInternalConstructing) { + throw new Error("PODGroupSpecBuilder is not constructable"); + } + PODGroupSpecBuilder.#isInternalConstructing = false; + this.#spec = spec; } - public add( - key: K, - pod: T - ): PodGroupBuilder> { - return new PodGroupBuilder({ ...this.pods, [key]: pod } as Concrete< - P & { [PK in K]: T } - >); + public static create() { + // JavaScript does not have true private constructors, so we use a static + // variable to prevent construction. + PODGroupSpecBuilder.#isInternalConstructing = true; + return new PODGroupSpecBuilder({ + pods: {}, + statements: {} + }); } - public tuple(tuple: TupleSpec

) { - this.tuples.push(tuple); - return this; + public spec(): PODGroupSpec { + return structuredClone(this.#spec); } - public lessThan( - entryPair1: PodEntryStringsOfType, - entryPair2: PodEntryStringsOfType - ) { - // eslint-disable-next-line @typescript-eslint/no-unused-vars - const [pod1, entry1] = entryPair1.split("."); - // eslint-disable-next-line @typescript-eslint/no-unused-vars - const [pod2, entry2] = entryPair2.split("."); - return this; + public pod< + N extends PODName, + Spec extends PODSpec, + NewPods extends AddPOD + >(name: N, spec: Spec): PODGroupSpecBuilder { + if (name in this.#spec.pods) { + throw new Error(`POD "${name}" already exists`); + } + + // Will throw if the name is not valid. + checkPODName(name); + + return new PODGroupSpecBuilder({ + ...this.#spec, + pods: { ...this.#spec.pods, [name]: spec } as unknown as NewPods + }); } - public build(): PodGroupSpec

{ - return this.spec; + public isMemberOf( + name: N + ): PODGroupSpecBuilder { + return new PODGroupSpecBuilder({ + ...this.#spec, + statements: { + ...this.#spec.statements, + [name]: { type: "isMemberOf", isMemberOf: name } + } + }); } } if (import.meta.vitest) { const { it, expect } = import.meta.vitest; - it("can add a POD to the group", () => { - const group = new PodGroupBuilder({ - pod1: { - entries: { - foo: { type: "int" }, - bar: { type: "string" } - } - } - }) - .add("pod2", { - entries: { - foo: { type: "int" } - } - }) - .build(); - - expect(group.pods.pod1).toEqual({ - entries: { - foo: { type: "int" }, - bar: { type: "string" } - } + + it("PODGroupSpecBuilder", () => { + const group = PODGroupSpecBuilder.create(); + const podBuilder = PODSpecBuilder.create().entry("my_string", "string"); + const group2 = group.pod("foo", podBuilder.spec()); + const spec = group2.spec(); + expect(group2.spec()).toEqual({ + pods: { + foo: podBuilder.spec() + }, + statements: {} }); }); } diff --git a/packages/podspec/src/builders/groupv2.ts b/packages/podspec/src/builders/groupv2.ts deleted file mode 100644 index 8476e63..0000000 --- a/packages/podspec/src/builders/groupv2.ts +++ /dev/null @@ -1,102 +0,0 @@ -import type { PODName } from "@pcd/pod"; -import { - PODSpecBuilderV2, - type StatementMap, - type PODSpecV2, - type EntryTypes -} from "./podv2.js"; - -type PODGroupPODs = Record>; - -// @TODO add group constraints, where instead of extending EntryListSpec, -// we have some kind of group entry list, with each entry name prefixed -// by the name of the POD it belongs to. - -// type GroupIsMemberOf = { -// entry: N[number]; -// type: "isMemberOf"; -// isMemberOf: N[number]; -// }; - -type PODGroupSpec< - PODs extends PODGroupPODs, - Statements extends StatementMap -> = { - pods: PODs; - statements: Statements; -}; - -// type AddEntry< -// E extends EntryListSpec, -// K extends keyof E, -// V extends PODValueType -// > = Concrete; - -type Evaluate = T extends infer O ? { [K in keyof O]: O[K] } : never; - -type AddPOD< - PODs extends PODGroupPODs, - N extends PODName, - Spec extends PODSpecV2 -> = Evaluate<{ - [K in keyof PODs | N]: K extends N ? Spec : PODs[K & keyof PODs]; -}>; - -class PODGroupSpecBuilder< - PODs extends PODGroupPODs, - Statements extends StatementMap -> { - #spec: PODGroupSpec; - - constructor(pods: PODs, statements: Statements) { - this.#spec = { - pods, - statements - }; - } - - public static create(pods: PODs) { - return new PODGroupSpecBuilder(pods, {}); - } - - public spec(): PODGroupSpec { - return this.#spec; - } - - public pod< - N extends PODName, - Spec extends PODSpecV2, - NewPods extends AddPOD - >(name: N, spec: Spec): PODGroupSpecBuilder { - return new PODGroupSpecBuilder( - { ...this.#spec.pods, [name]: spec } as unknown as NewPods, - this.#spec.statements - ); - } - - public isMemberOf( - name: N - ): PODGroupSpecBuilder { - return new PODGroupSpecBuilder(this.#spec.pods, { - ...this.#spec.statements, - [name]: { type: "isMemberOf", isMemberOf: name } - }); - } -} - -if (import.meta.vitest) { - const { it, expect } = import.meta.vitest; - - it("PODGroupSpecBuilder", () => { - const group = PODGroupSpecBuilder.create({}); - const podBuilder = PODSpecBuilderV2.create().entry("my_string", "string"); - const group2 = group.pod("foo", podBuilder.spec()); - const spec = group2.spec(); - expect(group2.spec()).toEqual({ - pods: { - foo: podBuilder.spec() - }, - statements: {} - }); - }); -} diff --git a/packages/podspec/src/builders/pod.ts b/packages/podspec/src/builders/pod.ts index 129f0a5..08570ec 100644 --- a/packages/podspec/src/builders/pod.ts +++ b/packages/podspec/src/builders/pod.ts @@ -1,168 +1,857 @@ -import type { EntryListSpec } from "../types/entries.js"; -import type { PODSpec, PODTupleSpec, PODTuplesSpec } from "../types/pod.js"; +import { + checkPODName, + POD_CRYPTOGRAPHIC_MAX, + POD_CRYPTOGRAPHIC_MIN, + POD_DATE_MAX, + POD_DATE_MIN, + POD_INT_MAX, + POD_INT_MIN, + type PODName, + type PODValue +} from "@pcd/pod"; +import type { PODValueType } from "../types/utils.js"; +import { deepFreeze, validateRange } from "./shared.js"; -export class PODSpecBuilder { - readonly #spec: PODSpec; +/** + @todo + - [ ] add lessThan, greaterThan, lessThanEq, greaterThanEq + - [ ] add omit + - [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 + - [ ] validate isMemberOf/isNotMemberOf parameters + - [ ] handle multiple/incompatible range checks on the same entry + - [x] switch to using value types rather than PODValues (everywhere? maybe not membership lists) + - [ ] better error messages + */ - private constructor(spec: PODSpec) { +const virtualEntries: VirtualEntries = { + $contentID: { type: "string" }, + $signature: { type: "string" }, + $signerPublicKey: { type: "eddsa_pubkey" } +}; + +type VirtualEntries = { + $contentID: { type: "string" }; + $signature: { type: "string" }; + $signerPublicKey: { type: "eddsa_pubkey" }; +}; + +export type PODSpec = { + entries: E; + statements: S; +}; + +export type EntryTypes = Record; + +export type EntryKeys = (keyof E & string)[]; + +export type PODValueTupleForNamedEntries< + E extends EntryTypes, + Names extends EntryKeys +> = { + [K in keyof Names]: PODValueTypeFromTypeName; +}; + +type PODValueTypeFromTypeName = Extract< + PODValue, + { type: T } +>["value"]; + +type EntriesOfType = { + [P in keyof E as E[P] extends T ? P & string : never]: E[P]; +}; + +/** + * @TODO Consider not having the E type parameter here. + * We can practically constrain the entry names using the constraint method + * signature, and then store a lighter-weight type that just lists the entry + * names used, without keeping a reference to the entry type list. + */ + +type IsMemberOf & string[]> = { + entries: N; + type: "isMemberOf"; + isMemberOf: PODValueTupleForNamedEntries[]; +}; + +type IsNotMemberOf> = { + entries: N; + type: "isNotMemberOf"; + isNotMemberOf: PODValueTupleForNamedEntries[]; +}; + +type SupportsRangeChecks = "int" | "boolean" | "date"; +type DoesNotSupportRangeChecks = Exclude; + +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; + } +} + +type InRange< + E extends EntryTypes, + N extends keyof EntriesOfType +> = { + entry: N; + type: "inRange"; + inRange: { + min: E[N] extends "date" ? Date : bigint; + max: E[N] extends "date" ? Date : bigint; + }; +}; + +type NotInRange< + E extends EntryTypes, + N extends keyof EntriesOfType +> = { + entry: N; + type: "notInRange"; + notInRange: { + min: E[N] extends "date" ? Date : bigint; + max: E[N] extends "date" ? Date : bigint; + }; +}; + +type EqualsEntry< + E extends EntryTypes, + N1 extends keyof (E & VirtualEntries), + N2 extends keyof (E & VirtualEntries) +> = E[N2] extends E[N1] + ? { + entry: N1; + type: "equalsEntry"; + equalsEntry: N2; + } + : never; + +type NotEqualsEntry< + E extends EntryTypes, + N1 extends keyof (E & VirtualEntries), + N2 extends keyof (E & VirtualEntries) +> = E[N2] extends E[N1] + ? { + entry: N1; + type: "notEqualsEntry"; + notEqualsEntry: N2; + } + : never; + +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; + +/** + * Given a list of entry names, return the names of the entries that are not in the list + */ +type OmittedEntryNames = Exclude< + keyof E, + N[number] +>; + +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; + +// 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; + +type StatementName< + N extends readonly string[], + S extends Statements["type"], + Map extends StatementMap +> = NextAvailableSuffix, Map>; + +// Base constraint map +export type StatementMap = Record; + +export class PODSpecBuilder< + E extends EntryTypes, + // eslint-disable-next-line @typescript-eslint/no-empty-object-type + S extends StatementMap = {} +> { + readonly #spec: PODSpec; + + private constructor(spec: PODSpec) { this.#spec = spec; } - public static create(entries: E): PODSpecBuilder { - return new PODSpecBuilder({ entries, tuples: {} as PODTuplesSpec }); + public static create() { + return new PODSpecBuilder({ + entries: {}, + statements: {} + }); } - /** - * Add a tuple constraint to the schema - */ - tuple( - name: N, - tuple: { entries: [...K] } & Omit, "entries"> - ): PODSpecBuilder { - if (name in this.#spec.tuples) { - throw new ReferenceError( - `Tuple ${name.toString()} already exists: ${Object.keys( - this.#spec.tuples - ).join(", ")}` - ); + public spec(): PODSpec { + return deepFreeze(this.#spec); + } + + public entry< + K extends string, + V extends PODValueType, + NewEntries extends AddEntry + >(key: Exclude, type: V): PODSpecBuilder { + if (key in this.#spec.entries) { + throw new Error(`Entry "${key}" already exists`); } + + // Will throw if not a valid POD entry name + checkPODName(key); + return new PODSpecBuilder({ ...this.#spec, - tuples: { - ...this.#spec.tuples, - [name]: tuple - } as PODTuplesSpec + entries: { + ...this.#spec.entries, + [key]: type + } as NewEntries, + statements: this.#spec.statements }); } /** - * Pick tuples by name - * - * @todo Make the names type-safe for better DX + * Pick entries by key */ - pickTuples(names: string[]) { + public pick( + keys: K[] + ): PODSpecBuilder, Concrete>> { + return new PODSpecBuilder({ + entries: Object.fromEntries( + Object.entries(this.#spec.entries).filter(([key]) => + keys.includes(key as K) + ) + ) as Pick, + statements: Object.fromEntries( + Object.entries(this.#spec.statements).filter(([_key, statement]) => { + const statementType = statement.type; + switch (statementType) { + case "isMemberOf": + case "isNotMemberOf": + return (statement.entries as EntryKeys).every((entry) => + keys.includes(entry as K) + ); + case "inRange": + case "notInRange": + return keys.includes(statement.entry as K); + case "equalsEntry": + return ( + keys.includes(statement.entry as K) && + keys.includes(statement.equalsEntry as K) + ); + case "notEqualsEntry": + return ( + keys.includes(statement.entry as K) && + keys.includes(statement.notEqualsEntry as K) + ); + default: + const _exhaustiveCheck: never = statement; + throw new Error( + `Unsupported statement type: ${statementType as string}` + ); + } + }) + ) as Concrete> + }); + } + + public pickStatements( + keys: K[] + ): PODSpecBuilder>> { return new PODSpecBuilder({ ...this.#spec, - tuples: Object.fromEntries( - Object.entries(this.#spec.tuples).filter(([name]) => - names.includes(name) + statements: Object.fromEntries( + Object.entries(this.#spec.statements).filter(([key]) => + keys.includes(key as K) ) - ) + ) as Concrete> }); } - /** - * Omit tuples by name - * - * @todo Make the names type-safe for better DX - */ - omitTuples(names: string[]) { + public omitStatements( + keys: K[] + ): PODSpecBuilder>> { return new PODSpecBuilder({ ...this.#spec, - tuples: Object.fromEntries( - Object.entries(this.#spec.tuples).filter( - ([name]) => !names.includes(name) + statements: Object.fromEntries( + Object.entries(this.#spec.statements).filter( + ([key]) => !keys.includes(key as K) ) - ) + ) as Concrete> }); } /** - * Configure signer public key constraints + * 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 */ - signerPublicKey(config: { - isRevealed?: boolean; - isMemberOf?: string[]; - isNotMemberOf?: string[]; - }): PODSpecBuilder { + public isMemberOf>( + names: [...N], + values: N["length"] extends 1 + ? PODValueTypeFromTypeName< + (E & VirtualEntries)[N[0] & keyof (E & VirtualEntries)] + >[] + : PODValueTupleForNamedEntries[] + ): PODSpecBuilder< + E, + S & { [K in StatementName]: IsMemberOf } + > { + // Check that all names exist in entries + for (const name of names) { + if (!(name in this.#spec.entries) && !(name in virtualEntries)) { + 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 statement: IsMemberOf = { + entries: names, + type: "isMemberOf", + // Wrap single values in arrays to match the expected tuple format + isMemberOf: (names.length === 1 + ? ( + values as PODValueTypeFromTypeName< + (E & VirtualEntries)[N[0] & keyof (E & VirtualEntries)] + >[] + ).map((v) => [v]) + : values) as PODValueTupleForNamedEntries[] + }; + + const baseName = `${names.join("_")}_isMemberOf`; + let statementName = baseName; + let suffix = 1; + + while (statementName in this.#spec.statements) { + statementName = `${baseName}_${suffix++}`; + } + return new PODSpecBuilder({ ...this.#spec, - signerPublicKey: config + statements: { + ...this.#spec.statements, + [statementName]: statement + } }); } /** - * Configure signature constraints + * 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 */ - signature(config: { - isMemberOf?: string[]; - isNotMemberOf?: string[]; - }): PODSpecBuilder { + public isNotMemberOf>( + names: [...N], + values: N["length"] extends 1 + ? PODValueTypeFromTypeName< + (E & VirtualEntries)[N[0] & keyof (E & VirtualEntries)] + >[] + : PODValueTupleForNamedEntries[] + ): PODSpecBuilder< + E, + S & { [K in StatementName]: IsNotMemberOf } + > { + // Check that all names exist in entries + for (const name of names) { + if (!(name in this.#spec.entries)) { + 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 statement: IsNotMemberOf = { + entries: names, + type: "isNotMemberOf", + // Wrap single values in arrays to match the expected tuple format + isNotMemberOf: (names.length === 1 + ? ( + values as PODValueTypeFromTypeName< + (E & VirtualEntries)[N[0] & keyof (E & VirtualEntries)] + >[] + ).map((v) => [v]) + : values) as PODValueTupleForNamedEntries[] + }; + + const baseName = `${names.join("_")}_isNotMemberOf`; + let statementName = baseName; + let suffix = 1; + + while (statementName in this.#spec.statements) { + statementName = `${baseName}_${suffix++}`; + } + return new PODSpecBuilder({ ...this.#spec, - signature: config + statements: { + ...this.#spec.statements, + [statementName]: statement + } }); } /** - * Set metadata for the schema + * 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 */ - meta(config: { labelEntry: keyof E & string }): PODSpecBuilder { + public inRange< + N extends keyof EntriesOfType & string + >( + name: N, + range: { + min: E[N] extends "date" ? Date : bigint; + max: E[N] extends "date" ? Date : bigint; + } + ): PODSpecBuilder< + E, + S & { [K in StatementName<[N & string], "inRange", S>]: InRange } + > { + // Check that the entry exists + if (!(name in this.#spec.entries) && !(name in virtualEntries)) { + 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 = { + entry: name, + type: "inRange", + inRange: range + }; + + const baseName = `${name}_inRange`; + let statementName = baseName; + let suffix = 1; + + while (statementName in this.#spec.statements) { + statementName = `${baseName}_${suffix++}`; + } + return new PODSpecBuilder({ ...this.#spec, - meta: config + statements: { + ...this.#spec.statements, + [statementName]: statement + } }); } /** - * Build and return the final POD schema + * 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 */ - build(): PODSpec { - return structuredClone(this.#spec); + public notInRange< + N extends keyof EntriesOfType & string + >( + name: N, + range: { + min: E[N] extends "date" ? Date : bigint; + max: E[N] extends "date" ? Date : bigint; + } + ): PODSpecBuilder< + E, + S & { + [K in StatementName<[N & string], "notInRange", S>]: NotInRange; + } + > { + // Check that the entry exists + if (!(name in this.#spec.entries) && !(name in virtualEntries)) { + throw new Error(`Entry "${name}" does not exist`); + } + + const entryType = this.#spec.entries[name]; + + if (entryType === "int") { + validateRange( + range.min as bigint, + range.max as bigint, + POD_INT_MIN, + POD_INT_MAX + ); + } else if (entryType === "cryptographic") { + validateRange( + range.min as bigint, + range.max as bigint, + POD_CRYPTOGRAPHIC_MIN, + POD_CRYPTOGRAPHIC_MAX + ); + } else if (entryType === "date") { + validateRange( + range.min as Date, + range.max as Date, + POD_DATE_MIN, + POD_DATE_MAX + ); + } + + const statement: NotInRange = { + entry: name, + type: "notInRange", + notInRange: range + }; + + const baseName = `${name}_notInRange`; + let statementName = baseName; + let suffix = 1; + + while (statementName in this.#spec.statements) { + statementName = `${baseName}_${suffix++}`; + } + + return new PODSpecBuilder({ + ...this.#spec, + statements: { + ...this.#spec.statements, + [statementName]: statement + } + }); } - /** - * Pick entries by key - */ - pick(keys: K[]): PODSpecBuilder> { - // Remove tuples whose keys are not picked - const tuples = Object.fromEntries( - Object.entries(this.#spec.tuples).filter(([_key, tuple]) => - tuple.entries.every( - (entry) => entry === "$signerPublicKey" || keys.includes(entry as K) - ) - ) - ); + public equalsEntry< + N1 extends keyof (E & VirtualEntries) & string, + N2 extends keyof (E & VirtualEntries) & string + >( + name1: N1, + name2: E[N2] extends E[N1] ? N2 : never + ): PODSpecBuilder< + E, + S & { + [K in StatementName<[N1, N2], "equalsEntry", S>]: EqualsEntry; + } + > { + // Check that both names exist in entries + if (!(name1 in this.#spec.entries) && !(name1 in virtualEntries)) { + throw new Error(`Entry "${name1}" does not exist`); + } + if (!(name2 in this.#spec.entries) && !(name2 in virtualEntries)) { + throw new Error(`Entry "${name2}" does not exist`); + } + if ((name1 as string) === (name2 as string)) { + throw new Error("Entry names must be different"); + } + if ((this.#spec.entries[name1] as string) !== this.#spec.entries[name2]) { + throw new Error("Entry types must be the same"); + } + + const statement = { + entry: name1, + type: "equalsEntry", + equalsEntry: name2 + // We know that the types are compatible, so we can cast to the correct type + } as unknown as EqualsEntry; + + const baseName = `${name1}_${name2}_equalsEntry`; + let statementName = baseName; + let suffix = 1; + + while (statementName in this.#spec.statements) { + statementName = `${baseName}_${suffix++}`; + } - // If the labelEntry is picked, keep it, otherwise remove it - const meta = this.#spec.meta?.labelEntry - ? keys.includes(this.#spec.meta.labelEntry as K) - ? this.#spec.meta - : undefined - : undefined; + return new PODSpecBuilder({ + ...this.#spec, + statements: { + ...this.#spec.statements, + [statementName]: statement + } + }); + } + + public notEqualsEntry< + N1 extends keyof E & VirtualEntries & string, + N2 extends keyof E & VirtualEntries & string + >( + name1: N1, + name2: E[N2] extends E[N1] ? N2 : never + ): PODSpecBuilder< + E, + S & { + [K in StatementName<[N1, N2], "notEqualsEntry", S>]: NotEqualsEntry< + E, + N1, + N2 + >; + } + > { + // Check that both names exist in entries + if (!(name1 in this.#spec.entries) && !(name1 in virtualEntries)) { + throw new Error(`Entry "${name1}" does not exist`); + } + if (!(name2 in this.#spec.entries) && !(name2 in virtualEntries)) { + throw new Error(`Entry "${name2}" does not exist`); + } + if ((name1 as string) === (name2 as string)) { + throw new Error("Entry names must be different"); + } + if ((this.#spec.entries[name1] as string) !== this.#spec.entries[name2]) { + throw new Error("Entry types must be the same"); + } + + const statement = { + entry: name1, + type: "notEqualsEntry", + notEqualsEntry: name2 + // We know that the types are compatible, so we can cast to the correct type + } as unknown as NotEqualsEntry; + + const baseName = `${name1}_${name2}_notEqualsEntry`; + let statementName = baseName; + let suffix = 1; + + while (statementName in this.#spec.statements) { + statementName = `${baseName}_${suffix++}`; + } return new PODSpecBuilder({ ...this.#spec, - entries: Object.fromEntries( - keys.map((k) => [k, this.#spec.entries[k]]) - ) as Pick, - tuples, - meta - } as PODSpec>); + statements: { + ...this.#spec.statements, + [statementName]: statement + } + }); } } if (import.meta.vitest) { const { it, expect } = import.meta.vitest; - it("add", () => { - const pod = PODSpecBuilder.create({ - name: { type: "string" }, - age: { type: "int" } - }).tuple("foo", { - entries: ["name"], - isMemberOf: [[{ type: "string", value: "foo" }]] - }); - - const output = pod.build(); - - expect(output).toEqual({ - entries: { name: { type: "string" }, age: { type: "int" } }, - tuples: { - foo: { - entries: ["name"], - isMemberOf: [[{ type: "string", value: "foo" }]] - } + it("PODSpecBuilder", () => { + const a = PODSpecBuilder.create(); + const b = a.entry("a", "string").entry("b", "int"); + expect(b.spec().entries).toEqual({ + a: "string", + b: "int" + }); + + 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: { + entry: "b", + type: "inRange", + inRange: { min: 10n, max: 100n } + } + }); + + const e = d.isMemberOf(["a", "b"], [["foo", 10n]]); + expect(e.spec().statements.a_b_isMemberOf.entries).toEqual(["a", "b"]); + + const f = e.pick(["b"]); + expect(f.spec().statements).toEqual({ + b_inRange: { + entry: "b", + type: "inRange", + inRange: { min: 10n, max: 100n } + } + }); + + const g = e.entry("new", "string").equalsEntry("a", "new"); + const _GEntries = g.spec().entries; + type EntriesType = typeof _GEntries; + g.spec().statements.a_new_equalsEntry satisfies EqualsEntry< + EntriesType, + "a", + "new" + >; + + expect(g.spec().statements).toMatchObject({ + a_new_equalsEntry: { + entry: "a", + type: "equalsEntry", + equalsEntry: "new" + } + }); + + const h = g.pickStatements(["a_isMemberOf"]); + expect(h.spec().statements).toEqual({ + a_isMemberOf: { + entries: ["a"], + type: "isMemberOf", + isMemberOf: [["foo"]] } }); }); } + +// Example entry list spec +type TestEntries = { + a: "string"; + b: "int"; + c: "int"; +}; + +// Example statement map +type TestStatements = { + a_isMemberOf: IsMemberOf; + b_inRange: InRange; + ac_isMemberOf: IsMemberOf; +}; + +// Let's test picking just 'a' and 'b' +type PickedKeys = ["b"]; + +// First, let's see what OmittedEntryNames gives us +type TestOmitted = OmittedEntryNames; +// Should be: "c" + +// Now let's test NonOverlapping +type TestNonOverlapping = NonOverlappingStatements; + +// Let's see what we get when picking just 'a' +type TestPickA = NonOverlappingStatements; diff --git a/packages/podspec/src/builders/podv2.ts b/packages/podspec/src/builders/podv2.ts deleted file mode 100644 index 0ae358b..0000000 --- a/packages/podspec/src/builders/podv2.ts +++ /dev/null @@ -1,763 +0,0 @@ -import { - POD_CRYPTOGRAPHIC_MAX, - POD_CRYPTOGRAPHIC_MIN, - POD_DATE_MAX, - POD_DATE_MIN, - POD_INT_MAX, - POD_INT_MIN, - type PODName, - type PODValue -} from "@pcd/pod"; -import type { PODValueType } from "../types/utils.js"; -import { validateRange } from "./shared.js"; - -/** - @todo - - [ ] add lessThan, greaterThan, lessThanEq, greaterThanEq - - [ ] add omit - - [ ] maybe add pick/omit for constraints? - - [x] add signerPublicKey support - - [ ] add constraints on signature - - [x] add contentID virtual entry - - [ ] refactor types - - [ ] rename away from v2 suffix - - [ ] validate entry names - - [ ] validate constraint parameters - - [ ] switch to using value types rather than PODValues (everywhere? maybe not membership lists) - */ - -const virtualEntries: VirtualEntries = { - $contentID: { type: "string" }, - $signature: { type: "string" }, - $signerPublicKey: { type: "eddsa_pubkey" } -}; - -type VirtualEntries = { - $contentID: { type: "string" }; - $signature: { type: "string" }; - $signerPublicKey: { type: "eddsa_pubkey" }; -}; - -export type PODSpecV2 = { - entries: E; - statements: S; -}; - -export type EntryTypes = Record; - -export type EntryKeys = (keyof E & string)[]; - -export type PODValueTupleForNamedEntries< - E extends EntryTypes, - Names extends EntryKeys -> = { - [K in keyof Names]: PODValueTypeFromTypeName; -}; - -type PODValueTypeFromTypeName = Extract< - PODValue, - { type: T } ->; - -type EntriesOfType = { - [P in keyof E as E[P] extends T ? P & string : never]: E[P]; -}; - -/** - * @TODO Consider not having the E type parameter here. - * We can practically constrain the entry names using the constraint method - * signature, and then store a lighter-weight type that just lists the entry - * names used, without keeping a reference to the entry type list. - */ - -type IsMemberOf> = { - entries: N; - type: "isMemberOf"; - isMemberOf: PODValueTupleForNamedEntries[]; -}; - -type IsNotMemberOf> = { - entries: N; - type: "isNotMemberOf"; - isNotMemberOf: PODValueTupleForNamedEntries[]; -}; - -type SupportsRangeChecks = "int" | "cryptographic" | "date"; - -type InRange< - E extends EntryTypes, - N extends keyof EntriesOfType -> = { - entry: N; - type: "inRange"; - inRange: { - min: E[N] extends "date" ? Date : bigint; - max: E[N] extends "date" ? Date : bigint; - }; -}; - -type NotInRange< - E extends EntryTypes, - N extends keyof EntriesOfType -> = { - entry: N; - type: "notInRange"; - notInRange: { - min: E[N] extends "date" ? Date : bigint; - max: E[N] extends "date" ? Date : bigint; - }; -}; - -type EqualsEntry< - E extends EntryTypes, - N1 extends keyof E, - N2 extends keyof E -> = E[N2] extends E[N1] - ? { - entry: N1; - type: "equalsEntry"; - equalsEntry: N2; - } - : never; - -type NotEqualsEntry< - E extends EntryTypes, - N1 extends keyof E, - N2 extends keyof E -> = E[N2] extends E[N1] - ? { - entry: N1; - type: "notEqualsEntry"; - notEqualsEntry: N2; - } - : never; - -type Statements = - | IsMemberOf - | IsNotMemberOf - | InRange - | NotInRange - | EqualsEntry - | NotEqualsEntry; - -/** - * Given a list of entry names, return the names of the entries that are not in the list - */ -type OmittedEntryNames = Exclude< - keyof E, - N[number] ->; - -type NonOverlappingStatements = { - [K in keyof S as S[K] extends - | IsMemberOf - | IsNotMemberOf - ? Entries[number] extends N[number] - ? K - : never - : S[K] extends InRange - ? Entry extends N[number] - ? K - : never - : S[K] extends NotInRange - ? Entry extends N[number] - ? K - : never - : S[K] extends EqualsEntry - ? [Entry1, Entry2][number] extends N[number] - ? K - : never - : 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; - -// 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; - -type StatementName< - N extends readonly string[], - S extends Statements["type"], - Map extends StatementMap -> = NextAvailableSuffix, Map>; - -// Base constraint map -export type StatementMap = Record; - -export class PODSpecBuilderV2< - E extends EntryTypes, - // eslint-disable-next-line @typescript-eslint/no-empty-object-type - S extends StatementMap = {} -> { - readonly #spec: PODSpecV2; - - private constructor(spec: PODSpecV2) { - this.#spec = spec; - } - - public static create() { - return new PODSpecBuilderV2({ - entries: {}, - statements: {} - }); - } - - public spec(): PODSpecV2 { - return structuredClone(this.#spec); - } - - public entry< - K extends string, - V extends PODValueType, - NewEntries extends AddEntry - >(key: Exclude, type: V): PODSpecBuilderV2 { - // @todo handle existing entries? - return new PODSpecBuilderV2({ - ...this.#spec, - entries: { - ...this.#spec.entries, - [key]: type - } as NewEntries, - statements: this.#spec.statements - }); - } - - /** - * Pick entries by key - */ - public pick( - keys: K[] - ): PODSpecBuilderV2, Concrete>> { - return new PODSpecBuilderV2({ - entries: Object.fromEntries( - Object.entries(this.#spec.entries).filter(([key]) => - keys.includes(key as K) - ) - ) as Pick, - statements: Object.fromEntries( - Object.entries(this.#spec.statements).filter(([_key, statement]) => { - if (statement.type === "isMemberOf") { - return (statement.entries as EntryKeys).every((entry) => - keys.includes(entry as K) - ); - } else if (statement.type === "inRange") { - return keys.includes(statement.entry as K); - } - return false; - }) - ) as Concrete> - }); - } - - /** - * 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 PODSpecBuilderV2 with the constraint added - */ - public isMemberOf>( - names: [...N], - values: N["length"] extends 1 - ? PODValueTypeFromTypeName< - (E & VirtualEntries)[N[0] & keyof (E & VirtualEntries)] - >[] - : PODValueTupleForNamedEntries[] - ): PODSpecBuilderV2< - E, - S & { [K in StatementName]: IsMemberOf } - > { - // Check that all names exist in entries - for (const name of names) { - if (!(name in this.#spec.entries) && !(name in virtualEntries)) { - 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 statement: IsMemberOf = { - entries: names, - type: "isMemberOf", - // Wrap single values in arrays to match the expected tuple format - isMemberOf: (names.length === 1 - ? // @todo handle virtual entries - (values as PODValueTypeFromTypeName[]).map((v) => [ - v - ]) - : values) as PODValueTupleForNamedEntries[] - }; - - const baseName = `${names.join("_")}_isMemberOf`; - let statementName = baseName; - let suffix = 1; - - while (statementName in this.#spec.statements) { - statementName = `${baseName}_${suffix++}`; - } - - return new PODSpecBuilderV2({ - ...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 PODSpecBuilderV2 with the constraint added - */ - public isNotMemberOf>( - names: [...N], - values: N["length"] extends 1 - ? PODValueTypeFromTypeName[] - : PODValueTupleForNamedEntries[] - ): PODSpecBuilderV2< - E, - S & { [K in StatementName]: IsNotMemberOf } - > { - // Check that all names exist in entries - for (const name of names) { - if (!(name in this.#spec.entries)) { - 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 statement: IsNotMemberOf = { - entries: names, - type: "isNotMemberOf", - // Wrap single values in arrays to match the expected tuple format - isNotMemberOf: (names.length === 1 - ? (values as PODValueTypeFromTypeName[]).map((v) => [ - v - ]) - : values) as PODValueTupleForNamedEntries[] - }; - - const baseName = `${names.join("_")}_isNotMemberOf`; - let statementName = baseName; - let suffix = 1; - - while (statementName in this.#spec.statements) { - statementName = `${baseName}_${suffix++}`; - } - - return new PODSpecBuilderV2({ - ...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 PODSpecBuilderV2 with the constraint added - */ - public inRange< - N extends keyof EntriesOfType & string - >( - name: N, - range: { - min: E[N] extends "date" ? Date : bigint; - max: E[N] extends "date" ? Date : bigint; - } - ): PODSpecBuilderV2< - E, - S & { [K in StatementName<[N & string], "inRange", S>]: InRange } - > { - // Check that the entry exists - if (!(name in this.#spec.entries) && !(name in virtualEntries)) { - throw new Error(`Entry "${name}" does not exist`); - } - - const entryType = this.#spec.entries[name]; - - if (entryType === "int") { - validateRange( - range.min as bigint, - range.max as bigint, - POD_INT_MIN, - POD_INT_MAX - ); - } else if (entryType === "cryptographic") { - validateRange( - range.min as bigint, - range.max as bigint, - POD_CRYPTOGRAPHIC_MIN, - POD_CRYPTOGRAPHIC_MAX - ); - } else if (entryType === "date") { - validateRange( - range.min as Date, - range.max as Date, - POD_DATE_MIN, - POD_DATE_MAX - ); - } - - const statement: InRange = { - entry: name, - type: "inRange", - inRange: range - }; - - const baseName = `${name}_inRange`; - let statementName = baseName; - let suffix = 1; - - while (statementName in this.#spec.statements) { - statementName = `${baseName}_${suffix++}`; - } - - return new PODSpecBuilderV2({ - ...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 PODSpecBuilderV2 with the constraint added - */ - public notInRange< - N extends keyof EntriesOfType & string - >( - name: N, - range: { - min: E[N] extends "date" ? Date : bigint; - max: E[N] extends "date" ? Date : bigint; - } - ): PODSpecBuilderV2< - E, - S & { - [K in StatementName<[N & string], "notInRange", S>]: NotInRange; - } - > { - // Check that the entry exists - if (!(name in this.#spec.entries) && !(name in virtualEntries)) { - throw new Error(`Entry "${name}" does not exist`); - } - - const entryType = this.#spec.entries[name]; - - if (entryType === "int") { - validateRange( - range.min as bigint, - range.max as bigint, - POD_INT_MIN, - POD_INT_MAX - ); - } else if (entryType === "cryptographic") { - validateRange( - range.min as bigint, - range.max as bigint, - POD_CRYPTOGRAPHIC_MIN, - POD_CRYPTOGRAPHIC_MAX - ); - } else if (entryType === "date") { - validateRange( - range.min as Date, - range.max as Date, - POD_DATE_MIN, - POD_DATE_MAX - ); - } - - const statement: NotInRange = { - entry: name, - type: "notInRange", - notInRange: range - }; - - const baseName = `${name}_notInRange`; - let statementName = baseName; - let suffix = 1; - - while (statementName in this.#spec.statements) { - statementName = `${baseName}_${suffix++}`; - } - - return new PODSpecBuilderV2({ - ...this.#spec, - statements: { - ...this.#spec.statements, - [statementName]: statement - } - }); - } - - public equalsEntry( - name1: N1, - name2: E[N2] extends E[N1] ? N2 : never - ): PODSpecBuilderV2< - E, - S & { - [K in StatementName<[N1, N2], "equalsEntry", S>]: EqualsEntry; - } - > { - // Check that both names exist in entries - if (!(name1 in this.#spec.entries)) { - throw new Error(`Entry "${name1}" does not exist`); - } - if (!(name2 in this.#spec.entries)) { - throw new Error(`Entry "${name2}" does not exist`); - } - if ((name1 as string) === (name2 as string)) { - throw new Error("Entry names must be different"); - } - if ((this.#spec.entries[name1] as string) !== this.#spec.entries[name2]) { - throw new Error("Entry types must be the same"); - } - - const statement = { - entry: name1, - type: "equalsEntry", - equalsEntry: name2 - // We know that the types are compatible, so we can cast to the correct type - } as unknown as EqualsEntry; - - const baseName = `${name1}_${name2}_equalsEntry`; - let statementName = baseName; - let suffix = 1; - - while (statementName in this.#spec.statements) { - statementName = `${baseName}_${suffix++}`; - } - - return new PODSpecBuilderV2({ - ...this.#spec, - statements: { - ...this.#spec.statements, - [statementName]: statement - } - }); - } - - public notEqualsEntry< - N1 extends keyof E & string, - N2 extends keyof E & string - >( - name1: N1, - name2: E[N2] extends E[N1] ? N2 : never - ): PODSpecBuilderV2< - E, - S & { - [K in StatementName<[N1, N2], "notEqualsEntry", S>]: NotEqualsEntry< - E, - N1, - N2 - >; - } - > { - // Check that both names exist in entries - if (!(name1 in this.#spec.entries)) { - throw new Error(`Entry "${name1}" does not exist`); - } - if (!(name2 in this.#spec.entries)) { - throw new Error(`Entry "${name2}" does not exist`); - } - if ((name1 as string) === (name2 as string)) { - throw new Error("Entry names must be different"); - } - if ((this.#spec.entries[name1] as string) !== this.#spec.entries[name2]) { - throw new Error("Entry types must be the same"); - } - - const statement = { - entry: name1, - type: "notEqualsEntry", - notEqualsEntry: name2 - // We know that the types are compatible, so we can cast to the correct type - } as unknown as NotEqualsEntry; - - const baseName = `${name1}_${name2}_notEqualsEntry`; - let statementName = baseName; - let suffix = 1; - - while (statementName in this.#spec.statements) { - statementName = `${baseName}_${suffix++}`; - } - - return new PODSpecBuilderV2({ - ...this.#spec, - statements: { - ...this.#spec.statements, - [statementName]: statement - } - }); - } -} - -if (import.meta.vitest) { - const { it, expect } = import.meta.vitest; - it("PODSpecBuilderV2", () => { - const a = PODSpecBuilderV2.create(); - const b = a.entry("a", "string").entry("b", "int"); - expect(b.spec().entries).toEqual({ - a: { type: "string" }, - b: { type: "int" } - }); - - const c = b.isMemberOf(["a"], [{ type: "string", value: "foo" }]); - expect(c.spec().statements).toEqual({ - a_isMemberOf: { - entries: ["a"], - type: "isMemberOf", - isMemberOf: [[{ type: "string", value: "foo" }]] - } - }); - - const d = c.inRange("b", { min: 10n, max: 100n }); - expect(d.spec().statements).toEqual({ - a_isMemberOf: { - entries: ["a"], - type: "isMemberOf", - isMemberOf: [[{ type: "string", value: "foo" }]] - }, - b_inRange: { - entry: "b", - type: "inRange", - inRange: { min: 10n, max: 100n } - } - }); - - const e = d.isMemberOf( - ["a", "b"], - [ - [ - { type: "string", value: "foo" }, - { type: "int", value: 10n } - ] - ] - ); - expect(e.spec().statements.a_b_isMemberOf.entries).toEqual(["a", "b"]); - - const f = e.pick(["b"]); - expect(f.spec().statements).toEqual({ - b_inRange: { - entry: "b", - type: "inRange", - inRange: { min: 10n, max: 100n } - } - }); - - const g = e.entry("new", "string").equalsEntry("a", "new"); - const _GEntries = g.spec().entries; - type EntriesType = typeof _GEntries; - g.spec().statements.a_new_equalsEntry satisfies EqualsEntry< - EntriesType, - "a", - "new" - >; - - expect(g.spec().statements).toMatchObject({ - a_new_equalsEntry: { - entry: "a", - type: "equalsEntry", - equalsEntry: "new" - } - }); - }); -} - -// Example entry list spec -type TestEntries = { - a: { type: "string" }; - b: { type: "int" }; - c: { type: "int" }; -}; - -// Example statement map -type TestStatements = { - a_isMemberOf: IsMemberOf; - b_inRange: InRange; - ac_isMemberOf: IsMemberOf; -}; - -// Let's test picking just 'a' and 'b' -type PickedKeys = ["b"]; - -// First, let's see what OmittedEntryNames gives us -type TestOmitted = OmittedEntryNames; -// Should be: "c" - -// Now let's test NonOverlapping -type TestNonOverlapping = NonOverlappingStatements; - -// Let's see what we get when picking just 'a' -type TestPickA = NonOverlappingStatements; diff --git a/packages/podspec/src/builders/shared.ts b/packages/podspec/src/builders/shared.ts index 5da0689..ddb826a 100644 --- a/packages/podspec/src/builders/shared.ts +++ b/packages/podspec/src/builders/shared.ts @@ -1,19 +1,21 @@ -import type { - PODBooleanValue, - PODBytesValue, - PODCryptographicValue, - PODDateValue, - PODIntValue, - PODNullValue, - PODStringValue -} from "@pcd/pod"; +import { checkPODValue, type PODValue } from "@pcd/pod"; +import type { PODValueType } from "../types/utils.js"; +/** + * 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"); } @@ -22,30 +24,43 @@ export function validateRange( } } -export function toPODStringValue(value: string): PODStringValue { - return { type: "string", value }; +/** + * 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); } -export function toPODIntValue(value: bigint): PODIntValue { - return { type: "int", value }; -} - -export function toPODBooleanValue(value: boolean): PODBooleanValue { - return { type: "boolean", value }; -} - -export function toPODBytesValue(value: Uint8Array): PODBytesValue { - return { type: "bytes", value }; -} +/** + * 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) + ]; -export function toPODCryptographicValue(value: bigint): PODCryptographicValue { - return { type: "cryptographic", value }; -} - -export function toPODDateValue(value: Date): PODDateValue { - return { type: "date", value }; -} - -export function toPODNullValue(): PODNullValue { - return { type: "null", value: null }; + properties.forEach((prop) => { + const value = obj[prop as keyof T]; + if (value && typeof value === "object" && !Object.isFrozen(value)) { + deepFreeze(value); + } + }); + } + return Object.freeze(obj); } diff --git a/packages/podspec/src/processors/validate.ts b/packages/podspec/src/processors/validate.ts index e69de29..2928eae 100644 --- a/packages/podspec/src/processors/validate.ts +++ b/packages/podspec/src/processors/validate.ts @@ -0,0 +1,143 @@ +import { + POD, + type PODEntries, + type PODValue, + type PODContent, + type PODStringValue, + type PODName +} from "@pcd/pod"; +import { + PODSpecBuilder, + type EntryTypes, + type PODSpec, + type StatementMap +} from "../builders/pod.js"; + +/** + @TOOO + - [ ] Return a typed POD if validation succeeds. + - [ ] "Compile" a spec by hashing the statement parameters where necessary. +*/ + +/** + * "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; +} + +type PODEntriesFromEntryTypes = { + [K in keyof E]: Extract; +}; + +/** + * Validate a POD against a PODSpec. + * + * @param pod - The POD to validate. + * @param spec - The PODSpec to validate against. + * @returns true if the POD is valid, false otherwise. + */ +function validatePOD( + pod: POD, + spec: PODSpec +): pod is StrongPOD> { + const podEntries = pod.content.asEntries(); + + for (const [key, entryType] of Object.entries(spec.entries)) { + if (!(key in podEntries)) { + console.error(`Entry ${key} not found in pod`); + return false; + } + if (podEntries[key]?.type !== entryType) { + console.error( + `Entry ${key} type mismatch: ${podEntries[key]?.type} !== ${entryType}` + ); + return false; + } + } + + for (const [key, statement] of Object.entries(spec.statements)) { + switch (statement.type) { + case "isMemberOf": + const tuple = statement.entries.map( + (entry) => podEntries[entry]?.value + ); + for (const listMember of statement.isMemberOf) { + if ( + listMember.some((value, index) => { + return value === tuple[index]; + }) + ) { + break; + } + console.error( + `Statement ${key} failed: could not find ${statement.entries.join( + ", " + )} in isMemberOf list` + ); + return false; + } + } + } + + return true; +} + +if (import.meta.vitest) { + const { test, expect } = import.meta.vitest; + + const privKey = + "f72c3def0a54280ded2990a66fabcf717130c6f2bb595004658ec77774b98924"; + + const signPOD = (entries: PODEntries) => POD.sign(entries, privKey); + + test("validatePOD", () => { + const myPOD = signPOD({ foo: { type: "string", value: "foo" } }); + 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(validatePOD(myPOD, myPodSpecBuilder.spec())).toBe(true); + + if (validatePOD(myPOD, myPodSpecBuilder.spec())) { + // After validation, the entries are strongly typed + myPOD.content.asEntries().bar?.value satisfies + | PODValue["value"] + | undefined; + myPOD.content.asEntries().foo.value satisfies string; + myPOD.content.getValue("bar")?.value satisfies + | PODValue["value"] + | undefined; + myPOD.content.getRawValue("bar") satisfies PODValue["value"] | undefined; + myPOD.content.getValue("foo") satisfies PODStringValue; + myPOD.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(validatePOD(myPOD, secondBuilder.spec())).toBe(false); + + // If we omit the new statement, it should pass + expect( + validatePOD( + myPOD, + secondBuilder.omitStatements(["foo_isMemberOf_1"]).spec() + ) + ).toBe(true); + }); +} From bde0221c1a68f966b039275d294251e60d29b6ec Mon Sep 17 00:00:00 2001 From: Rob Knight Date: Wed, 29 Jan 2025 09:57:21 +0100 Subject: [PATCH 04/20] Start building out validator --- packages/podspec/src/builders/group.ts | 152 +++++++++++---- packages/podspec/src/builders/pod.ts | 181 +++--------------- .../podspec/src/builders/types/entries.ts | 28 +++ .../podspec/src/builders/types/statements.ts | 130 +++++++++++++ packages/podspec/src/processors/validate.ts | 147 ++++++++++---- .../src/processors/validate/checks/inRange.ts | 49 +++++ .../processors/validate/checks/isMemberOf.ts | 76 ++++++++ .../validate/checks/isNotMemberOf.ts | 77 ++++++++ .../podspec/src/processors/validate/issues.ts | 110 +++++++++++ .../podspec/src/processors/validate/result.ts | 25 +++ .../podspec/src/processors/validate/types.ts | 13 ++ pnpm-lock.yaml | 40 ---- 12 files changed, 758 insertions(+), 270 deletions(-) create mode 100644 packages/podspec/src/builders/types/entries.ts create mode 100644 packages/podspec/src/builders/types/statements.ts create mode 100644 packages/podspec/src/processors/validate/checks/inRange.ts create mode 100644 packages/podspec/src/processors/validate/checks/isMemberOf.ts create mode 100644 packages/podspec/src/processors/validate/checks/isNotMemberOf.ts create mode 100644 packages/podspec/src/processors/validate/issues.ts create mode 100644 packages/podspec/src/processors/validate/result.ts create mode 100644 packages/podspec/src/processors/validate/types.ts diff --git a/packages/podspec/src/builders/group.ts b/packages/podspec/src/builders/group.ts index f2a1e14..d1a881c 100644 --- a/packages/podspec/src/builders/group.ts +++ b/packages/podspec/src/builders/group.ts @@ -1,10 +1,14 @@ import { checkPODName, type PODName } from "@pcd/pod"; -import { - PODSpecBuilder, - type StatementMap, - type PODSpec, - type EntryTypes -} from "./pod.js"; +import type { PODValueType } from "../types/utils.js"; +import { type PODSpec, PODSpecBuilder } from "./pod.js"; +import type { + EntryTypes, + VirtualEntries, + EntryKeys, + PODValueTypeFromTypeName, + PODValueTupleForNamedEntries +} from "./types/entries.js"; +import type { StatementMap, IsMemberOf } from "./types/statements.js"; type PODGroupPODs = Record>; @@ -18,14 +22,22 @@ type PODGroupPODs = Record>; // isMemberOf: N[number]; // }; -type PODGroupSpec< - PODs extends PODGroupPODs, - Statements extends StatementMap -> = { - pods: PODs; - statements: Statements; +type PODGroupSpec

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

= { + [K in keyof P as `${K & string}.${keyof (P[K]["entries"] & VirtualEntries) & + string}`]: P[K]["entries"][keyof P[K]["entries"] & string]; }; +// Add this helper type to preserve literal types +type EntryType< + P extends PODGroupPODs, + K extends keyof AllPODEntries

+> = AllPODEntries

[K] extends PODValueType ? AllPODEntries

[K] : never; + // type AddEntry< // E extends EntryListSpec, // K extends keyof E, @@ -42,40 +54,30 @@ type AddPOD< [K in keyof PODs | N]: K extends N ? Spec : PODs[K & keyof PODs]; }>; -class PODGroupSpecBuilder< - PODs extends PODGroupPODs, - Statements extends StatementMap -> { - readonly #spec: PODGroupSpec; - static #isInternalConstructing = false; - - private constructor(spec: PODGroupSpec) { - if (PODGroupSpecBuilder.#isInternalConstructing) { - throw new Error("PODGroupSpecBuilder is not constructable"); - } - PODGroupSpecBuilder.#isInternalConstructing = false; +class PODGroupSpecBuilder

{ + readonly #spec: PODGroupSpec; + + private constructor(spec: PODGroupSpec) { this.#spec = spec; } - public static create() { - // JavaScript does not have true private constructors, so we use a static - // variable to prevent construction. - PODGroupSpecBuilder.#isInternalConstructing = true; + // eslint-disable-next-line @typescript-eslint/no-empty-object-type + public static create(): PODGroupSpecBuilder<{}, {}> { return new PODGroupSpecBuilder({ pods: {}, statements: {} }); } - public spec(): PODGroupSpec { + public spec(): PODGroupSpec { return structuredClone(this.#spec); } public pod< N extends PODName, Spec extends PODSpec, - NewPods extends AddPOD - >(name: N, spec: Spec): PODGroupSpecBuilder { + NewPods extends AddPOD + >(name: N, spec: Spec): PODGroupSpecBuilder { if (name in this.#spec.pods) { throw new Error(`POD "${name}" already exists`); } @@ -89,32 +91,104 @@ class PODGroupSpecBuilder< }); } - public isMemberOf( - name: N - ): PODGroupSpecBuilder { + public isMemberOf>>( + names: [...N], + values: N["length"] extends 1 + ? PODValueTypeFromTypeName>>[] + : PODValueTupleForNamedEntries, N>[] + ): PODGroupSpecBuilder { + // Check that all names exist in entries + for (const name of names) { + const [podName, entryName] = name.split("."); + if ( + podName === undefined || + entryName === undefined || + !(podName in this.#spec.pods) || + !(entryName in this.#spec.pods[podName]!.entries) + ) { + 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 statement: IsMemberOf, N> = { + entries: names, + type: "isMemberOf", + // Wrap single values in arrays to match the expected tuple format + isMemberOf: (names.length === 1 + ? ( + values as PODValueTypeFromTypeName< + EntryType> + >[] + ).map((v) => [v]) + : values) as PODValueTupleForNamedEntries, N>[] + }; + + const baseName = `${names.join("_")}_isMemberOf`; + let statementName = baseName; + let suffix = 1; + + while (statementName in this.#spec.statements) { + statementName = `${baseName}_${suffix++}`; + } + return new PODGroupSpecBuilder({ ...this.#spec, statements: { ...this.#spec.statements, - [name]: { type: "isMemberOf", isMemberOf: name } + [statementName]: statement } }); } } if (import.meta.vitest) { - const { it, expect } = import.meta.vitest; + const { it, expect, assertType } = import.meta.vitest; it("PODGroupSpecBuilder", () => { const group = PODGroupSpecBuilder.create(); const podBuilder = PODSpecBuilder.create().entry("my_string", "string"); - const group2 = group.pod("foo", podBuilder.spec()); - const spec = group2.spec(); - expect(group2.spec()).toEqual({ + 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.$signerPublicKey": "string", + "foo.$contentID": "string", + "foo.$signature": "string" + }); + + 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" + } + } + }); }); } diff --git a/packages/podspec/src/builders/pod.ts b/packages/podspec/src/builders/pod.ts index 08570ec..ab996c6 100644 --- a/packages/podspec/src/builders/pod.ts +++ b/packages/podspec/src/builders/pod.ts @@ -5,12 +5,29 @@ import { POD_DATE_MAX, POD_DATE_MIN, POD_INT_MAX, - POD_INT_MIN, - type PODName, - type PODValue + POD_INT_MIN } from "@pcd/pod"; import type { PODValueType } from "../types/utils.js"; import { deepFreeze, validateRange } from "./shared.js"; +import type { + IsMemberOf, + IsNotMemberOf, + InRange, + NotInRange, + EqualsEntry, + NotEqualsEntry, + SupportsRangeChecks, + StatementMap, + StatementName +} from "./types/statements.js"; +import type { + EntriesOfType, + EntryKeys, + EntryTypes, + PODValueTupleForNamedEntries, + PODValueTypeFromTypeName, + VirtualEntries +} from "./types/entries.js"; /** @todo @@ -35,57 +52,11 @@ const virtualEntries: VirtualEntries = { $signerPublicKey: { type: "eddsa_pubkey" } }; -type VirtualEntries = { - $contentID: { type: "string" }; - $signature: { type: "string" }; - $signerPublicKey: { type: "eddsa_pubkey" }; -}; - export type PODSpec = { entries: E; statements: S; }; -export type EntryTypes = Record; - -export type EntryKeys = (keyof E & string)[]; - -export type PODValueTupleForNamedEntries< - E extends EntryTypes, - Names extends EntryKeys -> = { - [K in keyof Names]: PODValueTypeFromTypeName; -}; - -type PODValueTypeFromTypeName = Extract< - PODValue, - { type: T } ->["value"]; - -type EntriesOfType = { - [P in keyof E as E[P] extends T ? P & string : never]: E[P]; -}; - -/** - * @TODO Consider not having the E type parameter here. - * We can practically constrain the entry names using the constraint method - * signature, and then store a lighter-weight type that just lists the entry - * names used, without keeping a reference to the entry type list. - */ - -type IsMemberOf & string[]> = { - entries: N; - type: "isMemberOf"; - isMemberOf: PODValueTupleForNamedEntries[]; -}; - -type IsNotMemberOf> = { - entries: N; - type: "isNotMemberOf"; - isNotMemberOf: PODValueTupleForNamedEntries[]; -}; - -type SupportsRangeChecks = "int" | "boolean" | "date"; type DoesNotSupportRangeChecks = Exclude; function supportsRangeChecks(type: PODValueType): type is SupportsRangeChecks { @@ -102,68 +73,6 @@ function supportsRangeChecks(type: PODValueType): type is SupportsRangeChecks { } } -type InRange< - E extends EntryTypes, - N extends keyof EntriesOfType -> = { - entry: N; - type: "inRange"; - inRange: { - min: E[N] extends "date" ? Date : bigint; - max: E[N] extends "date" ? Date : bigint; - }; -}; - -type NotInRange< - E extends EntryTypes, - N extends keyof EntriesOfType -> = { - entry: N; - type: "notInRange"; - notInRange: { - min: E[N] extends "date" ? Date : bigint; - max: E[N] extends "date" ? Date : bigint; - }; -}; - -type EqualsEntry< - E extends EntryTypes, - N1 extends keyof (E & VirtualEntries), - N2 extends keyof (E & VirtualEntries) -> = E[N2] extends E[N1] - ? { - entry: N1; - type: "equalsEntry"; - equalsEntry: N2; - } - : never; - -type NotEqualsEntry< - E extends EntryTypes, - N1 extends keyof (E & VirtualEntries), - N2 extends keyof (E & VirtualEntries) -> = E[N2] extends E[N1] - ? { - entry: N1; - type: "notEqualsEntry"; - notEqualsEntry: N2; - } - : never; - -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; - /** * Given a list of entry names, return the names of the entries that are not in the list */ @@ -211,41 +120,6 @@ type AddEntry< V extends PODValueType > = Concrete; -// 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; - -type StatementName< - N extends readonly string[], - S extends Statements["type"], - Map extends StatementMap -> = NextAvailableSuffix, Map>; - -// Base constraint map -export type StatementMap = Record; - export class PODSpecBuilder< E extends EntryTypes, // eslint-disable-next-line @typescript-eslint/no-empty-object-type @@ -385,7 +259,12 @@ export class PODSpecBuilder< : PODValueTupleForNamedEntries[] ): PODSpecBuilder< E, - S & { [K in StatementName]: IsMemberOf } + S & { + [K in StatementName]: IsMemberOf< + E & VirtualEntries, + N + >; + } > { // Check that all names exist in entries for (const name of names) { @@ -400,7 +279,7 @@ export class PODSpecBuilder< throw new Error("Duplicate entry names are not allowed"); } - const statement: IsMemberOf = { + const statement: IsMemberOf = { entries: names, type: "isMemberOf", // Wrap single values in arrays to match the expected tuple format @@ -847,11 +726,11 @@ type TestStatements = { type PickedKeys = ["b"]; // First, let's see what OmittedEntryNames gives us -type TestOmitted = OmittedEntryNames; +type _TestOmitted = OmittedEntryNames; // Should be: "c" // Now let's test NonOverlapping -type TestNonOverlapping = NonOverlappingStatements; +type _TestNonOverlapping = NonOverlappingStatements; // Let's see what we get when picking just 'a' -type TestPickA = NonOverlappingStatements; +type _TestPickA = NonOverlappingStatements; diff --git a/packages/podspec/src/builders/types/entries.ts b/packages/podspec/src/builders/types/entries.ts new file mode 100644 index 0000000..f30e460 --- /dev/null +++ b/packages/podspec/src/builders/types/entries.ts @@ -0,0 +1,28 @@ +import type { PODName, PODValue } from "@pcd/pod"; +import type { PODValueType } from "../../types/utils.js"; + +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: { type: "string" }; + $signature: { type: "string" }; + $signerPublicKey: { type: "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..4c7f674 --- /dev/null +++ b/packages/podspec/src/builders/types/statements.ts @@ -0,0 +1,130 @@ +import type { + EntryTypes, + EntryKeys, + PODValueTupleForNamedEntries, + EntriesOfType, + VirtualEntries +} from "./entries.js"; + +/**************************************************************************** + * Statements + ****************************************************************************/ + +export type IsMemberOf< + E extends EntryTypes, + N extends EntryKeys & string[] +> = { + entries: N; + type: "isMemberOf"; + isMemberOf: PODValueTupleForNamedEntries[]; +}; + +export type IsNotMemberOf> = { + entries: N; + type: "isNotMemberOf"; + isNotMemberOf: PODValueTupleForNamedEntries[]; +}; + +// Which entry types support range checks? +export type SupportsRangeChecks = "int" | "boolean" | "date"; + +export type InRange< + E extends EntryTypes, + N extends keyof EntriesOfType +> = { + entry: N; + type: "inRange"; + inRange: { + min: E[N] extends "date" ? Date : bigint; + max: E[N] extends "date" ? Date : bigint; + }; +}; + +export type NotInRange< + E extends EntryTypes, + N extends keyof EntriesOfType +> = { + entry: N; + type: "notInRange"; + notInRange: { + min: E[N] extends "date" ? Date : bigint; + max: E[N] extends "date" ? Date : bigint; + }; +}; + +export type EqualsEntry< + E extends EntryTypes, + N1 extends keyof (E & VirtualEntries), + N2 extends keyof (E & VirtualEntries) +> = E[N2] extends E[N1] + ? { + entry: N1; + type: "equalsEntry"; + equalsEntry: N2; + } + : never; + +export type NotEqualsEntry< + E extends EntryTypes, + N1 extends keyof (E & VirtualEntries), + N2 extends keyof (E & VirtualEntries) +> = E[N2] extends E[N1] + ? { + entry: N1; + type: "notEqualsEntry"; + notEqualsEntry: N2; + } + : never; + +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; + +// Base map of named statements +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/processors/validate.ts b/packages/podspec/src/processors/validate.ts index 2928eae..134a213 100644 --- a/packages/podspec/src/processors/validate.ts +++ b/packages/podspec/src/processors/validate.ts @@ -6,17 +6,23 @@ import { type PODStringValue, type PODName } from "@pcd/pod"; +import { PODSpecBuilder, type PODSpec } from "../builders/pod.js"; +import type { EntryTypes } from "../builders/types/entries.js"; +import type { StatementMap } from "../builders/types/statements.js"; +import type { ValidateResult } from "./validate/types.js"; +import { FAILURE, SUCCESS } from "./validate/result.js"; import { - PODSpecBuilder, - type EntryTypes, - type PODSpec, - type StatementMap -} from "../builders/pod.js"; + IssueCode, + type ValidationMissingEntryIssue, + type ValidationTypeMismatchIssue, + type ValidationUnexpectedInputEntryIssue +} from "./validate/issues.js"; +import { checkIsMemberOf } from "./validate/checks/isMemberOf.js"; +import { checkIsNotMemberOf } from "./validate/checks/isNotMemberOf.js"; /** @TOOO - - [ ] Return a typed POD if validation succeeds. - - [ ] "Compile" a spec by hashing the statement parameters where necessary. + - [ ] "Compile" a spec by hashing the statement parameters where necessary? */ /** @@ -44,6 +50,22 @@ type PODEntriesFromEntryTypes = { [K in keyof E]: Extract; }; +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 +}; + /** * Validate a POD against a PODSpec. * @@ -53,48 +75,91 @@ type PODEntriesFromEntryTypes = { */ function validatePOD( pod: POD, - spec: PODSpec -): pod is StrongPOD> { + spec: PODSpec, + options: ValidateOptions = DEFAULT_VALIDATE_OPTIONS +): ValidateResult>> { const podEntries = pod.content.asEntries(); + const issues = []; + const path: string[] = []; + for (const [key, entryType] of Object.entries(spec.entries)) { if (!(key in podEntries)) { - console.error(`Entry ${key} not found in pod`); - return false; + const issue = { + code: IssueCode.missing_entry, + path: [...path, key], + key + } satisfies ValidationMissingEntryIssue; + if (options.strict) { + return FAILURE([issue]); + } else { + issues.push(issue); + } } if (podEntries[key]?.type !== entryType) { - console.error( - `Entry ${key} type mismatch: ${podEntries[key]?.type} !== ${entryType}` - ); - return false; + const issue = { + code: IssueCode.type_mismatch, + path: [...path, key], + expectedType: entryType + } satisfies ValidationTypeMismatchIssue; + if (options.strict) { + return FAILURE([issue]); + } else { + issues.push(issue); + } + } + } + + if (options.strict) { + for (const key in podEntries) { + if (!(key in spec.entries)) { + const issue = { + code: IssueCode.unexpected_input_entry, + path: [...path, key], + key + } satisfies ValidationUnexpectedInputEntryIssue; + return FAILURE([issue]); + } } } for (const [key, statement] of Object.entries(spec.statements)) { switch (statement.type) { case "isMemberOf": - const tuple = statement.entries.map( - (entry) => podEntries[entry]?.value + issues.push( + ...checkIsMemberOf( + statement, + key, + path, + podEntries, + spec.entries, + options.exitOnError ?? false + ) ); - for (const listMember of statement.isMemberOf) { - if ( - listMember.some((value, index) => { - return value === tuple[index]; - }) - ) { - break; - } - console.error( - `Statement ${key} failed: could not find ${statement.entries.join( - ", " - )} in isMemberOf list` - ); - return false; - } + break; + case "isNotMemberOf": + issues.push( + ...checkIsNotMemberOf( + statement, + key, + path, + podEntries, + spec.entries, + options.exitOnError ?? false + ) + ); + break; + default: + // prettier-ignore + statement.type satisfies never; + // maybe throw an exception here + } + if (options.exitOnError && issues.length > 0) { + return FAILURE(issues); } } - return true; + return SUCCESS(pod as StrongPOD>); } if (import.meta.vitest) { @@ -114,18 +179,20 @@ if (import.meta.vitest) { // This should pass because the entry "foo" is in the list ["foo", "bar"] expect(validatePOD(myPOD, myPodSpecBuilder.spec())).toBe(true); - if (validatePOD(myPOD, myPodSpecBuilder.spec())) { + const result = validatePOD(myPOD, myPodSpecBuilder.spec()); + if (result.isValid) { + const pod = result.value; // After validation, the entries are strongly typed - myPOD.content.asEntries().bar?.value satisfies + pod.content.asEntries().bar?.value satisfies | PODValue["value"] | undefined; - myPOD.content.asEntries().foo.value satisfies string; - myPOD.content.getValue("bar")?.value satisfies + pod.content.asEntries().foo.value satisfies string; + pod.content.getValue("bar")?.value satisfies | PODValue["value"] | undefined; - myPOD.content.getRawValue("bar") satisfies PODValue["value"] | undefined; - myPOD.content.getValue("foo") satisfies PODStringValue; - myPOD.content.getRawValue("foo") satisfies string; + 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"] diff --git a/packages/podspec/src/processors/validate/checks/inRange.ts b/packages/podspec/src/processors/validate/checks/inRange.ts new file mode 100644 index 0000000..71edf71 --- /dev/null +++ b/packages/podspec/src/processors/validate/checks/inRange.ts @@ -0,0 +1,49 @@ +import { isPODArithmeticValue, type PODEntries } from "@pcd/pod"; +import type { InRange } from "../../../builders/types/statements.js"; +import { + IssueCode, + type ValidationBaseIssue, + type ValidationStatementNegativeResultIssue +} from "../issues.js"; +import type { EntryTypes } from "../../../builders/types/entries.js"; + +export function checkInRange( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + statement: InRange, + statementName: string, + path: string[], + entries: PODEntries, + specEntries: EntryTypes, + exitOnError: boolean +): ValidationBaseIssue[] { + const entryName = statement.entry; + const entry = entries[entryName]!; + + // @TODO need an issue type for statement referring to a non-existent entry + // or entry of the wrong type + + if (isPODArithmeticValue(entry)) { + const value = entry.value; + // @TODO date comparison? + // maybe the spec should convert dates to bigints, and we also convert + // dates to bigints here, so we have a consistent way to compare dates + // correct framing here is "how do we serialize statement parameters", + // followed by "how do we deserialize statement parameters into the + // format required by the processor". + // so the specifier provides, say, Date objects. the builder may serialize + // those as strings or bigints. the processor needs bigints. + if (value < statement.inRange.min || value > statement.inRange.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/checks/isMemberOf.ts b/packages/podspec/src/processors/validate/checks/isMemberOf.ts new file mode 100644 index 0000000..ee4e879 --- /dev/null +++ b/packages/podspec/src/processors/validate/checks/isMemberOf.ts @@ -0,0 +1,76 @@ +import type { PODEntries } from "@pcd/pod"; +import type { + ValidationBaseIssue, + ValidationInvalidStatementIssue, + ValidationStatementNegativeResultIssue +} from "../issues.js"; +import { IssueCode } from "../issues.js"; +import type { IsMemberOf } from "../../../builders/types/statements.js"; +import type { EntryTypes } from "../../../builders/types/entries.js"; + +function validateIsMemberOfStatement( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + statement: IsMemberOf, + statementName: string, + path: string[], + specEntries: EntryTypes +): ValidationInvalidStatementIssue[] { + if (statement.entries.some((entry) => !(entry in specEntries))) { + 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[], + podEntries: PODEntries, + specEntries: EntryTypes, + exitOnError: boolean +): ValidationBaseIssue[] { + const issues: ValidationBaseIssue[] = validateIsMemberOfStatement( + statement, + statementName, + path, + specEntries + ); + if (issues.length > 0) { + // Can't proceed if there are issues with the statement + return issues; + } + + const tuple = statement.entries.map((entry) => podEntries[entry]?.value); + + for (const listMember of statement.isMemberOf) { + if ( + listMember.some((value, index) => { + return value === tuple[index]; + }) + ) { + break; + } + 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/isNotMemberOf.ts b/packages/podspec/src/processors/validate/checks/isNotMemberOf.ts new file mode 100644 index 0000000..60b0422 --- /dev/null +++ b/packages/podspec/src/processors/validate/checks/isNotMemberOf.ts @@ -0,0 +1,77 @@ +import type { PODEntries } from "@pcd/pod"; +import { + IssueCode, + type ValidationBaseIssue, + type ValidationInvalidStatementIssue, + type ValidationStatementNegativeResultIssue +} from "../issues.js"; +import type { IsNotMemberOf } from "../../../builders/types/statements.js"; +import type { EntryTypes } from "../../../builders/types/entries.js"; + +function validateIsNotMemberOfStatement( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + statement: IsNotMemberOf, + statementName: string, + path: string[], + specEntries: EntryTypes +): ValidationInvalidStatementIssue[] { + if (statement.entries.some((entry) => !(entry in specEntries))) { + 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[], + entries: PODEntries, + specEntries: EntryTypes, + exitOnError: boolean +): ValidationBaseIssue[] { + const issues: ValidationBaseIssue[] = validateIsNotMemberOfStatement( + statement, + statementName, + path, + specEntries + ); + + // Can't proceed if there are any issues with the statement + if (issues.length > 0) { + return issues; + } + + const tuple = statement.entries.map((entry) => entries[entry]?.value); + + for (const listMember of statement.isNotMemberOf) { + if ( + listMember.every((value, index) => { + return value !== tuple[index]; + }) + ) { + break; + } + 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/issues.ts b/packages/podspec/src/processors/validate/issues.ts new file mode 100644 index 0000000..bbffce1 --- /dev/null +++ b/packages/podspec/src/processors/validate/issues.ts @@ -0,0 +1,110 @@ +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", + invalid_entry_name: "invalid_entry_name", + invalid_pod_value: "invalid_pod_value", + invalid_statement: "invalid_statement", + unexpected_input_entry: "unexpected_input_entry", + 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 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 a statement fails. + */ +export interface ValidationStatementNegativeResultIssue + extends ValidationBaseIssue { + code: typeof IssueCode.statement_negative_result; + statementName: string; + statementType: Statements["type"]; + entries: string[]; +} + +/** + * Exception class for errors that occur when parsing. + */ +export class ValidationError extends Error { + issues: ValidationBaseIssue[] = []; + + public get errors(): ValidationBaseIssue[] { + return this.issues; + } + + constructor(issues: ValidationBaseIssue[]) { + super(); + this.name = "ValidationError"; + this.issues = issues; + } +} diff --git a/packages/podspec/src/processors/validate/result.ts b/packages/podspec/src/processors/validate/result.ts new file mode 100644 index 0000000..554143c --- /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..64a9c8d --- /dev/null +++ b/packages/podspec/src/processors/validate/types.ts @@ -0,0 +1,13 @@ +import type { ValidationBaseIssue } from "./issues.js"; + +export type ValidateSuccess = { + value: T; + isValid: true; +}; + +export type ValidateFailure = { + issues: ValidationBaseIssue[]; + isValid: false; +}; + +export type ValidateResult = ValidateSuccess | ValidateFailure; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 7b92091..67a5611 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -461,46 +461,6 @@ importers: 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) - packages/podspec3: - dependencies: - '@pcd/gpc': - specifier: ^0.4.0 - version: 0.4.0(typescript@5.6.2) - '@pcd/pod': - specifier: ^0.5.0 - version: 0.5.0 - devDependencies: - '@parcnet-js/eslint-config': - specifier: workspace:* - version: link:../eslint-config - '@parcnet-js/typescript-config': - specifier: workspace:* - version: link:../typescript-config - '@pcd/proto-pod-gpc-artifacts': - specifier: ^0.11.0 - version: 0.11.0 - '@semaphore-protocol/identity': - specifier: ^3.15.2 - version: 3.15.2 - '@types/uuid': - specifier: ^9.0.0 - version: 9.0.8 - '@zk-kit/eddsa-poseidon': - specifier: ^1.0.3 - version: 1.0.4 - tsup: - specifier: ^8.2.4 - version: 8.2.4(jiti@1.21.6)(postcss@8.4.44)(tsx@4.19.0)(typescript@5.6.2)(yaml@2.5.1) - typescript: - specifier: ^5.5 - version: 5.6.2 - uuid: - specifier: ^9.0.0 - version: 9.0.1 - vitest: - specifier: ^2.1.1 - version: 2.1.2(@types/node@22.5.5)(sass@1.80.3)(terser@5.34.1) - packages/ticket-spec: dependencies: '@parcnet-js/client-rpc': From baac297e32947258b65a1222e7514d97ee3be87b Mon Sep 17 00:00:00 2001 From: Rob Knight Date: Wed, 29 Jan 2025 11:14:45 +0100 Subject: [PATCH 05/20] Try out Typia --- packages/eslint-config/eslint.base.config.mjs | 1 + packages/podspec/package.json | 4 +- packages/podspec/src/generated/podspec.ts | 919 ++++++++++++++++++ packages/podspec/src/typia/podspec.ts | 7 + pnpm-lock.yaml | 231 ++++- 5 files changed, 1157 insertions(+), 5 deletions(-) create mode 100644 packages/podspec/src/generated/podspec.ts create mode 100644 packages/podspec/src/typia/podspec.ts diff --git a/packages/eslint-config/eslint.base.config.mjs b/packages/eslint-config/eslint.base.config.mjs index d543764..3ce407f 100644 --- a/packages/eslint-config/eslint.base.config.mjs +++ b/packages/eslint-config/eslint.base.config.mjs @@ -12,6 +12,7 @@ export default tseslint.config( { ignores: [ + "**/src/generated/**", "**/node_modules/*", "**/dist/", "**/vitest.config.ts", diff --git a/packages/podspec/package.json b/packages/podspec/package.json index 7d81d45..3ea0ca9 100644 --- a/packages/podspec/package.json +++ b/packages/podspec/package.json @@ -37,7 +37,9 @@ "files": ["dist", "./README.md", "./LICENSE"], "dependencies": { "@pcd/gpc": "^0.4.0", - "@pcd/pod": "^0.5.0" + "@pcd/pod": "^0.5.0", + "canonicalize": "^2.0.0", + "typia": "^7.6.0" }, "devDependencies": { "@parcnet-js/eslint-config": "workspace:*", diff --git a/packages/podspec/src/generated/podspec.ts b/packages/podspec/src/generated/podspec.ts new file mode 100644 index 0000000..15d1c60 --- /dev/null +++ b/packages/podspec/src/generated/podspec.ts @@ -0,0 +1,919 @@ +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 { StatementMap } from "../builders/types/statements.js"; +import type { EntryTypes } from "../builders/types/entries.js"; +import type { PODSpec } from "../builders/pod.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) => + undefined !== elem && + (null === elem || + "string" === typeof elem || + "bigint" === typeof elem || + "boolean" === typeof elem || + elem instanceof Uint8Array || + elem instanceof Date) + ) + ); + 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) => + undefined !== elem && + (null === elem || + "string" === typeof elem || + "bigint" === typeof elem || + "boolean" === typeof elem || + elem instanceof Uint8Array || + elem instanceof Date) + ) + ); + const _io5 = (input: any): boolean => + "string" === typeof input.entry && + "inRange" === input.type && + "object" === typeof input.inRange && null !== input.inRange && + _io6(input.inRange); + const _io6 = (input: any): boolean => + null !== input.min && + undefined !== input.min && + ("bigint" === typeof input.min || input.min instanceof Date) && + null !== input.max && undefined !== input.max && + ("bigint" === typeof input.max || input.max instanceof Date); + const _io7 = (input: any): boolean => + "string" === typeof input.entry && + "notInRange" === input.type && + "object" === typeof input.notInRange && null !== input.notInRange && + _io8(input.notInRange); + const _io8 = (input: any): boolean => + null !== input.min && + undefined !== input.min && + ("bigint" === typeof input.min || input.min instanceof Date) && + null !== input.max && undefined !== input.max && + ("bigint" === typeof input.max || input.max instanceof Date); + const _io9 = (input: any): boolean => + "string" === typeof input.entry && + "equalsEntry" === input.type && + "string" === typeof input.equalsEntry; + const _io10 = (input: any): boolean => + "string" === typeof input.entry && + "notEqualsEntry" === input.type && + "string" === typeof input.notEqualsEntry; + 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 ("equalsEntry" === input.type) return _io9(input); + else if ("notEqualsEntry" === input.type) return _io10(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: + "(InRange | IsMemberOf> | IsNotMemberOf> | NotInRange | __type.o2 | __type.o3)", + 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: + "(InRange | IsMemberOf> | IsNotMemberOf> | NotInRange | __type.o2 | __type.o3)", + 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) => + (undefined !== elem || + __typia_transform__assertGuard._assertGuard( + _exceptionable, + { + method: "typia.createAssert", + path: + _path + ".isMemberOf[" + _index8 + "][" + _index9 + "]", + expected: + "(Date | Uint8Array | bigint | boolean | null | string)", + value: elem + }, + _errorFactory + )) && + (null === elem || + "string" === typeof elem || + "bigint" === typeof elem || + "boolean" === typeof elem || + elem instanceof Uint8Array || + elem instanceof Date || + __typia_transform__assertGuard._assertGuard( + _exceptionable, + { + method: "typia.createAssert", + path: + _path + ".isMemberOf[" + _index8 + "][" + _index9 + "]", + expected: + "(Date | Uint8Array | bigint | boolean | null | 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) => + (undefined !== elem || + __typia_transform__assertGuard._assertGuard( + _exceptionable, + { + method: "typia.createAssert", + path: + _path + + ".isNotMemberOf[" + + _index11 + + "][" + + _index12 + + "]", + expected: + "(Date | Uint8Array | bigint | boolean | null | string)", + value: elem + }, + _errorFactory + )) && + (null === elem || + "string" === typeof elem || + "bigint" === typeof elem || + "boolean" === typeof elem || + elem instanceof Uint8Array || + elem instanceof Date || + __typia_transform__assertGuard._assertGuard( + _exceptionable, + { + method: "typia.createAssert", + path: + _path + + ".isNotMemberOf[" + + _index11 + + "][" + + _index12 + + "]", + expected: + "(Date | Uint8Array | bigint | boolean | null | 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 => + ("string" === typeof input.entry || + __typia_transform__assertGuard._assertGuard( + _exceptionable, + { + method: "typia.createAssert", + path: _path + ".entry", + expected: "string", + value: input.entry + }, + _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: "__type", + value: input.inRange + }, + _errorFactory + )) && + _ao6(input.inRange, _path + ".inRange", true && _exceptionable)) || + __typia_transform__assertGuard._assertGuard( + _exceptionable, + { + method: "typia.createAssert", + path: _path + ".inRange", + expected: "__type", + value: input.inRange + }, + _errorFactory + )); + const _ao6 = ( + input: any, + _path: string, + _exceptionable: boolean = true + ): boolean => + (null !== input.min || + __typia_transform__assertGuard._assertGuard( + _exceptionable, + { + method: "typia.createAssert", + path: _path + ".min", + expected: "(Date | bigint)", + value: input.min + }, + _errorFactory + )) && + (undefined !== input.min || + __typia_transform__assertGuard._assertGuard( + _exceptionable, + { + method: "typia.createAssert", + path: _path + ".min", + expected: "(Date | bigint)", + value: input.min + }, + _errorFactory + )) && + ("bigint" === typeof input.min || + input.min instanceof Date || + __typia_transform__assertGuard._assertGuard( + _exceptionable, + { + method: "typia.createAssert", + path: _path + ".min", + expected: "(Date | bigint)", + value: input.min + }, + _errorFactory + )) && + (null !== input.max || + __typia_transform__assertGuard._assertGuard( + _exceptionable, + { + method: "typia.createAssert", + path: _path + ".max", + expected: "(Date | bigint)", + value: input.max + }, + _errorFactory + )) && + (undefined !== input.max || + __typia_transform__assertGuard._assertGuard( + _exceptionable, + { + method: "typia.createAssert", + path: _path + ".max", + expected: "(Date | bigint)", + value: input.max + }, + _errorFactory + )) && + ("bigint" === typeof input.max || + input.max instanceof Date || + __typia_transform__assertGuard._assertGuard( + _exceptionable, + { + method: "typia.createAssert", + path: _path + ".max", + expected: "(Date | bigint)", + value: input.max + }, + _errorFactory + )); + const _ao7 = ( + input: any, + _path: string, + _exceptionable: boolean = true + ): boolean => + ("string" === typeof input.entry || + __typia_transform__assertGuard._assertGuard( + _exceptionable, + { + method: "typia.createAssert", + path: _path + ".entry", + expected: "string", + value: input.entry + }, + _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: "__type.o1", + value: input.notInRange + }, + _errorFactory + )) && + _ao8(input.notInRange, _path + ".notInRange", true && _exceptionable)) || + __typia_transform__assertGuard._assertGuard( + _exceptionable, + { + method: "typia.createAssert", + path: _path + ".notInRange", + expected: "__type.o1", + value: input.notInRange + }, + _errorFactory + )); + const _ao8 = ( + input: any, + _path: string, + _exceptionable: boolean = true + ): boolean => + (null !== input.min || + __typia_transform__assertGuard._assertGuard( + _exceptionable, + { + method: "typia.createAssert", + path: _path + ".min", + expected: "(Date | bigint)", + value: input.min + }, + _errorFactory + )) && + (undefined !== input.min || + __typia_transform__assertGuard._assertGuard( + _exceptionable, + { + method: "typia.createAssert", + path: _path + ".min", + expected: "(Date | bigint)", + value: input.min + }, + _errorFactory + )) && + ("bigint" === typeof input.min || + input.min instanceof Date || + __typia_transform__assertGuard._assertGuard( + _exceptionable, + { + method: "typia.createAssert", + path: _path + ".min", + expected: "(Date | bigint)", + value: input.min + }, + _errorFactory + )) && + (null !== input.max || + __typia_transform__assertGuard._assertGuard( + _exceptionable, + { + method: "typia.createAssert", + path: _path + ".max", + expected: "(Date | bigint)", + value: input.max + }, + _errorFactory + )) && + (undefined !== input.max || + __typia_transform__assertGuard._assertGuard( + _exceptionable, + { + method: "typia.createAssert", + path: _path + ".max", + expected: "(Date | bigint)", + value: input.max + }, + _errorFactory + )) && + ("bigint" === typeof input.max || + input.max instanceof Date || + __typia_transform__assertGuard._assertGuard( + _exceptionable, + { + method: "typia.createAssert", + path: _path + ".max", + expected: "(Date | bigint)", + value: input.max + }, + _errorFactory + )); + const _ao9 = ( + input: any, + _path: string, + _exceptionable: boolean = true + ): boolean => + ("string" === typeof input.entry || + __typia_transform__assertGuard._assertGuard( + _exceptionable, + { + method: "typia.createAssert", + path: _path + ".entry", + expected: "string", + value: input.entry + }, + _errorFactory + )) && + ("equalsEntry" === input.type || + __typia_transform__assertGuard._assertGuard( + _exceptionable, + { + method: "typia.createAssert", + path: _path + ".type", + expected: '"equalsEntry"', + value: input.type + }, + _errorFactory + )) && + ("string" === typeof input.equalsEntry || + __typia_transform__assertGuard._assertGuard( + _exceptionable, + { + method: "typia.createAssert", + path: _path + ".equalsEntry", + expected: "string", + value: input.equalsEntry + }, + _errorFactory + )); + const _ao10 = ( + input: any, + _path: string, + _exceptionable: boolean = true + ): boolean => + ("string" === typeof input.entry || + __typia_transform__assertGuard._assertGuard( + _exceptionable, + { + method: "typia.createAssert", + path: _path + ".entry", + expected: "string", + value: input.entry + }, + _errorFactory + )) && + ("notEqualsEntry" === input.type || + __typia_transform__assertGuard._assertGuard( + _exceptionable, + { + method: "typia.createAssert", + path: _path + ".type", + expected: '"notEqualsEntry"', + value: input.type + }, + _errorFactory + )) && + ("string" === typeof input.notEqualsEntry || + __typia_transform__assertGuard._assertGuard( + _exceptionable, + { + method: "typia.createAssert", + path: _path + ".notEqualsEntry", + expected: "string", + value: input.notEqualsEntry + }, + _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 ("equalsEntry" === input.type) + return _ao9(input, _path, true && _exceptionable); + else if ("notEqualsEntry" === 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 | __type.o2 | __type.o3)", + 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; + }; +})(); diff --git a/packages/podspec/src/typia/podspec.ts b/packages/podspec/src/typia/podspec.ts new file mode 100644 index 0000000..dcb244a --- /dev/null +++ b/packages/podspec/src/typia/podspec.ts @@ -0,0 +1,7 @@ +import typia from "typia"; +import type { StatementMap } from "../builders/types/statements.js"; +import type { EntryTypes } from "../builders/types/entries.js"; +import type { PODSpec } from "../builders/pod.js"; + +export const assertPODSpec = + typia.createAssert>(); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 67a5611..3300aa9 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -429,6 +429,12 @@ importers: '@pcd/pod': specifier: ^0.5.0 version: 0.5.0 + 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: '@parcnet-js/eslint-config': specifier: workspace:* @@ -1761,6 +1767,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==} @@ -2211,6 +2220,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'} @@ -2269,6 +2282,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'} @@ -2488,6 +2504,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==} @@ -2570,6 +2589,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'} @@ -2578,6 +2601,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'} @@ -2620,6 +2647,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==} @@ -2627,6 +2658,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==} @@ -2843,6 +2878,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'} @@ -3193,6 +3232,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'} @@ -3366,6 +3409,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==} @@ -3535,6 +3582,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'} @@ -3622,6 +3673,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'} @@ -3685,6 +3740,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'} @@ -3873,6 +3932,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'} @@ -4185,6 +4248,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==} @@ -4314,6 +4380,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'} @@ -4645,6 +4715,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==} @@ -4801,6 +4875,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==} @@ -4830,10 +4908,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==} @@ -4858,9 +4944,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'} @@ -5212,6 +5305,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'} @@ -5395,6 +5491,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'} @@ -5458,6 +5558,13 @@ packages: 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==} @@ -5918,6 +6025,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'} @@ -7343,6 +7454,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 @@ -7996,6 +8109,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: {} @@ -8050,6 +8167,8 @@ snapshots: array-iterate@2.0.1: {} + array-timsort@1.0.3: {} + array-union@2.1.0: {} array.prototype.findlastindex@1.2.5: @@ -8408,6 +8527,8 @@ snapshots: caniuse-lite@1.0.30001655: {} + canonicalize@2.0.0: {} + ccount@2.0.1: {} chai@5.1.1: @@ -8493,20 +8614,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: {} @@ -8538,11 +8664,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: {} @@ -8682,7 +8818,6 @@ snapshots: defaults@1.0.4: dependencies: clone: 1.0.4 - optional: true define-data-property@1.1.4: dependencies: @@ -8751,6 +8886,8 @@ snapshots: dotenv@16.0.3: {} + drange@1.1.1: {} + dset@3.1.4: {} eastasianwidth@0.2.0: {} @@ -9251,6 +9388,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 @@ -9423,6 +9564,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 @@ -9693,6 +9836,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 @@ -9773,6 +9934,8 @@ snapshots: dependencies: is-docker: 3.0.0 + is-interactive@1.0.0: {} + is-interactive@2.0.0: {} is-nan@1.3.2: @@ -9825,6 +9988,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: {} @@ -9995,6 +10160,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 @@ -10584,6 +10754,8 @@ snapshots: muggle-string@0.4.1: {} + mute-stream@0.0.8: {} + mz@2.7.0: dependencies: any-promise: 1.3.0 @@ -10740,6 +10912,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 @@ -11075,6 +11259,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 @@ -11306,6 +11495,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: {} @@ -11327,11 +11518,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 @@ -11386,10 +11584,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 @@ -11888,6 +12092,8 @@ snapshots: dependencies: any-promise: 1.3.0 + through@2.3.8: {} + timers-browserify@2.0.12: dependencies: setimmediate: 1.0.5 @@ -12120,6 +12326,8 @@ snapshots: dependencies: prelude-ls: 1.2.1 + type-fest@0.21.3: {} + type-fest@2.19.0: {} typed-array-buffer@1.0.2: @@ -12190,6 +12398,16 @@ snapshots: 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: @@ -12707,7 +12925,6 @@ snapshots: wcwidth@1.0.1: dependencies: defaults: 1.0.4 - optional: true web-namespaces@2.0.1: {} @@ -12762,6 +12979,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 From 6975f41e91a13c87b05d254187aed6a89d984a86 Mon Sep 17 00:00:00 2001 From: Rob Knight Date: Wed, 29 Jan 2025 11:15:01 +0100 Subject: [PATCH 06/20] Clean up unused files --- packages/podspec/src/builders/entries.ts | 174 ----------------- packages/podspec/src/builders/entry.ts | 54 ----- packages/podspec/src/builders/group.ts | 4 +- packages/podspec/src/builders/pod.ts | 26 ++- packages/podspec/src/builders/shared.ts | 5 +- .../podspec/src/builders/types/entries.ts | 3 +- packages/podspec/src/data.ts | 10 - packages/podspec/src/error.ts | 184 ------------------ packages/podspec/src/group.ts | 118 ----------- packages/podspec/src/index.ts | 16 -- packages/podspec/src/processors/validate.ts | 3 + packages/podspec/src/schemas/boolean.ts | 54 ----- packages/podspec/src/schemas/bytes.ts | 55 ------ packages/podspec/src/schemas/cryptographic.ts | 81 -------- packages/podspec/src/schemas/dates.ts | 55 ------ packages/podspec/src/schemas/eddsa_pubkey.ts | 63 ------ packages/podspec/src/schemas/entries.ts | 16 -- packages/podspec/src/schemas/entry.ts | 118 ----------- packages/podspec/src/schemas/int.ts | 74 ------- packages/podspec/src/schemas/null.ts | 52 ----- packages/podspec/src/schemas/pod.ts | 33 ---- packages/podspec/src/schemas/string.ts | 58 ------ packages/podspec/src/types/entries.ts | 98 ---------- packages/podspec/src/types/group.ts | 116 ----------- packages/podspec/src/types/pod.ts | 62 ------ packages/podspec/src/types/utils.ts | 3 - 26 files changed, 36 insertions(+), 1499 deletions(-) delete mode 100644 packages/podspec/src/builders/entries.ts delete mode 100644 packages/podspec/src/builders/entry.ts delete mode 100644 packages/podspec/src/data.ts delete mode 100644 packages/podspec/src/error.ts delete mode 100644 packages/podspec/src/group.ts delete mode 100644 packages/podspec/src/schemas/boolean.ts delete mode 100644 packages/podspec/src/schemas/bytes.ts delete mode 100644 packages/podspec/src/schemas/cryptographic.ts delete mode 100644 packages/podspec/src/schemas/dates.ts delete mode 100644 packages/podspec/src/schemas/eddsa_pubkey.ts delete mode 100644 packages/podspec/src/schemas/entries.ts delete mode 100644 packages/podspec/src/schemas/entry.ts delete mode 100644 packages/podspec/src/schemas/int.ts delete mode 100644 packages/podspec/src/schemas/null.ts delete mode 100644 packages/podspec/src/schemas/pod.ts delete mode 100644 packages/podspec/src/schemas/string.ts delete mode 100644 packages/podspec/src/types/entries.ts delete mode 100644 packages/podspec/src/types/group.ts delete mode 100644 packages/podspec/src/types/pod.ts delete mode 100644 packages/podspec/src/types/utils.ts diff --git a/packages/podspec/src/builders/entries.ts b/packages/podspec/src/builders/entries.ts deleted file mode 100644 index 10d3630..0000000 --- a/packages/podspec/src/builders/entries.ts +++ /dev/null @@ -1,174 +0,0 @@ -import type { - EntriesSpec, - EntryListSpec, - EntrySpec -} from "../types/entries.js"; - -/** - * Converts a complex type expression into its concrete, simplified form. - * For example, converts Pick<{a: string}, 'a'> into {a: string} - */ -type Concrete = T extends object ? { [K in keyof T]: T[K] } : T; - -/** - * @todo add some type parameter to keep track of tuples - */ -export class EntriesSpecBuilder { - readonly #spec: EntriesSpec; - - private constructor(spec: EntriesSpec) { - this.#spec = spec; - } - - public static create(entries: E) { - return new EntriesSpecBuilder({ entries }); - } - - public add( - key: Exclude, - type: T - ): EntriesSpecBuilder> { - if (key in this.#spec.entries) { - throw new ReferenceError( - `Key ${key.toString()} already exists in entries: ${Object.keys( - this.#spec.entries - ).join(", ")}` - ); - } - return new EntriesSpecBuilder>({ - entries: { - ...this.#spec.entries, - [key]: type - } as Concrete - }); - } - - public pick( - keys: K - ): EntriesSpecBuilder>> { - return new EntriesSpecBuilder>>({ - entries: keys.reduce( - (acc, key) => { - if (!(key in this.#spec.entries)) { - throw new ReferenceError( - `Key ${key.toString()} not found in entries: ${Object.keys( - this.#spec.entries - ).join(", ")}` - ); - } - return { - ...acc, - [key]: this.#spec.entries[key] - }; - }, - {} as Concrete> - ) - }); - } - - public omit( - keys: K, - { strict = true }: { strict?: boolean } = {} - ): EntriesSpecBuilder>> { - if (strict) { - for (const key of keys) { - if (!(key in this.#spec.entries)) { - throw new ReferenceError( - `Key ${key.toString()} not found in entries: ${Object.keys( - this.#spec.entries - ).join(", ")}` - ); - } - } - } - return new EntriesSpecBuilder>>({ - entries: Object.fromEntries( - Object.entries(this.#spec.entries).filter( - ([key]) => !keys.includes(key) - ) - ) as Concrete> - }); - } - - public merge( - other: EntriesSpecBuilder - ): EntriesSpecBuilder> { - return new EntriesSpecBuilder>({ - entries: { - ...this.#spec.entries, - ...other.#spec.entries - } as Concrete - }); - } - - public build(): EntriesSpec { - return structuredClone(this.#spec); - } -} - -if (import.meta.vitest) { - const { describe, it, expect } = import.meta.vitest; - - describe("EntriesSpecBuilder", () => { - it("should add entries correctly", () => { - const nameAndAgeBuilder = EntriesSpecBuilder.create({ - name: { - type: "string" - } - }).add("age", { type: "int" }); - - expect(nameAndAgeBuilder.build()).toEqual({ - name: { type: "string" }, - age: { type: "int" } - }); - - nameAndAgeBuilder satisfies EntriesSpecBuilder<{ - name: { type: "string" }; - age: { type: "int" }; - }>; - - const nameBuilder = nameAndAgeBuilder.pick(["name"]); - nameBuilder satisfies EntriesSpecBuilder<{ - name: { type: "string" }; - }>; - // @ts-expect-error age is not in the builder - nameBuilder satisfies EntriesSpecBuilder<{ - age: { type: "int" }; - }>; - - expect(nameBuilder.build()).toEqual({ - name: { type: "string" } - }); - - // @ts-expect-error nonExistingKey will not type-check, but we want to - // test the error for cases where the caller is not using TypeScript - expect(() => nameAndAgeBuilder.pick(["nonExistingKey"])).to.throw( - ReferenceError - ); - - expect(nameAndAgeBuilder.omit(["name"]).build()).toEqual({ - age: { type: "int" } - }); - - expect( - nameAndAgeBuilder.omit(["name"], { strict: false }).build() - ).toEqual({ - age: { type: "int" } - }); - - nameAndAgeBuilder.omit(["name"]) satisfies EntriesSpecBuilder<{ - age: { type: "int" }; - }>; - - // @ts-expect-error nonExistingKey will not type-check, but we want to - // test the error for cases where the caller is not using TypeScript - expect(() => nameAndAgeBuilder.omit(["nonExistingKey"])).to.throw( - ReferenceError - ); - - expect(() => - nameAndAgeBuilder.omit(["name"], { strict: false }) - ).not.to.throw(ReferenceError); - }); - }); -} diff --git a/packages/podspec/src/builders/entry.ts b/packages/podspec/src/builders/entry.ts deleted file mode 100644 index eb64b3e..0000000 --- a/packages/podspec/src/builders/entry.ts +++ /dev/null @@ -1,54 +0,0 @@ -import { POD_INT_MAX, POD_INT_MIN, type PODIntValue } from "@pcd/pod/podTypes"; -import type { IntEntrySpec } from "../types/entries.js"; -import { checkPODValue } from "@pcd/pod/podChecks"; - -function validateRange( - min: bigint, - max: bigint, - allowedMin: bigint, - allowedMax: bigint -) { - 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"); - } -} - -export class IntEntrySpecBuilder { - private readonly spec: T; - - constructor(spec: T) { - this.spec = spec; - } - - public inRange(min: bigint, max: bigint): IntEntrySpecBuilder { - validateRange(min, max, POD_INT_MIN, POD_INT_MAX); - return new IntEntrySpecBuilder({ - ...this.spec, - inRange: { min, max } - }); - } - - public isMemberOf(values: PODIntValue[]): IntEntrySpecBuilder { - for (const value of values) { - checkPODValue("", value); - } - return new IntEntrySpecBuilder({ - ...this.spec, - isMemberOf: values - }); - } - - public isNotMemberOf(values: PODIntValue[]): IntEntrySpecBuilder { - return new IntEntrySpecBuilder({ - ...this.spec, - isNotMemberOf: values - }); - } - - public build(): T { - return this.spec; - } -} diff --git a/packages/podspec/src/builders/group.ts b/packages/podspec/src/builders/group.ts index d1a881c..819a245 100644 --- a/packages/podspec/src/builders/group.ts +++ b/packages/podspec/src/builders/group.ts @@ -1,12 +1,12 @@ import { checkPODName, type PODName } from "@pcd/pod"; -import type { PODValueType } from "../types/utils.js"; import { type PODSpec, PODSpecBuilder } from "./pod.js"; import type { EntryTypes, VirtualEntries, EntryKeys, PODValueTypeFromTypeName, - PODValueTupleForNamedEntries + PODValueTupleForNamedEntries, + PODValueType } from "./types/entries.js"; import type { StatementMap, IsMemberOf } from "./types/statements.js"; diff --git a/packages/podspec/src/builders/pod.ts b/packages/podspec/src/builders/pod.ts index ab996c6..9d0dd7b 100644 --- a/packages/podspec/src/builders/pod.ts +++ b/packages/podspec/src/builders/pod.ts @@ -7,7 +7,6 @@ import { POD_INT_MAX, POD_INT_MIN } from "@pcd/pod"; -import type { PODValueType } from "../types/utils.js"; import { deepFreeze, validateRange } from "./shared.js"; import type { IsMemberOf, @@ -25,9 +24,11 @@ import type { EntryKeys, EntryTypes, PODValueTupleForNamedEntries, + PODValueType, PODValueTypeFromTypeName, VirtualEntries } from "./types/entries.js"; +import canonicalize from "canonicalize/lib/canonicalize.js"; /** @todo @@ -44,8 +45,16 @@ import type { - [ ] handle multiple/incompatible range 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 */ +function canonicalizeJSON(input: unknown): string | undefined { + // Something is screwy with the typings for canonicalize + return (canonicalize as unknown as (input: unknown) => string | undefined)( + input + ); +} + const virtualEntries: VirtualEntries = { $contentID: { type: "string" }, $signature: { type: "string" }, @@ -142,6 +151,21 @@ export class PODSpecBuilder< return deepFreeze(this.#spec); } + public toJSON(): string { + const canonicalized = canonicalizeJSON(this.#spec); + if (!canonicalized) { + throw new Error("Failed to canonicalize PODSpec"); + } + return JSON.stringify( + { + ...this.#spec, + hash: canonicalized /* TODO hashing! */ + }, + null, + 2 + ); + } + public entry< K extends string, V extends PODValueType, diff --git a/packages/podspec/src/builders/shared.ts b/packages/podspec/src/builders/shared.ts index ddb826a..fa52e83 100644 --- a/packages/podspec/src/builders/shared.ts +++ b/packages/podspec/src/builders/shared.ts @@ -1,5 +1,5 @@ import { checkPODValue, type PODValue } from "@pcd/pod"; -import type { PODValueType } from "../types/utils.js"; +import type { PODValueType } from "./types/entries.js"; /** * Validates a range check. @@ -58,6 +58,9 @@ export function deepFreeze(obj: T): T { 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); } }); diff --git a/packages/podspec/src/builders/types/entries.ts b/packages/podspec/src/builders/types/entries.ts index f30e460..2b4efcb 100644 --- a/packages/podspec/src/builders/types/entries.ts +++ b/packages/podspec/src/builders/types/entries.ts @@ -1,5 +1,6 @@ import type { PODName, PODValue } from "@pcd/pod"; -import type { PODValueType } from "../../types/utils.js"; + +export type PODValueType = PODValue["type"]; export type EntryTypes = Record; 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/group.ts b/packages/podspec/src/group.ts deleted file mode 100644 index a789be1..0000000 --- a/packages/podspec/src/group.ts +++ /dev/null @@ -1,118 +0,0 @@ -/* eslint-disable @typescript-eslint/no-explicit-any */ -/* eslint-disable @typescript-eslint/array-type */ -import type { PODValue } from "@pcd/pod"; -import type { PodSpec } from "./parse/pod.js"; - -/* -@todo -- [ ] rebuild group builder along the same line as podv2 -*/ - -// This describes the minimal shape we need -type HasSchemaEntries = { - schema: { - entries: T; - }; -}; - -// Get the keys from a type, excluding any index signature -type LiteralKeys = keyof { - [K in keyof T as string extends K ? never : K]: T[K]; -}; - -// Create a union type that maintains the relationship between pod and its entries -type PodEntryPair

= { - [K in keyof P]: P[K] extends { schema: { entries: infer E } } - ? `${K & string}.${LiteralKeys & string}` - : never; -}[keyof P]; - -// Helper to create fixed-length array type -type FixedLengthArray< - T, - N extends number, - R extends T[] = [] -> = R["length"] extends N ? R : FixedLengthArray; - -// eslint-disable-next-line @typescript-eslint/no-explicit-any -type Length = T["length"]; - -type TupleSpec[]]> = { - entries: E; - isMemberOf?: { [K in keyof E]: PODValue }[]; - isNotMemberOf?: { [K in keyof E]: PODValue }[]; -}; - -export type PodSpecGroupSchema< - P, - T extends ReadonlyArray[]]>> -> = { - pods: P; - tuples: T; -}; - -// Debug types -type TestPodSpec1 = PodSpec<{ foo: { type: "string" } }>; -type TestPodSpec2 = PodSpec<{ bar: { type: "string" } }>; -type TestPods = { - p1: TestPodSpec1; - p2: TestPodSpec2; -}; - -/// Expand a type recursively -type ExpandRecursively = T extends object - ? T extends infer O - ? { [K in keyof O]: ExpandRecursively } - : never - : T; - -// The class inherits the same constraint -export class PodSpecGroup< - P extends Record, - T extends readonly TupleSpec[]]>[] -> { - readonly specs: PodSpecGroupSchema; - - constructor(schema: PodSpecGroupSchema) { - this.specs = schema; - } - - get(key: K): P[K] { - return this.specs.pods[key]; - } -} -// Debug concrete PodEntryPair -type DebugPodEntryPair = ExpandRecursively>; -// This will show: "p1.foo" | "p2.bar" - -// Debug concrete PodSpecGroupSchema -type DebugPodSpecGroupSchema = ExpandRecursively< - PodSpecGroupSchema]> ->; -type DebugPodSpecTuples = ExpandRecursively; -// This will show the full structure: -// { -// pods: TestPods; -// tuples?: { -// entries: PodEntryPair[]; -// isMemberOf?: PODValue[][]; -// isNotMemberOf?: PODValue[][]; -// }[]; -// } - -type DebugTupleSpec = TupleSpec< - TestPods, - readonly [PodEntryPair, PodEntryPair] ->; - -// Debug types to understand length inference -type TestEntries = ["p1.foo", "p2.bar"]; -type TestEntriesLength = Length; // should be 2 -type TestFixedArray = FixedLengthArray; - -// Debug what happens in PodSpecGroupSchema -type TestGroupSchema = PodSpecGroupSchema< - TestPods, - [TupleSpec] ->; -type TestGroupSchemaTuples = ExpandRecursively; diff --git a/packages/podspec/src/index.ts b/packages/podspec/src/index.ts index aab6be7..08722e5 100644 --- a/packages/podspec/src/index.ts +++ b/packages/podspec/src/index.ts @@ -1,4 +1,3 @@ -import { podToPODData } from "./data.js"; import type { PodspecProofRequestSchema, ProofConfigPODSchema, @@ -10,29 +9,14 @@ import type { EntriesSpec } from "./parse/entries.js"; import { entries } from "./parse/entries.js"; import type { PODData, PodSpec } from "./parse/pod.js"; import { pod, merge } 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"; export { entries, pod, proofRequest, - podToPODData, merge, - type EntriesOutputType, - type EntriesSchema, type EntriesSpec, - type InferJavaScriptEntriesType, - type InferEntriesType, - type InferPodType, type ProofConfigPODSchema, - type PODSchema, type PodSpec, type PODData, type PodspecProofRequestSchema as PodspecProofRequest, diff --git a/packages/podspec/src/processors/validate.ts b/packages/podspec/src/processors/validate.ts index 134a213..c7f28be 100644 --- a/packages/podspec/src/processors/validate.ts +++ b/packages/podspec/src/processors/validate.ts @@ -19,6 +19,7 @@ import { } from "./validate/issues.js"; import { checkIsMemberOf } from "./validate/checks/isMemberOf.js"; import { checkIsNotMemberOf } from "./validate/checks/isNotMemberOf.js"; +import { assertPODSpec } from "../generated/podspec.js"; /** @TOOO @@ -78,6 +79,8 @@ function validatePOD( spec: PODSpec, options: ValidateOptions = DEFAULT_VALIDATE_OPTIONS ): ValidateResult>> { + assertPODSpec(spec); + const podEntries = pod.content.asEntries(); const issues = []; 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 c536b2c..0000000 --- a/packages/podspec/src/schemas/entry.ts +++ /dev/null @@ -1,118 +0,0 @@ -import type { - PODName, - PODBytesValue, - PODCryptographicValue, - PODDateValue, - PODEdDSAPublicKeyValue, - PODIntValue, - PODBooleanValue, - PODNullValue, - PODStringValue -} from "@pcd/pod"; - -/** - * Schema for a PODStringValue. - */ -export interface StringSchema { - type: "string"; - isMemberOf?: PODStringValue[]; - isNotMemberOf?: PODStringValue[]; - equalsEntry?: PODName; -} - -/** - * Schema for a PODBytesValue. - */ -export interface BytesSchema { - type: "bytes"; - isMemberOf?: PODBytesValue[]; - isNotMemberOf?: PODBytesValue[]; - equalsEntry?: PODName; -} - -/** - * Schema for a PODIntValue. - */ -export interface IntSchema { - type: "int"; - isMemberOf?: PODIntValue[]; - isNotMemberOf?: PODIntValue[]; - equalsEntry?: PODName; - inRange?: { min: bigint; max: bigint }; -} - -/** - * Schema for a PODCryptographicValue. - */ -export interface CryptographicSchema { - type: "cryptographic"; - isMemberOf?: PODCryptographicValue[]; - isNotMemberOf?: PODCryptographicValue[]; - equalsEntry?: PODName; - inRange?: { min: bigint; max: bigint }; - isOwnerID?: boolean; -} - -/** - * Schema for a PODEdDSAPublicKeyValue. - */ -export interface EdDSAPublicKeySchema { - type: "eddsa_pubkey"; - isMemberOf?: PODEdDSAPublicKeyValue[]; - isNotMemberOf?: PODEdDSAPublicKeyValue[]; - equalsEntry?: PODName; -} - -/** - * Schema for a PODBooleanValue. - */ -export interface BooleanSchema { - type: "boolean"; - isMemberOf?: PODBooleanValue[]; - isNotMemberOf?: PODBooleanValue[]; -} - -/** - * Schema for a PODDateValue. - */ -export interface DateSchema { - type: "date"; - isMemberOf?: PODDateValue[]; - isNotMemberOf?: PODDateValue[]; - equalsEntry?: PODName; -} - -/** - * Schema for a PODNullValue. - */ -export interface NullSchema { - type: "null"; - isMemberOf?: PODNullValue[]; - isNotMemberOf?: PODNullValue[]; -} - -/** - * 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 3bf9ed7..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 PODStringValue. - * @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/types/entries.ts b/packages/podspec/src/types/entries.ts deleted file mode 100644 index 06391b5..0000000 --- a/packages/podspec/src/types/entries.ts +++ /dev/null @@ -1,98 +0,0 @@ -import type { - PODBytesValue, - PODCryptographicValue, - PODDateValue, - PODEdDSAPublicKeyValue, - PODIntValue, - PODBooleanValue, - PODNullValue, - PODStringValue, - PODName -} from "@pcd/pod"; - -export interface StringEntrySpec { - type: "string"; - isMemberOf?: PODStringValue[]; - isNotMemberOf?: PODStringValue[]; -} - -export interface BytesEntrySpec { - type: "bytes"; - isMemberOf?: PODBytesValue[]; - isNotMemberOf?: PODBytesValue[]; -} - -export interface IntEntrySpec { - type: "int"; - isMemberOf?: PODIntValue[]; - isNotMemberOf?: PODIntValue[]; - inRange?: { min: bigint; max: bigint }; -} - -export interface CryptographicEntrySpec { - type: "cryptographic"; - isMemberOf?: PODCryptographicValue[]; - isNotMemberOf?: PODCryptographicValue[]; - inRange?: { min: bigint; max: bigint }; - isOwnerID?: "SemaphoreV3"; // @todo constant from GPC? -} - -export interface EdDSAPublicKeyEntrySpec { - type: "eddsa_pubkey"; - isMemberOf?: PODEdDSAPublicKeyValue[]; - isNotMemberOf?: PODEdDSAPublicKeyValue[]; - isOwnerID?: "SemaphoreV4"; // @todo constant from GPC? -} - -export interface BooleanEntrySpec { - type: "boolean"; - isMemberOf?: PODBooleanValue[]; - isNotMemberOf?: PODBooleanValue[]; -} - -export interface DateEntrySpec { - type: "date"; - isMemberOf?: PODDateValue[]; - isNotMemberOf?: PODDateValue[]; -} - -export interface NullEntrySpec { - type: "null"; - isMemberOf?: PODNullValue[]; - isNotMemberOf?: PODNullValue[]; -} - -/** - * Union of non-optional entries. - */ -export type DefinedEntrySpec = - | StringEntrySpec - | CryptographicEntrySpec - | IntEntrySpec - | EdDSAPublicKeyEntrySpec - | BooleanEntrySpec - | BytesEntrySpec - | DateEntrySpec - | NullEntrySpec; - -/** - * Optional entry wrapper. - */ -export interface OptionalEntrySpec { - type: "optional"; - innerType: DefinedEntrySpec; -} - -/** - * Union of all entry types. - */ -export type EntrySpec = DefinedEntrySpec | OptionalEntrySpec; - -/** - * Spec for a PODEntries object - simply a keyed collection of EntrySpecs. - */ -export type EntryListSpec = Readonly>; - -export type EntriesSpec = { - entries: E; -}; diff --git a/packages/podspec/src/types/group.ts b/packages/podspec/src/types/group.ts deleted file mode 100644 index a60dd4b..0000000 --- a/packages/podspec/src/types/group.ts +++ /dev/null @@ -1,116 +0,0 @@ -import type { PODName, PODValue } from "@pcd/pod"; -import type { EntryListSpec } from "./entries.js"; -import type { PODValueType } from "./utils.js"; - -export type HasEntries = { - entries: T; -}; - -export type PodEntryStrings

> = { - [K in keyof P]: { - [E in keyof P[K]["entries"]]: `${K & string}.${E & string}`; - }[keyof P[K]["entries"]]; -}[keyof P]; - -export type TupleSpec< - P extends Record, - E extends readonly PodEntryStrings

[] = readonly PodEntryStrings

[] -> = { - entries: E; - isMemberOf?: [{ [K in keyof E]: PODValue }[number]][]; - isNotMemberOf?: [{ [K in keyof E]: PODValue }[number]][]; -}; - -export type PodGroupSpec

> = { - pods: P & Record; -}; - -export type PodGroupSpecPods< - P extends PodGroupSpec> = PodGroupSpec< - Record - > -> = P["pods"]; - -// Get the keys from a type, excluding any index signature -type LiteralKeys = keyof { - [K in keyof T as string extends K ? never : K]: T[K]; -}; - -// Create a union type that maintains the relationship between pod and its entries -type PodEntryPair

= { - [K in keyof P]: P[K] extends { schema: { entries: infer E } } - ? `${K & string}.${LiteralKeys & string}` - : never; -}[keyof P]; - -// eslint-disable-next-line @typescript-eslint/no-unused-vars -type PodEntryNames

> = { - [K in keyof P]: (keyof P[K]["entries"] & string)[]; -}; - -export type PodEntryStringsOfType< - P extends Record, - T extends PODValueType -> = { - [K in keyof P]: { - [E in keyof P[K]["entries"]]: P[K]["entries"][E]["type"] extends T - ? `${K & string}.${E & string}` - : never; - }[keyof P[K]["entries"]]; -}[keyof P]; - -// Add this helper type -type AssertEqual = [T] extends [U] - ? [U] extends [T] - ? true - : false - : false; - -type _TestPodEntryStrings = PodEntryStrings<{ - pod1: { entries: { foo: { type: "string" } } }; - pod2: { entries: { foo: { type: "int" }; bar: { type: "string" } } }; -}>; - -// This creates a type error when the condition is false -type Assert = T; -type IsEqual = (() => T extends A ? 1 : 2) extends () => T extends B - ? 1 - : 2 - ? true - : false; - -// This should fail compilation if types don't match -type _Test = Assert< - IsEqual<_TestPodEntryStrings, "pod1.foo" | "pod2.foo" | "pod2.bar"> ->; - -// If you want to see an error when types don't match: -type _TestShouldFail = AssertEqual<_TestPodEntryStrings, "wrong_type">; - -type _TestPodEntryStringsOfType = PodEntryStringsOfType< - { - pod1: { entries: { foo: { type: "int" } } }; - pod2: { entries: { foo: { type: "int" }; bar: { type: "string" } } }; - }, - "int" ->; // "pod1.foo" | "pod2.foo" - -// Split a PodEntryPair into its pod and entry components -type SplitPodEntry = T extends `${infer Pod}.${infer Entry}` - ? { pod: Pod; entry: Entry } - : never; - -// Get the type of an entry given a pod and entry name -// eslint-disable-next-line @typescript-eslint/no-unused-vars -type EntryType< - P extends Record, - PE extends PodEntryPair

-> = SplitPodEntry extends { pod: infer Pod } - ? Pod extends keyof P - ? SplitPodEntry extends { entry: infer Entry } - ? Entry extends keyof P[Pod]["entries"] - ? P[Pod]["entries"][Entry]["type"] - : never - : never - : never - : never; diff --git a/packages/podspec/src/types/pod.ts b/packages/podspec/src/types/pod.ts deleted file mode 100644 index 26c11fc..0000000 --- a/packages/podspec/src/types/pod.ts +++ /dev/null @@ -1,62 +0,0 @@ -import type { PODName, PODValue } from "@pcd/pod"; -import type { EntryListSpec } from "./entries.js"; - -type PODValueTypeForEntry< - E extends EntryListSpec, - K extends keyof E -> = E[K]["type"]; - -export type EntryNamesForPOD = readonly [ - keyof (E & { - $signerPublicKey: never; - }) & - PODName, - ...(keyof (E & { - $signerPublicKey: never; - }) & - PODName)[] -]; - -/** - * Schema for validating a POD. - */ -export type PODTupleSpec< - E extends EntryListSpec, - Names extends readonly (keyof E)[] = (keyof E)[] -> = { - entries: Names; - isMemberOf?: { - [I in keyof Names]: Extract< - PODValue, - { type: PODValueTypeForEntry } - >; - }[]; - isNotMemberOf?: { - [I in keyof Names]: Extract< - PODValue, - { type: PODValueTypeForEntry } - >; - }[]; -}; - -export type PODTuplesSpec = Record< - string, - PODTupleSpec> ->; - -export type PODSpec = { - entries: E; - tuples: PODTuplesSpec; - signerPublicKey?: { - isRevealed?: boolean; - isMemberOf?: string[]; - isNotMemberOf?: string[]; - }; - signature?: { - isMemberOf?: string[]; - isNotMemberOf?: string[]; - }; - meta?: { - labelEntry: keyof E & PODName; - }; -}; diff --git a/packages/podspec/src/types/utils.ts b/packages/podspec/src/types/utils.ts deleted file mode 100644 index 80a7e8d..0000000 --- a/packages/podspec/src/types/utils.ts +++ /dev/null @@ -1,3 +0,0 @@ -import type { PODValue } from "@pcd/pod"; - -export type PODValueType = PODValue["type"]; From c1e8332ad3b1c87dbed9506e2cb6b42ef8eaaa37 Mon Sep 17 00:00:00 2001 From: Rob Knight Date: Fri, 31 Jan 2025 14:47:16 +0100 Subject: [PATCH 07/20] Assorted WIP --- packages/podspec/package.json | 1 + packages/podspec/src/audit/podSpec.ts | 24 + packages/podspec/src/builders/group.ts | 17 +- packages/podspec/src/builders/pod.ts | 121 ++++- packages/podspec/src/builders/shared.ts | 34 ++ .../podspec/src/builders/types/statements.ts | 60 ++- packages/podspec/src/gpc/proof_request.ts | 474 +++++++++--------- packages/podspec/src/index.ts | 30 +- packages/podspec/src/parse/entries.ts | 330 ------------ packages/podspec/src/parse/entry.ts | 122 ----- packages/podspec/src/parse/parse_utils.ts | 361 ------------- packages/podspec/src/parse/pod.ts | 434 ---------------- packages/podspec/src/processors/validate.ts | 70 ++- .../src/processors/validate/checks/inRange.ts | 24 +- .../processors/validate/checks/isMemberOf.ts | 25 +- .../validate/checks/isNotMemberOf.ts | 25 +- .../podspec/src/processors/validate/utils.ts | 62 +++ packages/podspec/src/shared/jsonSafe.ts | 12 + pnpm-lock.yaml | 3 + 19 files changed, 641 insertions(+), 1588 deletions(-) create mode 100644 packages/podspec/src/audit/podSpec.ts delete mode 100644 packages/podspec/src/parse/entries.ts delete mode 100644 packages/podspec/src/parse/entry.ts delete mode 100644 packages/podspec/src/parse/parse_utils.ts delete mode 100644 packages/podspec/src/parse/pod.ts create mode 100644 packages/podspec/src/processors/validate/utils.ts create mode 100644 packages/podspec/src/shared/jsonSafe.ts diff --git a/packages/podspec/package.json b/packages/podspec/package.json index 3ea0ca9..092bcf8 100644 --- a/packages/podspec/package.json +++ b/packages/podspec/package.json @@ -38,6 +38,7 @@ "dependencies": { "@pcd/gpc": "^0.4.0", "@pcd/pod": "^0.5.0", + "base64-js": "^1.5.1", "canonicalize": "^2.0.0", "typia": "^7.6.0" }, 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 index 819a245..50ba970 100644 --- a/packages/podspec/src/builders/group.ts +++ b/packages/podspec/src/builders/group.ts @@ -9,6 +9,7 @@ import type { PODValueType } from "./types/entries.js"; import type { StatementMap, IsMemberOf } from "./types/statements.js"; +import { convertValuesToStringTuples } from "./shared.js"; type PODGroupPODs = Record>; @@ -22,7 +23,7 @@ type PODGroupPODs = Record>; // isMemberOf: N[number]; // }; -type PODGroupSpec

= { +export type PODGroupSpec

= { pods: P; statements: S; }; @@ -54,7 +55,10 @@ type AddPOD< [K in keyof PODs | N]: K extends N ? Spec : PODs[K & keyof PODs]; }>; -class PODGroupSpecBuilder

{ +export class PODGroupSpecBuilder< + P extends PODGroupPODs, + S extends StatementMap +> { readonly #spec: PODGroupSpec; private constructor(spec: PODGroupSpec) { @@ -119,14 +123,7 @@ class PODGroupSpecBuilder

{ const statement: IsMemberOf, N> = { entries: names, type: "isMemberOf", - // Wrap single values in arrays to match the expected tuple format - isMemberOf: (names.length === 1 - ? ( - values as PODValueTypeFromTypeName< - EntryType> - >[] - ).map((v) => [v]) - : values) as PODValueTupleForNamedEntries, N>[] + isMemberOf: convertValuesToStringTuples(names, values) }; const baseName = `${names.join("_")}_isMemberOf`; diff --git a/packages/podspec/src/builders/pod.ts b/packages/podspec/src/builders/pod.ts index 9d0dd7b..fa36e4d 100644 --- a/packages/podspec/src/builders/pod.ts +++ b/packages/podspec/src/builders/pod.ts @@ -7,7 +7,11 @@ import { POD_INT_MAX, POD_INT_MIN } from "@pcd/pod"; -import { deepFreeze, validateRange } from "./shared.js"; +import { + convertValuesToStringTuples, + deepFreeze, + validateRange +} from "./shared.js"; import type { IsMemberOf, IsNotMemberOf, @@ -16,8 +20,8 @@ import type { EqualsEntry, NotEqualsEntry, SupportsRangeChecks, - StatementMap, - StatementName + StatementName, + StatementMap } from "./types/statements.js"; import type { EntriesOfType, @@ -29,6 +33,7 @@ import type { VirtualEntries } from "./types/entries.js"; import canonicalize from "canonicalize/lib/canonicalize.js"; +import type { IsJsonSafe } from "../shared/jsonSafe.js"; /** @todo @@ -66,6 +71,9 @@ export type PODSpec = { statements: S; }; +// This is a compile-time check that the PODSpec is JSON-safe +true satisfies IsJsonSafe>; + type DoesNotSupportRangeChecks = Exclude; function supportsRangeChecks(type: PODValueType): type is SupportsRangeChecks { @@ -303,17 +311,23 @@ export class PODSpecBuilder< throw new Error("Duplicate entry names are not allowed"); } + /** + * 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", - // Wrap single values in arrays to match the expected tuple format - isMemberOf: (names.length === 1 - ? ( - values as PODValueTypeFromTypeName< - (E & VirtualEntries)[N[0] & keyof (E & VirtualEntries)] - >[] - ).map((v) => [v]) - : values) as PODValueTupleForNamedEntries[] + isMemberOf: convertValuesToStringTuples(names, values) }; const baseName = `${names.join("_")}_isMemberOf`; @@ -372,17 +386,23 @@ export class PODSpecBuilder< throw new Error("Duplicate entry names are not allowed"); } - const statement: IsNotMemberOf = { + /** + * 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", - // Wrap single values in arrays to match the expected tuple format - isNotMemberOf: (names.length === 1 - ? ( - values as PODValueTypeFromTypeName< - (E & VirtualEntries)[N[0] & keyof (E & VirtualEntries)] - >[] - ).map((v) => [v]) - : values) as PODValueTupleForNamedEntries[] + isNotMemberOf: convertValuesToStringTuples(names, values) }; const baseName = `${names.join("_")}_isNotMemberOf`; @@ -460,7 +480,16 @@ export class PODSpecBuilder< const statement: InRange = { entry: name, type: "inRange", - inRange: range + 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 = `${name}_inRange`; @@ -534,7 +563,16 @@ export class PODSpecBuilder< const statement: NotInRange = { entry: name, type: "notInRange", - notInRange: range + 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 = `${name}_notInRange`; @@ -705,9 +743,10 @@ if (import.meta.vitest) { }); const g = e.entry("new", "string").equalsEntry("a", "new"); - const _GEntries = g.spec().entries; + const _GSpec = g.spec(); + const _GEntries = _GSpec.entries; type EntriesType = typeof _GEntries; - g.spec().statements.a_new_equalsEntry satisfies EqualsEntry< + _GSpec.statements.a_new_equalsEntry satisfies EqualsEntry< EntriesType, "a", "new" @@ -721,6 +760,40 @@ if (import.meta.vitest) { } }); + expect(g.spec()).toEqual({ + entries: { + a: "string", + b: "int", + new: "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: { + entry: "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: { + entry: "a", + type: "equalsEntry", + equalsEntry: "new" + } + } + } satisfies typeof _GSpec); + const h = g.pickStatements(["a_isMemberOf"]); expect(h.spec().statements).toEqual({ a_isMemberOf: { diff --git a/packages/podspec/src/builders/shared.ts b/packages/podspec/src/builders/shared.ts index fa52e83..33f6655 100644 --- a/packages/podspec/src/builders/shared.ts +++ b/packages/podspec/src/builders/shared.ts @@ -1,5 +1,6 @@ import { checkPODValue, type PODValue } from "@pcd/pod"; import type { PODValueType } from "./types/entries.js"; +import { fromByteArray } from "base64-js"; /** * Validates a range check. @@ -67,3 +68,36 @@ export function deepFreeze(obj: T): T { } 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"][][] +): { [K in keyof N]: string }[] { + return names.length === 1 + ? (values as PODValue["value"][]).map( + (v) => [valueToString(v)] as { [K in keyof N]: string } + ) + : (values as PODValue["value"][][]).map( + (tuple) => + tuple.map((v) => valueToString(v)) as { + [K in keyof N]: string; + } + ); +} diff --git a/packages/podspec/src/builders/types/statements.ts b/packages/podspec/src/builders/types/statements.ts index 4c7f674..ca70892 100644 --- a/packages/podspec/src/builders/types/statements.ts +++ b/packages/podspec/src/builders/types/statements.ts @@ -10,52 +10,77 @@ import type { * 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: PODValueTupleForNamedEntries[]; + isMemberOf: Concrete>; }; -export type IsNotMemberOf> = { +export type IsNotMemberOf< + E extends EntryTypes, + N extends EntryKeys & string[] +> = { entries: N; type: "isNotMemberOf"; - isNotMemberOf: PODValueTupleForNamedEntries[]; + isNotMemberOf: Concrete>; }; // Which entry types support range checks? export type SupportsRangeChecks = "int" | "boolean" | "date"; +export type RangeInput< + E extends EntryTypes, + N extends keyof EntriesOfType & + 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 EntriesOfType + N extends keyof EntriesOfType & + string > = { entry: N; type: "inRange"; - inRange: { - min: E[N] extends "date" ? Date : bigint; - max: E[N] extends "date" ? Date : bigint; - }; + inRange: RangePersistent; }; export type NotInRange< E extends EntryTypes, - N extends keyof EntriesOfType + N extends keyof EntriesOfType & + string > = { entry: N; type: "notInRange"; - notInRange: { - min: E[N] extends "date" ? Date : bigint; - max: E[N] extends "date" ? Date : bigint; - }; + notInRange: RangePersistent; }; export type EqualsEntry< E extends EntryTypes, - N1 extends keyof (E & VirtualEntries), - N2 extends keyof (E & VirtualEntries) + N1 extends keyof (E & VirtualEntries) & string, + N2 extends keyof (E & VirtualEntries) & string > = E[N2] extends E[N1] ? { entry: N1; @@ -66,8 +91,8 @@ export type EqualsEntry< export type NotEqualsEntry< E extends EntryTypes, - N1 extends keyof (E & VirtualEntries), - N2 extends keyof (E & VirtualEntries) + N1 extends keyof (E & VirtualEntries) & string, + N2 extends keyof (E & VirtualEntries) & string > = E[N2] extends E[N1] ? { entry: N1; @@ -90,7 +115,6 @@ export type Statements = // eslint-disable-next-line @typescript-eslint/no-explicit-any | NotEqualsEntry; -// Base map of named statements export type StatementMap = Record; /**************************************************************************** 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 08722e5..423a27f 100644 --- a/packages/podspec/src/index.ts +++ b/packages/podspec/src/index.ts @@ -1,25 +1,11 @@ -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, merge } from "./parse/pod.js"; +import { type PODSpec, PODSpecBuilder } from "./builders/pod.js"; +import { type PODGroupSpec, PODGroupSpecBuilder } from "./builders/group.js"; +import { validatePOD } from "./processors/validate.js"; export { - entries, - pod, - proofRequest, - merge, - type EntriesSpec, - type ProofConfigPODSchema, - type PodSpec, - type PODData, - type PodspecProofRequestSchema as PodspecProofRequest, - type ProofRequest, - type ProofRequestSpec + type PODSpec, + PODSpecBuilder, + type PODGroupSpec, + PODGroupSpecBuilder, + validatePOD }; 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 acb7ba6..0000000 --- a/packages/podspec/src/parse/pod.ts +++ /dev/null @@ -1,434 +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"; - -const invalid = Symbol("invalid"); -type Invalid = { [invalid]: T }; - -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); - } -} - -/** - * Merges two PodSpecs, combining their schemas. - * The resulting PodSpec will only accept PODs that satisfy both specs. - * - * @param spec1 The first PodSpec to merge - * @param spec2 The second PodSpec to merge - * @returns A new PodSpec that combines both schemas - * @throws {Error} If the specs have overlapping entries or conflicting constraints - */ -export function merge< - const E extends EntriesSchema, - const F extends EntriesSchema ->( - spec1: NoOverlappingEntries extends never - ? PodSpec & Invalid<"Cannot merge PodSpecs with overlapping entries"> - : PodSpec, - spec2: PodSpec -): PodSpec { - // Runtime checks for constraints that complement the type checks - const entriesOverlap = Object.keys(spec1.schema.entries).some( - (key) => key in spec2.schema.entries - ); - if (entriesOverlap) { - throw new Error("Cannot merge PodSpecs with overlapping entries"); - } - - if (spec1.schema.signature && spec2.schema.signature) { - throw new Error( - "Cannot merge PodSpecs that both have signature constraints" - ); - } - - if (spec1.schema.signerPublicKey && spec2.schema.signerPublicKey) { - throw new Error( - "Cannot merge PodSpecs that both have signerPublicKey constraints" - ); - } - - const mergedSchema: PODSchema = { - entries: { - ...spec1.schema.entries, - ...spec2.schema.entries - }, - signature: spec1.schema.signature ?? spec2.schema.signature, - signerPublicKey: - spec1.schema.signerPublicKey ?? spec2.schema.signerPublicKey, - tuples: [...(spec1.schema.tuples ?? []), ...(spec2.schema.tuples ?? [])] - }; - - return PodSpec.create(mergedSchema); -} - -/** - * 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> - ); -} - -/** Check if two types share any keys */ -type HasOverlap = keyof T & keyof U extends never ? false : true; - -/** Ensure two schemas don't have overlapping entries */ -type NoOverlappingEntries< - E1 extends EntriesSchema, - E2 extends EntriesSchema -> = HasOverlap extends true ? never : E1 & E2; diff --git a/packages/podspec/src/processors/validate.ts b/packages/podspec/src/processors/validate.ts index c7f28be..2e60971 100644 --- a/packages/podspec/src/processors/validate.ts +++ b/packages/podspec/src/processors/validate.ts @@ -67,19 +67,75 @@ const DEFAULT_VALIDATE_OPTIONS: ValidateOptions = { strict: false }; +interface PODValidator { + validate(pod: POD): ValidateResult>>; + check(pod: POD): boolean; + assert(pod: POD): asserts pod is StrongPOD>; + strictValidate( + pod: POD + ): ValidateResult>>; + strictCheck(pod: POD): boolean; + strictAssert(pod: POD): asserts pod is StrongPOD>; +} + +function validate( + spec: PODSpec +): PODValidator { + // @TODO maybe typia's clone is better + spec = structuredClone(spec); + assertPODSpec(spec); + + return { + validate: (pod) => validatePOD(pod, spec, {}), + check: (pod) => 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) => validatePOD(pod, spec, { strict: true }), + strictCheck: (pod) => + 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"); + } + }; +} + +const SpecValidatorState = new WeakMap< + PODSpec, + boolean +>(); + /** * 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( +export function validatePOD( pod: POD, spec: PODSpec, options: ValidateOptions = DEFAULT_VALIDATE_OPTIONS ): ValidateResult>> { - assertPODSpec(spec); + const validSpec = SpecValidatorState.get(spec); + if (validSpec === undefined) { + // If we haven't seen this spec before, we need to validate it + try { + assertPODSpec(spec); + + // If we successfully validated the spec, we can cache the result + SpecValidatorState.set(spec, true); + } catch (e) { + SpecValidatorState.set(spec, false); + throw e; + } + } const podEntries = pod.content.asEntries(); @@ -162,7 +218,9 @@ function validatePOD( } } - return SUCCESS(pod as StrongPOD>); + return issues.length > 0 + ? FAILURE(issues) + : SUCCESS(pod as StrongPOD>); } if (import.meta.vitest) { @@ -180,7 +238,7 @@ if (import.meta.vitest) { .isMemberOf(["foo"], ["foo", "bar"]); // This should pass because the entry "foo" is in the list ["foo", "bar"] - expect(validatePOD(myPOD, myPodSpecBuilder.spec())).toBe(true); + expect(validatePOD(myPOD, myPodSpecBuilder.spec()).isValid).toBe(true); const result = validatePOD(myPOD, myPodSpecBuilder.spec()); if (result.isValid) { @@ -200,14 +258,14 @@ if (import.meta.vitest) { // This should fail because the entry "foo" is not in the list ["baz", "quux"] const secondBuilder = myPodSpecBuilder.isMemberOf(["foo"], ["baz", "quux"]); - expect(validatePOD(myPOD, secondBuilder.spec())).toBe(false); + expect(validatePOD(myPOD, secondBuilder.spec()).isValid).toBe(false); // If we omit the new statement, it should pass expect( validatePOD( myPOD, secondBuilder.omitStatements(["foo_isMemberOf_1"]).spec() - ) + ).isValid ).toBe(true); }); } diff --git a/packages/podspec/src/processors/validate/checks/inRange.ts b/packages/podspec/src/processors/validate/checks/inRange.ts index 71edf71..71dce7a 100644 --- a/packages/podspec/src/processors/validate/checks/inRange.ts +++ b/packages/podspec/src/processors/validate/checks/inRange.ts @@ -13,26 +13,26 @@ export function checkInRange( statementName: string, path: string[], entries: PODEntries, - specEntries: EntryTypes, - exitOnError: boolean + _specEntries: EntryTypes, + _exitOnError: boolean ): ValidationBaseIssue[] { const entryName = statement.entry; const entry = entries[entryName]!; - // @TODO need an issue type for statement referring to a non-existent entry + // TODO need an issue type for statement referring to a non-existent entry // or entry of the wrong type + const isDate = entry.type === "date"; + const min = isDate + ? new Date(statement.inRange.min) + : BigInt(statement.inRange.min); + const max = isDate + ? new Date(statement.inRange.max) + : BigInt(statement.inRange.max); + if (isPODArithmeticValue(entry)) { const value = entry.value; - // @TODO date comparison? - // maybe the spec should convert dates to bigints, and we also convert - // dates to bigints here, so we have a consistent way to compare dates - // correct framing here is "how do we serialize statement parameters", - // followed by "how do we deserialize statement parameters into the - // format required by the processor". - // so the specifier provides, say, Date objects. the builder may serialize - // those as strings or bigints. the processor needs bigints. - if (value < statement.inRange.min || value > statement.inRange.max) { + if (value < min || value > max) { return [ { code: IssueCode.statement_negative_result, diff --git a/packages/podspec/src/processors/validate/checks/isMemberOf.ts b/packages/podspec/src/processors/validate/checks/isMemberOf.ts index ee4e879..6c3f26d 100644 --- a/packages/podspec/src/processors/validate/checks/isMemberOf.ts +++ b/packages/podspec/src/processors/validate/checks/isMemberOf.ts @@ -7,6 +7,7 @@ import type { import { IssueCode } from "../issues.js"; import type { IsMemberOf } from "../../../builders/types/statements.js"; import type { EntryTypes } from "../../../builders/types/entries.js"; +import { tupleToPODValueTypeValues, valueIsEqual } from "../utils.js"; function validateIsMemberOfStatement( // eslint-disable-next-line @typescript-eslint/no-explicit-any @@ -38,6 +39,7 @@ export function checkIsMemberOf( specEntries: EntryTypes, exitOnError: boolean ): ValidationBaseIssue[] { + // TODO Move this to a pre-processing step const issues: ValidationBaseIssue[] = validateIsMemberOfStatement( statement, statementName, @@ -51,14 +53,25 @@ export function checkIsMemberOf( const tuple = statement.entries.map((entry) => podEntries[entry]?.value); - for (const listMember of statement.isMemberOf) { - if ( - listMember.some((value, index) => { - return value === tuple[index]; - }) - ) { + // TODO Move this to a pre-processing step + const tuplesToMatch = tupleToPODValueTypeValues( + statement.isMemberOf, + statement.entries + ); + + 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, diff --git a/packages/podspec/src/processors/validate/checks/isNotMemberOf.ts b/packages/podspec/src/processors/validate/checks/isNotMemberOf.ts index 60b0422..fea5c42 100644 --- a/packages/podspec/src/processors/validate/checks/isNotMemberOf.ts +++ b/packages/podspec/src/processors/validate/checks/isNotMemberOf.ts @@ -7,6 +7,7 @@ import { } from "../issues.js"; import type { IsNotMemberOf } from "../../../builders/types/statements.js"; import type { EntryTypes } from "../../../builders/types/entries.js"; +import { tupleToPODValueTypeValues, valueIsEqual } from "../utils.js"; function validateIsNotMemberOfStatement( // eslint-disable-next-line @typescript-eslint/no-explicit-any @@ -38,6 +39,7 @@ export function checkIsNotMemberOf( specEntries: EntryTypes, exitOnError: boolean ): ValidationBaseIssue[] { + // TODO Move this to a pre-processing step const issues: ValidationBaseIssue[] = validateIsNotMemberOfStatement( statement, statementName, @@ -51,15 +53,26 @@ export function checkIsNotMemberOf( } const tuple = statement.entries.map((entry) => entries[entry]?.value); + // TODO Move this to a pre-processing step + const tuplesToMatch = tupleToPODValueTypeValues( + statement.isNotMemberOf, + statement.entries + ); - for (const listMember of statement.isNotMemberOf) { - if ( - listMember.every((value, index) => { - return value !== tuple[index]; - }) - ) { + 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, 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/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/pnpm-lock.yaml b/pnpm-lock.yaml index 3300aa9..e539b9f 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -429,6 +429,9 @@ 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 From 07e6df17c9e36949cfbea470047d2c3244b6b5cc Mon Sep 17 00:00:00 2001 From: Rob Knight Date: Sat, 1 Feb 2025 09:16:18 +0100 Subject: [PATCH 08/20] checkpoint while fixing podgroupspec types --- packages/podspec/src/builders/group.ts | 749 +++++++++++++++++- packages/podspec/src/builders/pod.ts | 341 ++++++-- packages/podspec/src/builders/shared.ts | 19 + .../podspec/src/builders/types/entries.ts | 6 +- .../podspec/src/builders/types/statements.ts | 74 +- .../processors/validate/checks/notInRange.ts | 49 ++ 6 files changed, 1146 insertions(+), 92 deletions(-) create mode 100644 packages/podspec/src/processors/validate/checks/notInRange.ts diff --git a/packages/podspec/src/builders/group.ts b/packages/podspec/src/builders/group.ts index 50ba970..434aadc 100644 --- a/packages/podspec/src/builders/group.ts +++ b/packages/podspec/src/builders/group.ts @@ -1,4 +1,11 @@ -import { checkPODName, type PODName } from "@pcd/pod"; +import { + checkPODName, + POD_DATE_MAX, + POD_DATE_MIN, + POD_INT_MAX, + POD_INT_MIN, + type PODName +} from "@pcd/pod"; import { type PODSpec, PODSpecBuilder } from "./pod.js"; import type { EntryTypes, @@ -6,10 +13,29 @@ import type { EntryKeys, PODValueTypeFromTypeName, PODValueTupleForNamedEntries, - PODValueType + PODValueType, + EntriesOfType } from "./types/entries.js"; -import type { StatementMap, IsMemberOf } from "./types/statements.js"; -import { convertValuesToStringTuples } from "./shared.js"; +import type { + StatementMap, + IsMemberOf, + IsNotMemberOf, + InRange, + NotInRange, + EqualsEntry, + NotEqualsEntry, + GreaterThan, + GreaterThanEq, + LessThan, + LessThanEq, + SupportsRangeChecks +} from "./types/statements.js"; +import { + convertValuesToStringTuples, + supportsRangeChecks, + validateRange +} from "./shared.js"; +import { assertType } from "vitest"; type PODGroupPODs = Record>; @@ -28,16 +54,35 @@ export type PODGroupSpec

= { statements: S; }; +// type AllPODEntries

= { +// [K in keyof P]: { +// [E in keyof (P[K]["entries"] & VirtualEntries) as `${K & string}.${E & +// string}`]: (P[K]["entries"] & VirtualEntries)[E]; +// }; +// }[keyof P]; + +// First get the entries with pod name prefixes +type PodEntries

= { + [K in keyof P]: { + [E in keyof (P[K]["entries"] & VirtualEntries) as `${K & string}.${E & + string}`]: (P[K]["entries"] & VirtualEntries)[E]; + }; +}[keyof P]; + +// Then ensure we only get POD value types type AllPODEntries

= { - [K in keyof P as `${K & string}.${keyof (P[K]["entries"] & VirtualEntries) & - string}`]: P[K]["entries"][keyof P[K]["entries"] & string]; + [K in keyof PodEntries

]: PodEntries

[K] extends PODValueType + ? PodEntries

[K] + : never; }; +type MustBePODValueType = T extends PODValueType ? T : never; + // Add this helper type to preserve literal types type EntryType< P extends PODGroupPODs, K extends keyof AllPODEntries

-> = AllPODEntries

[K] extends PODValueType ? AllPODEntries

[K] : never; +> = MustBePODValueType[K]>; // type AddEntry< // E extends EntryListSpec, @@ -55,13 +100,57 @@ type AddPOD< [K in keyof PODs | N]: K extends N ? Spec : PODs[K & keyof PODs]; }>; +type PODGroupWithFirstPOD< + First extends PODSpec, + Rest extends PODGroupPODs +> = { + firstPOD: First; +} & Rest; + +class BasePODGroupSpecBuilder

{ + protected readonly spec: PODGroupSpec; + + protected constructor(spec: PODGroupSpec) { + this.spec = spec; + } + + // eslint-disable-next-line @typescript-eslint/no-empty-object-type + public static create(): BasePODGroupSpecBuilder<{}, {}> { + return new BasePODGroupSpecBuilder({ + pods: {}, + statements: {} + }); + } + + public pod< + N extends PODName, + Spec extends PODSpec, + NewPods extends AddPOD + >(name: N, spec: Spec): PODGroupSpecBuilder { + if (name in this.spec.pods) { + throw new Error(`POD "${name}" already exists`); + } + + checkPODName(name); + + return new PODGroupSpecBuilder({ + ...this.spec, + pods: { ...this.spec.pods, [name]: spec } as unknown as NewPods + }); + } +} + export class PODGroupSpecBuilder< - P extends PODGroupPODs, + P extends PODGroupWithFirstPOD< + PODSpec, + PODGroupPODs + >, S extends StatementMap -> { +> extends BasePODGroupSpecBuilder { readonly #spec: PODGroupSpec; - private constructor(spec: PODGroupSpec) { + constructor(spec: PODGroupSpec) { + super(spec); this.#spec = spec; } @@ -142,6 +231,568 @@ export class PODGroupSpecBuilder< } }); } + + public isNotMemberOf>>( + names: [...N], + values: N["length"] extends 1 + ? PODValueTypeFromTypeName>>[] + : PODValueTupleForNamedEntries, N>[] + ): PODGroupSpecBuilder { + // Check that all names exist in entries + for (const name of names) { + const [podName, entryName] = name.split("."); + if ( + podName === undefined || + entryName === undefined || + !(podName in this.#spec.pods) || + !(entryName in this.#spec.pods[podName]!.entries) + ) { + 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 statement: IsNotMemberOf, N> = { + entries: names, + type: "isNotMemberOf", + isNotMemberOf: convertValuesToStringTuples(names, values) + }; + + const baseName = `${names.join("_")}_isNotMemberOf`; + let statementName = baseName; + let suffix = 1; + + while (statementName in this.#spec.statements) { + statementName = `${baseName}_${suffix++}`; + } + + return new PODGroupSpecBuilder({ + ...this.#spec, + statements: { + ...this.#spec.statements, + [statementName]: statement + } + }); + } + + public inRange< + N extends keyof EntriesOfType, SupportsRangeChecks> & + string + >( + name: N, + range: { + min: AllPODEntries

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

[N] extends "date" ? Date : bigint; + } + ): PODGroupSpecBuilder { + // Check that the entry exists + const [podName, entryName] = name.split("."); + if ( + podName === undefined || + entryName === undefined || + !(podName in this.#spec.pods) || + !(entryName in this.#spec.pods[podName]!.entries) + ) { + 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: InRange, N> = { + entry: 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 = `${name}_inRange`; + let statementName = baseName; + let suffix = 1; + + while (statementName in this.#spec.statements) { + statementName = `${baseName}_${suffix++}`; + } + + return new PODGroupSpecBuilder({ + ...this.#spec, + statements: { + ...this.#spec.statements, + [statementName]: statement + } + }); + } + + public notInRange< + N extends keyof EntriesOfType, SupportsRangeChecks> & + string + >( + name: N, + range: { + min: AllPODEntries

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

[N] extends "date" ? Date : bigint; + } + ): PODGroupSpecBuilder { + // Check that the entry exists + const [podName, entryName] = name.split("."); + if ( + podName === undefined || + entryName === undefined || + !(podName in this.#spec.pods) || + !(entryName in this.#spec.pods[podName]!.entries) + ) { + 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: NotInRange, N> = { + entry: 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 = `${name}_notInRange`; + let statementName = baseName; + let suffix = 1; + + while (statementName in this.#spec.statements) { + statementName = `${baseName}_${suffix++}`; + } + + return new PODGroupSpecBuilder({ + ...this.#spec, + statements: { + ...this.#spec.statements, + [statementName]: statement + } + }); + } + + public greaterThan< + N1 extends keyof AllPODEntries

& string, + N2 extends keyof EntriesOfType, EntryType> & string + >(name1: N1, name2: N2): PODGroupSpecBuilder { + // Check that both entries exist + const [pod1, entry1] = name1.split("."); + const [pod2, entry2] = name2.split("."); + if ( + pod1 === undefined || + entry1 === undefined || + !(pod1 in this.#spec.pods) || + !(entry1 in this.#spec.pods[pod1]!.entries) + ) { + throw new Error(`Entry "${name1}" does not exist`); + } + if ( + pod2 === undefined || + entry2 === undefined || + !(pod2 in this.#spec.pods) || + !(entry2 in this.#spec.pods[pod2]!.entries) + ) { + throw new Error(`Entry "${name2}" does not exist`); + } + + if ((name1 as string) === (name2 as string)) { + 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: GreaterThan, N1, N2> = { + entry: name1, + type: "greaterThan", + otherEntry: name2 + }; + + const baseName = `${name1}_${name2}_greaterThan`; + let statementName = baseName; + let suffix = 1; + + while (statementName in this.#spec.statements) { + statementName = `${baseName}_${suffix++}`; + } + + return new PODGroupSpecBuilder({ + ...this.#spec, + statements: { + ...this.#spec.statements, + [statementName]: statement + } + }); + } + + public greaterThanEq< + N1 extends keyof AllPODEntries

& string, + N2 extends keyof EntriesOfType, EntryType> & string + >(name1: N1, name2: N2): PODGroupSpecBuilder { + // Check that both entries exist + const [pod1, entry1] = name1.split("."); + const [pod2, entry2] = name2.split("."); + if ( + pod1 === undefined || + entry1 === undefined || + !(pod1 in this.#spec.pods) || + !(entry1 in this.#spec.pods[pod1]!.entries) + ) { + throw new Error(`Entry "${name1}" does not exist`); + } + if ( + pod2 === undefined || + entry2 === undefined || + !(pod2 in this.#spec.pods) || + !(entry2 in this.#spec.pods[pod2]!.entries) + ) { + throw new Error(`Entry "${name2}" does not exist`); + } + + if ((name1 as string) === (name2 as string)) { + 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: GreaterThanEq, N1, N2> = { + entry: name1, + type: "greaterThanEq", + otherEntry: name2 + }; + + const baseName = `${name1}_${name2}_greaterThanEq`; + let statementName = baseName; + let suffix = 1; + + while (statementName in this.#spec.statements) { + statementName = `${baseName}_${suffix++}`; + } + + return new PODGroupSpecBuilder({ + ...this.#spec, + statements: { + ...this.#spec.statements, + [statementName]: statement + } + }); + } + + public lessThan< + N1 extends keyof AllPODEntries

& string, + N2 extends keyof EntriesOfType, EntryType> & string + >(name1: N1, name2: N2): PODGroupSpecBuilder { + // Check that both entries exist + const [pod1, entry1] = name1.split("."); + const [pod2, entry2] = name2.split("."); + if ( + pod1 === undefined || + entry1 === undefined || + !(pod1 in this.#spec.pods) || + !(entry1 in this.#spec.pods[pod1]!.entries) + ) { + throw new Error(`Entry "${name1}" does not exist`); + } + if ( + pod2 === undefined || + entry2 === undefined || + !(pod2 in this.#spec.pods) || + !(entry2 in this.#spec.pods[pod2]!.entries) + ) { + throw new Error(`Entry "${name2}" does not exist`); + } + + if ((name1 as string) === (name2 as string)) { + 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: LessThan, N1, N2> = { + entry: name1, + type: "lessThan", + otherEntry: name2 + }; + + const baseName = `${name1}_${name2}_lessThan`; + let statementName = baseName; + let suffix = 1; + + while (statementName in this.#spec.statements) { + statementName = `${baseName}_${suffix++}`; + } + + return new PODGroupSpecBuilder({ + ...this.#spec, + statements: { + ...this.#spec.statements, + [statementName]: statement + } + }); + } + + public lessThanEq< + N1 extends keyof AllPODEntries

& string, + N2 extends keyof EntriesOfType, EntryType> & string + >(name1: N1, name2: N2): PODGroupSpecBuilder { + // Check that both entries exist + const [pod1, entry1] = name1.split("."); + const [pod2, entry2] = name2.split("."); + if ( + pod1 === undefined || + entry1 === undefined || + !(pod1 in this.#spec.pods) || + !(entry1 in this.#spec.pods[pod1]!.entries) + ) { + throw new Error(`Entry "${name1}" does not exist`); + } + if ( + pod2 === undefined || + entry2 === undefined || + !(pod2 in this.#spec.pods) || + !(entry2 in this.#spec.pods[pod2]!.entries) + ) { + throw new Error(`Entry "${name2}" does not exist`); + } + + if ((name1 as string) === (name2 as string)) { + 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: LessThanEq, N1, N2> = { + entry: name1, + type: "lessThanEq", + otherEntry: name2 + }; + + const baseName = `${name1}_${name2}_lessThanEq`; + let statementName = baseName; + let suffix = 1; + + while (statementName in this.#spec.statements) { + statementName = `${baseName}_${suffix++}`; + } + + return new PODGroupSpecBuilder({ + ...this.#spec, + statements: { + ...this.#spec.statements, + [statementName]: statement + } + }); + } + + public equalsEntry< + N1 extends keyof AllPODEntries

& string, + N2 extends keyof EntriesOfType< + Evaluate>, + EntryType + > & + string + >(name1: N1, name2: Exclude): PODGroupSpecBuilder { + // Check that both entries exist + const [pod1, entry1] = name1.split("."); + const [pod2, entry2] = name2.split("."); + if ( + pod1 === undefined || + entry1 === undefined || + !(pod1 in this.#spec.pods) || + !(entry1 in this.#spec.pods[pod1]!.entries) + ) { + throw new Error(`Entry "${name1}" does not exist`); + } + if ( + pod2 === undefined || + entry2 === undefined || + !(pod2 in this.#spec.pods) || + !(entry2 in this.#spec.pods[pod2]!.entries) + ) { + throw new Error(`Entry "${name2}" does not exist`); + } + + if ((name1 as string) === (name2 as string)) { + 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: EqualsEntry, N1, N2> = { + entry: name1, + type: "equalsEntry", + otherEntry: name2 + }; + + const baseName = `${name1}_${name2}_equalsEntry`; + let statementName = baseName; + let suffix = 1; + + while (statementName in this.#spec.statements) { + statementName = `${baseName}_${suffix++}`; + } + + return new PODGroupSpecBuilder({ + ...this.#spec, + statements: { + ...this.#spec.statements, + [statementName]: statement + } + }); + } + + public notEqualsEntry< + N1 extends keyof AllPODEntries

& string, + N2 extends keyof EntriesOfType, EntryType> & string + >(name1: N1, name2: N2): PODGroupSpecBuilder { + // Check that both entries exist + const [pod1, entry1] = name1.split("."); + const [pod2, entry2] = name2.split("."); + if ( + pod1 === undefined || + entry1 === undefined || + !(pod1 in this.#spec.pods) || + !(entry1 in this.#spec.pods[pod1]!.entries) + ) { + throw new Error(`Entry "${name1}" does not exist`); + } + if ( + pod2 === undefined || + entry2 === undefined || + !(pod2 in this.#spec.pods) || + !(entry2 in this.#spec.pods[pod2]!.entries) + ) { + throw new Error(`Entry "${name2}" does not exist`); + } + + if ((name1 as string) === (name2 as string)) { + 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: NotEqualsEntry, N1, N2> = { + entry: name1, + type: "notEqualsEntry", + otherEntry: name2 + }; + + const baseName = `${name1}_${name2}_notEqualsEntry`; + let statementName = baseName; + let suffix = 1; + + while (statementName in this.#spec.statements) { + statementName = `${baseName}_${suffix++}`; + } + + return new PODGroupSpecBuilder({ + ...this.#spec, + statements: { + ...this.#spec.statements, + [statementName]: statement + } + }); + } } if (import.meta.vitest) { @@ -149,7 +800,9 @@ if (import.meta.vitest) { it("PODGroupSpecBuilder", () => { const group = PODGroupSpecBuilder.create(); - const podBuilder = PODSpecBuilder.create().entry("my_string", "string"); + const podBuilder = PODSpecBuilder.create() + .entry("my_string", "string") + .entry("my_num", "int"); const groupWithPod = group.pod("foo", podBuilder.spec()); const _spec = groupWithPod.spec(); @@ -157,7 +810,8 @@ if (import.meta.vitest) { // for the 'foo' pod, as well as the virtual entries. assertType>({ "foo.my_string": "string", - "foo.$signerPublicKey": "string", + "foo.my_num": "int", + "foo.$signerPublicKey": "eddsa_pubkey", "foo.$contentID": "string", "foo.$signature": "string" }); @@ -188,4 +842,75 @@ if (import.meta.vitest) { } }); }); + + 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": "string", + "foo.$signature": "string", + "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 + }); + + it("debug AllPODEntries types", () => { + const group = PODGroupSpecBuilder.create(); + const podBuilder = PODSpecBuilder.create() + .entry("my_string", "string") + .entry("my_other_string", "string") + .entry("my_num", "int"); + + const groupWithPod = group.pod("foo", podBuilder.spec()); + + type TestPods = ReturnType["pods"]; + + // Verify type equivalence + type TestPodEntries = AllPODEntries; + + // Check that entries are exactly the types we expect + type Test1 = TestPodEntries["foo.my_string"] extends "string" + ? true + : false; // should be true + type Test2 = "string" extends TestPodEntries["foo.my_string"] + ? true + : false; // should be true + type Test3 = TestPodEntries["foo.my_num"] extends "int" ? true : false; // should be true + type Test4 = "int" extends TestPodEntries["foo.my_num"] ? true : false; // should be true + + // Verify that the types are exactly equal + type Test5 = TestPodEntries["foo.my_string"] extends "string" + ? true + : false; // should be true + + assertType(true); + assertType(true); + assertType(true); + assertType(true); + assertType(true); + }); } + +type PodTest

= { + [K in keyof P]: { + [E in keyof (P[K]["entries"] & VirtualEntries) as `${K & string}.${E & + string}`]: (P[K]["entries"] & VirtualEntries)[E]; + }; +}[keyof P]; diff --git a/packages/podspec/src/builders/pod.ts b/packages/podspec/src/builders/pod.ts index fa36e4d..dba9ca3 100644 --- a/packages/podspec/src/builders/pod.ts +++ b/packages/podspec/src/builders/pod.ts @@ -1,7 +1,5 @@ import { checkPODName, - POD_CRYPTOGRAPHIC_MAX, - POD_CRYPTOGRAPHIC_MIN, POD_DATE_MAX, POD_DATE_MIN, POD_INT_MAX, @@ -10,6 +8,7 @@ import { import { convertValuesToStringTuples, deepFreeze, + supportsRangeChecks, validateRange } from "./shared.js"; import type { @@ -21,7 +20,11 @@ import type { NotEqualsEntry, SupportsRangeChecks, StatementName, - StatementMap + StatementMap, + GreaterThan, + GreaterThanEq, + LessThan, + LessThanEq } from "./types/statements.js"; import type { EntriesOfType, @@ -61,9 +64,9 @@ function canonicalizeJSON(input: unknown): string | undefined { } const virtualEntries: VirtualEntries = { - $contentID: { type: "string" }, - $signature: { type: "string" }, - $signerPublicKey: { type: "eddsa_pubkey" } + $contentID: "string", + $signature: "string", + $signerPublicKey: "eddsa_pubkey" }; export type PODSpec = { @@ -74,22 +77,6 @@ export type PODSpec = { // This is a compile-time check that the PODSpec is JSON-safe true satisfies IsJsonSafe>; -type DoesNotSupportRangeChecks = Exclude; - -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; - } -} - /** * Given a list of entry names, return the names of the entries that are not in the list */ @@ -221,15 +208,16 @@ export class PODSpecBuilder< case "notInRange": return keys.includes(statement.entry as K); case "equalsEntry": - return ( - keys.includes(statement.entry as K) && - keys.includes(statement.equalsEntry as K) - ); case "notEqualsEntry": + case "greaterThan": + case "greaterThanEq": + case "lessThan": + case "lessThanEq": return ( keys.includes(statement.entry as K) && - keys.includes(statement.notEqualsEntry as K) + keys.includes(statement.otherEntry as K) ); + default: const _exhaustiveCheck: never = statement; throw new Error( @@ -535,29 +523,36 @@ export class PODSpecBuilder< throw new Error(`Entry "${name}" does not exist`); } - const entryType = this.#spec.entries[name]; - - if (entryType === "int") { - validateRange( - range.min as bigint, - range.max as bigint, - POD_INT_MIN, - POD_INT_MAX - ); - } else if (entryType === "cryptographic") { - validateRange( - range.min as bigint, - range.max as bigint, - POD_CRYPTOGRAPHIC_MIN, - POD_CRYPTOGRAPHIC_MAX - ); - } else if (entryType === "date") { - validateRange( - range.min as Date, - range.max as Date, - POD_DATE_MIN, - POD_DATE_MAX - ); + 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 = { @@ -594,10 +589,14 @@ export class PODSpecBuilder< public equalsEntry< N1 extends keyof (E & VirtualEntries) & string, - N2 extends keyof (E & VirtualEntries) & string + N2 extends keyof EntriesOfType< + E & VirtualEntries, + (E & VirtualEntries)[N1] + > & + string >( name1: N1, - name2: E[N2] extends E[N1] ? N2 : never + name2: Exclude ): PODSpecBuilder< E, S & { @@ -621,9 +620,8 @@ export class PODSpecBuilder< const statement = { entry: name1, type: "equalsEntry", - equalsEntry: name2 - // We know that the types are compatible, so we can cast to the correct type - } as unknown as EqualsEntry; + otherEntry: name2 + } satisfies EqualsEntry; const baseName = `${name1}_${name2}_equalsEntry`; let statementName = baseName; @@ -643,11 +641,15 @@ export class PODSpecBuilder< } public notEqualsEntry< - N1 extends keyof E & VirtualEntries & string, - N2 extends keyof E & VirtualEntries & string + N1 extends keyof (E & VirtualEntries) & string, + N2 extends keyof EntriesOfType< + E & VirtualEntries, + (E & VirtualEntries)[N1] + > & + string >( name1: N1, - name2: E[N2] extends E[N1] ? N2 : never + name2: Exclude ): PODSpecBuilder< E, S & { @@ -675,9 +677,8 @@ export class PODSpecBuilder< const statement = { entry: name1, type: "notEqualsEntry", - notEqualsEntry: name2 - // We know that the types are compatible, so we can cast to the correct type - } as unknown as NotEqualsEntry; + otherEntry: name2 + } satisfies NotEqualsEntry; const baseName = `${name1}_${name2}_notEqualsEntry`; let statementName = baseName; @@ -695,6 +696,222 @@ export class PODSpecBuilder< } }); } + + public greaterThan< + N1 extends keyof (E & VirtualEntries) & string, + N2 extends keyof EntriesOfType< + E & VirtualEntries, + (E & VirtualEntries)[N1] + > & + string + >( + name1: N1, + name2: Exclude + ): PODSpecBuilder< + E, + S & { + [K in StatementName<[N1, N2], "greaterThan", S>]: GreaterThan; + } + > { + // Check that both names exist in entries + if (!(name1 in this.#spec.entries) && !(name1 in virtualEntries)) { + throw new Error(`Entry "${name1}" does not exist`); + } + if (!(name2 in this.#spec.entries) && !(name2 in virtualEntries)) { + throw new Error(`Entry "${name2}" does not exist`); + } + if ((name1 as string) === (name2 as string)) { + throw new Error("Entry names must be different"); + } + if ((this.#spec.entries[name1] as string) !== this.#spec.entries[name2]) { + throw new Error("Entry types must be the same"); + } + + const statement = { + entry: name1, + type: "greaterThan", + otherEntry: name2 + } satisfies GreaterThan; + + const baseName = `${name1}_${name2}_greaterThan`; + let statementName = baseName; + let suffix = 1; + + while (statementName in this.#spec.statements) { + statementName = `${baseName}_${suffix++}`; + } + + return new PODSpecBuilder({ + ...this.#spec, + statements: { + ...this.#spec.statements, + [statementName]: statement + } + }); + } + + public greaterThanEq< + N1 extends keyof (E & VirtualEntries) & string, + N2 extends keyof EntriesOfType< + E & VirtualEntries, + (E & VirtualEntries)[N1] + > & + string + >( + name1: N1, + name2: Exclude + ): PODSpecBuilder< + E, + S & { + [K in StatementName<[N1, N2], "greaterThanEq", S>]: GreaterThanEq< + E, + N1, + N2 + >; + } + > { + // Check that both names exist in entries + if (!(name1 in this.#spec.entries) && !(name1 in virtualEntries)) { + throw new Error(`Entry "${name1}" does not exist`); + } + if (!(name2 in this.#spec.entries) && !(name2 in virtualEntries)) { + throw new Error(`Entry "${name2}" does not exist`); + } + if ((name1 as string) === (name2 as string)) { + throw new Error("Entry names must be different"); + } + if ((this.#spec.entries[name1] as string) !== this.#spec.entries[name2]) { + throw new Error("Entry types must be the same"); + } + + const statement = { + entry: name1, + type: "greaterThanEq", + otherEntry: name2 + } satisfies GreaterThanEq; + + const baseName = `${name1}_${name2}_greaterThanEq`; + let statementName = baseName; + let suffix = 1; + + while (statementName in this.#spec.statements) { + statementName = `${baseName}_${suffix++}`; + } + + return new PODSpecBuilder({ + ...this.#spec, + statements: { + ...this.#spec.statements, + [statementName]: statement + } + }); + } + + public lessThan< + N1 extends keyof (E & VirtualEntries) & string, + N2 extends keyof EntriesOfType< + E & VirtualEntries, + (E & VirtualEntries)[N1] + > & + string + >( + name1: N1, + name2: Exclude + ): PODSpecBuilder< + E, + S & { + [K in StatementName<[N1, N2], "lessThan", S>]: LessThan; + } + > { + // Check that both names exist in entries + if (!(name1 in this.#spec.entries) && !(name1 in virtualEntries)) { + throw new Error(`Entry "${name1}" does not exist`); + } + if (!(name2 in this.#spec.entries) && !(name2 in virtualEntries)) { + throw new Error(`Entry "${name2}" does not exist`); + } + if ((name1 as string) === (name2 as string)) { + throw new Error("Entry names must be different"); + } + if ((this.#spec.entries[name1] as string) !== this.#spec.entries[name2]) { + throw new Error("Entry types must be the same"); + } + + const statement = { + entry: name1, + type: "lessThan", + otherEntry: name2 + } satisfies LessThan; + + const baseName = `${name1}_${name2}_lessThan`; + let statementName = baseName; + let suffix = 1; + + while (statementName in this.#spec.statements) { + statementName = `${baseName}_${suffix++}`; + } + + return new PODSpecBuilder({ + ...this.#spec, + statements: { + ...this.#spec.statements, + [statementName]: statement + } + }); + } + + public lessThanEq< + N1 extends keyof (E & VirtualEntries) & string, + N2 extends keyof EntriesOfType< + E & VirtualEntries, + (E & VirtualEntries)[N1] + > & + string + >( + name1: N1, + name2: Exclude + ): PODSpecBuilder< + E, + S & { + [K in StatementName<[N1, N2], "lessThanEq", S>]: LessThanEq; + } + > { + // Check that both names exist in entries + if (!(name1 in this.#spec.entries) && !(name1 in virtualEntries)) { + throw new Error(`Entry "${name1}" does not exist`); + } + if (!(name2 in this.#spec.entries) && !(name2 in virtualEntries)) { + throw new Error(`Entry "${name2}" does not exist`); + } + if ((name1 as string) === (name2 as string)) { + throw new Error("Entry names must be different"); + } + if ((this.#spec.entries[name1] as string) !== this.#spec.entries[name2]) { + throw new Error("Entry types must be the same"); + } + + const statement = { + entry: name1, + type: "lessThanEq", + otherEntry: name2 + } satisfies LessThanEq; + + const baseName = `${name1}_${name2}_lessThanEq`; + let statementName = baseName; + let suffix = 1; + + while (statementName in this.#spec.statements) { + statementName = `${baseName}_${suffix++}`; + } + + return new PODSpecBuilder({ + ...this.#spec, + statements: { + ...this.#spec.statements, + [statementName]: statement + } + }); + } } if (import.meta.vitest) { @@ -789,7 +1006,7 @@ if (import.meta.vitest) { a_new_equalsEntry: { entry: "a", type: "equalsEntry", - equalsEntry: "new" + otherEntry: "new" } } } satisfies typeof _GSpec); diff --git a/packages/podspec/src/builders/shared.ts b/packages/podspec/src/builders/shared.ts index 33f6655..6a1b37d 100644 --- a/packages/podspec/src/builders/shared.ts +++ b/packages/podspec/src/builders/shared.ts @@ -1,6 +1,7 @@ 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"; /** * Validates a range check. @@ -101,3 +102,21 @@ export function convertValuesToStringTuples( } ); } + +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; + } +} diff --git a/packages/podspec/src/builders/types/entries.ts b/packages/podspec/src/builders/types/entries.ts index 2b4efcb..389546d 100644 --- a/packages/podspec/src/builders/types/entries.ts +++ b/packages/podspec/src/builders/types/entries.ts @@ -23,7 +23,7 @@ export type EntriesOfType = { }; export type VirtualEntries = { - $contentID: { type: "string" }; - $signature: { type: "string" }; - $signerPublicKey: { type: "eddsa_pubkey" }; + $contentID: "string"; + $signature: "string"; + $signerPublicKey: "eddsa_pubkey"; }; diff --git a/packages/podspec/src/builders/types/statements.ts b/packages/podspec/src/builders/types/statements.ts index ca70892..b2a3024 100644 --- a/packages/podspec/src/builders/types/statements.ts +++ b/packages/podspec/src/builders/types/statements.ts @@ -81,25 +81,61 @@ export type EqualsEntry< E extends EntryTypes, N1 extends keyof (E & VirtualEntries) & string, N2 extends keyof (E & VirtualEntries) & string -> = E[N2] extends E[N1] - ? { - entry: N1; - type: "equalsEntry"; - equalsEntry: N2; - } - : never; +> = { + entry: N1; + type: "equalsEntry"; + otherEntry: N2; +}; export type NotEqualsEntry< E extends EntryTypes, N1 extends keyof (E & VirtualEntries) & string, N2 extends keyof (E & VirtualEntries) & string -> = E[N2] extends E[N1] - ? { - entry: N1; - type: "notEqualsEntry"; - notEqualsEntry: N2; - } - : never; +> = { + entry: N1; + type: "notEqualsEntry"; + otherEntry: N2; +}; + +export type GreaterThan< + E extends EntryTypes, + N1 extends keyof (E & VirtualEntries) & string, + N2 extends keyof (E & VirtualEntries) & string +> = { + entry: N1; + type: "greaterThan"; + otherEntry: N2; +}; + +export type GreaterThanEq< + E extends EntryTypes, + N1 extends keyof (E & VirtualEntries) & string, + N2 extends keyof (E & VirtualEntries) & string +> = { + entry: N1; + type: "greaterThanEq"; + otherEntry: N2; +}; + +export type LessThan< + E extends EntryTypes, + N1 extends keyof (E & VirtualEntries) & string, + N2 extends keyof (E & VirtualEntries) & string +> = { + entry: N1; + type: "lessThan"; + otherEntry: N2; +}; + +export type LessThanEq< + E extends EntryTypes, + N1 extends keyof (E & VirtualEntries) & string, + N2 extends keyof (E & VirtualEntries) & string +> = { + entry: N1; + type: "lessThanEq"; + otherEntry: N2; +}; export type Statements = // eslint-disable-next-line @typescript-eslint/no-explicit-any @@ -113,7 +149,15 @@ export type Statements = // eslint-disable-next-line @typescript-eslint/no-explicit-any | EqualsEntry // eslint-disable-next-line @typescript-eslint/no-explicit-any - | NotEqualsEntry; + | 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; diff --git a/packages/podspec/src/processors/validate/checks/notInRange.ts b/packages/podspec/src/processors/validate/checks/notInRange.ts new file mode 100644 index 0000000..864771b --- /dev/null +++ b/packages/podspec/src/processors/validate/checks/notInRange.ts @@ -0,0 +1,49 @@ +import { isPODArithmeticValue, type PODEntries } from "@pcd/pod"; +import type { NotInRange } from "../../../builders/types/statements.js"; +import { + IssueCode, + type ValidationBaseIssue, + type ValidationStatementNegativeResultIssue +} from "../issues.js"; +import type { EntryTypes } from "../../../builders/types/entries.js"; + +export function checkNotInRange( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + statement: NotInRange, + statementName: string, + path: string[], + entries: PODEntries, + _specEntries: EntryTypes, + _exitOnError: boolean +): ValidationBaseIssue[] { + const entryName = statement.entry; + const entry = entries[entryName]!; + + // TODO need an issue type for statement referring to a non-existent entry + // or entry of the wrong type + + 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 []; +} From d14ae24319b06410f6c6c1da5493305e14d4ec4c Mon Sep 17 00:00:00 2001 From: Rob Knight Date: Sat, 1 Feb 2025 14:26:15 +0100 Subject: [PATCH 09/20] Add EntrySource and better audit of PODSpec validity --- packages/podspec/src/builders/group.ts | 111 +---- packages/podspec/src/generated/podspec.ts | 424 +++++++++--------- packages/podspec/src/processors/validate.ts | 134 +++--- .../src/processors/validate/EntrySource.ts | 195 ++++++++ .../processors/validate/checks/equalsEntry.ts | 95 ++++ .../src/processors/validate/checks/inRange.ts | 22 +- .../processors/validate/checks/isMemberOf.ts | 20 +- .../validate/checks/isNotMemberOf.ts | 20 +- .../validate/checks/notEqualsEntry.ts | 95 ++++ .../processors/validate/checks/notInRange.ts | 22 +- .../podspec/src/processors/validate/issues.ts | 19 + 11 files changed, 768 insertions(+), 389 deletions(-) create mode 100644 packages/podspec/src/processors/validate/EntrySource.ts create mode 100644 packages/podspec/src/processors/validate/checks/equalsEntry.ts create mode 100644 packages/podspec/src/processors/validate/checks/notEqualsEntry.ts diff --git a/packages/podspec/src/builders/group.ts b/packages/podspec/src/builders/group.ts index 434aadc..433cba0 100644 --- a/packages/podspec/src/builders/group.ts +++ b/packages/podspec/src/builders/group.ts @@ -35,9 +35,8 @@ import { supportsRangeChecks, validateRange } from "./shared.js"; -import { assertType } from "vitest"; -type PODGroupPODs = Record>; +export type NamedPODSpecs = Record>; // @TODO add group constraints, where instead of extending EntryListSpec, // we have some kind of group entry list, with each entry name prefixed @@ -49,108 +48,50 @@ type PODGroupPODs = Record>; // isMemberOf: N[number]; // }; -export type PODGroupSpec

= { +export type PODGroupSpec

= { pods: P; statements: S; }; -// type AllPODEntries

= { -// [K in keyof P]: { -// [E in keyof (P[K]["entries"] & VirtualEntries) as `${K & string}.${E & -// string}`]: (P[K]["entries"] & VirtualEntries)[E]; -// }; -// }[keyof P]; - -// First get the entries with pod name prefixes -type PodEntries

= { +type AllPODEntries

= { [K in keyof P]: { [E in keyof (P[K]["entries"] & VirtualEntries) as `${K & string}.${E & string}`]: (P[K]["entries"] & VirtualEntries)[E]; }; }[keyof P]; -// Then ensure we only get POD value types -type AllPODEntries

= { - [K in keyof PodEntries

]: PodEntries

[K] extends PODValueType - ? PodEntries

[K] - : never; -}; - type MustBePODValueType = T extends PODValueType ? T : never; -// Add this helper type to preserve literal types type EntryType< - P extends PODGroupPODs, + P extends NamedPODSpecs, K extends keyof AllPODEntries

> = MustBePODValueType[K]>; -// type AddEntry< -// E extends EntryListSpec, -// K extends keyof E, -// V extends PODValueType -// > = Concrete; - type Evaluate = T extends infer O ? { [K in keyof O]: O[K] } : never; type AddPOD< - PODs extends PODGroupPODs, + PODs extends NamedPODSpecs, N extends PODName, Spec extends PODSpec > = Evaluate<{ [K in keyof PODs | N]: K extends N ? Spec : PODs[K & keyof PODs]; }>; -type PODGroupWithFirstPOD< - First extends PODSpec, - Rest extends PODGroupPODs -> = { - firstPOD: First; -} & Rest; - -class BasePODGroupSpecBuilder

{ - protected readonly spec: PODGroupSpec; - - protected constructor(spec: PODGroupSpec) { - this.spec = spec; - } - - // eslint-disable-next-line @typescript-eslint/no-empty-object-type - public static create(): BasePODGroupSpecBuilder<{}, {}> { - return new BasePODGroupSpecBuilder({ - pods: {}, - statements: {} - }); - } - - public pod< - N extends PODName, - Spec extends PODSpec, - NewPods extends AddPOD - >(name: N, spec: Spec): PODGroupSpecBuilder { - if (name in this.spec.pods) { - throw new Error(`POD "${name}" already exists`); - } - - checkPODName(name); - - return new PODGroupSpecBuilder({ - ...this.spec, - pods: { ...this.spec.pods, [name]: spec } as unknown as NewPods - }); - } -} - +// 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 PODGroupWithFirstPOD< - PODSpec, - PODGroupPODs - >, + P extends NamedPODSpecs, S extends StatementMap -> extends BasePODGroupSpecBuilder { +> { readonly #spec: PODGroupSpec; - constructor(spec: PODGroupSpec) { - super(spec); + private constructor(spec: PODGroupSpec) { this.#spec = spec; } @@ -678,12 +619,8 @@ export class PODGroupSpecBuilder< public equalsEntry< N1 extends keyof AllPODEntries

& string, - N2 extends keyof EntriesOfType< - Evaluate>, - EntryType - > & - string - >(name1: N1, name2: Exclude): PODGroupSpecBuilder { + N2 extends keyof EntriesOfType, EntryType> & string + >(name1: N1, name2: N2): PODGroupSpecBuilder { // Check that both entries exist const [pod1, entry1] = name1.split("."); const [pod2, entry2] = name2.split("."); @@ -867,8 +804,8 @@ if (import.meta.vitest) { 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 + type _T1 = Parameters[0]; // First parameter type + type _T2 = Parameters[1]; // Second parameter type }); it("debug AllPODEntries types", () => { @@ -878,12 +815,12 @@ if (import.meta.vitest) { .entry("my_other_string", "string") .entry("my_num", "int"); - const groupWithPod = group.pod("foo", podBuilder.spec()); + const _groupWithPod = group.pod("foo", podBuilder.spec()); - type TestPods = ReturnType["pods"]; + type TestPods = ReturnType["pods"]; // Verify type equivalence - type TestPodEntries = AllPODEntries; + type TestPodEntries = PodTest; // Check that entries are exactly the types we expect type Test1 = TestPodEntries["foo.my_string"] extends "string" @@ -908,7 +845,7 @@ if (import.meta.vitest) { }); } -type PodTest

= { +type PodTest

= { [K in keyof P]: { [E in keyof (P[K]["entries"] & VirtualEntries) as `${K & string}.${E & string}`]: (P[K]["entries"] & VirtualEntries)[E]; diff --git a/packages/podspec/src/generated/podspec.ts b/packages/podspec/src/generated/podspec.ts index 15d1c60..62c70ac 100644 --- a/packages/podspec/src/generated/podspec.ts +++ b/packages/podspec/src/generated/podspec.ts @@ -43,16 +43,7 @@ export const assertPODSpec = (() => { input.isMemberOf.every( (elem: any) => Array.isArray(elem) && - elem.every( - (elem: any) => - undefined !== elem && - (null === elem || - "string" === typeof elem || - "bigint" === typeof elem || - "boolean" === typeof elem || - elem instanceof Uint8Array || - elem instanceof Date) - ) + elem.every((elem: any) => "string" === typeof elem) ); const _io4 = (input: any): boolean => Array.isArray(input.entries) && @@ -62,16 +53,7 @@ export const assertPODSpec = (() => { input.isNotMemberOf.every( (elem: any) => Array.isArray(elem) && - elem.every( - (elem: any) => - undefined !== elem && - (null === elem || - "string" === typeof elem || - "bigint" === typeof elem || - "boolean" === typeof elem || - elem instanceof Uint8Array || - elem instanceof Date) - ) + elem.every((elem: any) => "string" === typeof elem) ); const _io5 = (input: any): boolean => "string" === typeof input.entry && @@ -79,38 +61,48 @@ export const assertPODSpec = (() => { "object" === typeof input.inRange && null !== input.inRange && _io6(input.inRange); const _io6 = (input: any): boolean => - null !== input.min && - undefined !== input.min && - ("bigint" === typeof input.min || input.min instanceof Date) && - null !== input.max && undefined !== input.max && - ("bigint" === typeof input.max || input.max instanceof Date); + "string" === typeof input.min && "string" === typeof input.max; const _io7 = (input: any): boolean => "string" === typeof input.entry && "notInRange" === input.type && "object" === typeof input.notInRange && null !== input.notInRange && - _io8(input.notInRange); + _io6(input.notInRange); const _io8 = (input: any): boolean => - null !== input.min && - undefined !== input.min && - ("bigint" === typeof input.min || input.min instanceof Date) && - null !== input.max && undefined !== input.max && - ("bigint" === typeof input.max || input.max instanceof Date); - const _io9 = (input: any): boolean => "string" === typeof input.entry && "equalsEntry" === input.type && - "string" === typeof input.equalsEntry; - const _io10 = (input: any): boolean => + "string" === typeof input.otherEntry; + const _io9 = (input: any): boolean => "string" === typeof input.entry && "notEqualsEntry" === input.type && - "string" === typeof input.notEqualsEntry; + "string" === typeof input.otherEntry; + const _io10 = (input: any): boolean => + "string" === typeof input.entry && + "greaterThan" === input.type && + "string" === typeof input.otherEntry; + const _io11 = (input: any): boolean => + "string" === typeof input.entry && + "greaterThanEq" === input.type && + "string" === typeof input.otherEntry; + const _io12 = (input: any): boolean => + "string" === typeof input.entry && + "lessThan" === input.type && + "string" === typeof input.otherEntry; + const _io13 = (input: any): boolean => + "string" === typeof input.entry && + "lessThanEq" === input.type && + "string" === typeof input.otherEntry; 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 ("equalsEntry" === input.type) return _io9(input); - else if ("notEqualsEntry" === input.type) return _io10(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 = ( @@ -222,7 +214,7 @@ export const assertPODSpec = (() => { key ), expected: - "(InRange | IsMemberOf> | IsNotMemberOf> | NotInRange | __type.o2 | __type.o3)", + "(EqualsEntry | GreaterThan | GreaterThanEq | InRange | IsMemberOf> | IsNotMemberOf> | LessThan | LessThanEq | NotEqualsEntry | NotInRange)", value: value }, _errorFactory @@ -245,7 +237,7 @@ export const assertPODSpec = (() => { key ), expected: - "(InRange | IsMemberOf> | IsNotMemberOf> | NotInRange | __type.o2 | __type.o3)", + "(EqualsEntry | GreaterThan | GreaterThanEq | InRange | IsMemberOf> | IsNotMemberOf> | LessThan | LessThanEq | NotEqualsEntry | NotInRange)", value: value }, _errorFactory @@ -309,8 +301,7 @@ export const assertPODSpec = (() => { { method: "typia.createAssert", path: _path + ".isMemberOf", - expected: - "Array>", + expected: "Array>", value: input.isMemberOf }, _errorFactory @@ -323,53 +314,32 @@ export const assertPODSpec = (() => { { method: "typia.createAssert", path: _path + ".isMemberOf[" + _index8 + "]", - expected: - "Array", + expected: "Array", value: elem }, _errorFactory )) && elem.every( (elem: any, _index9: number) => - (undefined !== elem || - __typia_transform__assertGuard._assertGuard( - _exceptionable, - { - method: "typia.createAssert", - path: - _path + ".isMemberOf[" + _index8 + "][" + _index9 + "]", - expected: - "(Date | Uint8Array | bigint | boolean | null | string)", - value: elem - }, - _errorFactory - )) && - (null === elem || - "string" === typeof elem || - "bigint" === typeof elem || - "boolean" === typeof elem || - elem instanceof Uint8Array || - elem instanceof Date || - __typia_transform__assertGuard._assertGuard( - _exceptionable, - { - method: "typia.createAssert", - path: - _path + ".isMemberOf[" + _index8 + "][" + _index9 + "]", - expected: - "(Date | Uint8Array | bigint | boolean | null | string)", - value: elem - }, - _errorFactory - )) + "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", + expected: "Array", value: elem }, _errorFactory @@ -380,8 +350,7 @@ export const assertPODSpec = (() => { { method: "typia.createAssert", path: _path + ".isMemberOf", - expected: - "Array>", + expected: "Array>", value: input.isMemberOf }, _errorFactory @@ -443,8 +412,7 @@ export const assertPODSpec = (() => { { method: "typia.createAssert", path: _path + ".isNotMemberOf", - expected: - "Array>", + expected: "Array>", value: input.isNotMemberOf }, _errorFactory @@ -457,63 +425,37 @@ export const assertPODSpec = (() => { { method: "typia.createAssert", path: _path + ".isNotMemberOf[" + _index11 + "]", - expected: - "Array", + expected: "Array", value: elem }, _errorFactory )) && elem.every( (elem: any, _index12: number) => - (undefined !== elem || - __typia_transform__assertGuard._assertGuard( - _exceptionable, - { - method: "typia.createAssert", - path: - _path + - ".isNotMemberOf[" + - _index11 + - "][" + - _index12 + - "]", - expected: - "(Date | Uint8Array | bigint | boolean | null | string)", - value: elem - }, - _errorFactory - )) && - (null === elem || - "string" === typeof elem || - "bigint" === typeof elem || - "boolean" === typeof elem || - elem instanceof Uint8Array || - elem instanceof Date || - __typia_transform__assertGuard._assertGuard( - _exceptionable, - { - method: "typia.createAssert", - path: - _path + - ".isNotMemberOf[" + - _index11 + - "][" + - _index12 + - "]", - expected: - "(Date | Uint8Array | bigint | boolean | null | string)", - value: elem - }, - _errorFactory - )) + "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", + expected: "Array", value: elem }, _errorFactory @@ -524,8 +466,7 @@ export const assertPODSpec = (() => { { method: "typia.createAssert", path: _path + ".isNotMemberOf", - expected: - "Array>", + expected: "Array>", value: input.isNotMemberOf }, _errorFactory @@ -563,7 +504,7 @@ export const assertPODSpec = (() => { { method: "typia.createAssert", path: _path + ".inRange", - expected: "__type", + expected: "RangePersistent", value: input.inRange }, _errorFactory @@ -574,7 +515,7 @@ export const assertPODSpec = (() => { { method: "typia.createAssert", path: _path + ".inRange", - expected: "__type", + expected: "RangePersistent", value: input.inRange }, _errorFactory @@ -584,75 +525,78 @@ export const assertPODSpec = (() => { _path: string, _exceptionable: boolean = true ): boolean => - (null !== input.min || + ("string" === typeof input.min || __typia_transform__assertGuard._assertGuard( _exceptionable, { method: "typia.createAssert", path: _path + ".min", - expected: "(Date | bigint)", + expected: "string", value: input.min }, _errorFactory )) && - (undefined !== input.min || + ("string" === typeof input.max || __typia_transform__assertGuard._assertGuard( _exceptionable, { method: "typia.createAssert", - path: _path + ".min", - expected: "(Date | bigint)", - value: input.min + path: _path + ".max", + expected: "string", + value: input.max + }, + _errorFactory + )); + const _ao7 = ( + input: any, + _path: string, + _exceptionable: boolean = true + ): boolean => + ("string" === typeof input.entry || + __typia_transform__assertGuard._assertGuard( + _exceptionable, + { + method: "typia.createAssert", + path: _path + ".entry", + expected: "string", + value: input.entry }, _errorFactory )) && - ("bigint" === typeof input.min || - input.min instanceof Date || + ("notInRange" === input.type || __typia_transform__assertGuard._assertGuard( _exceptionable, { method: "typia.createAssert", - path: _path + ".min", - expected: "(Date | bigint)", - value: input.min + path: _path + ".type", + expected: '"notInRange"', + value: input.type }, _errorFactory )) && - (null !== input.max || + (((("object" === typeof input.notInRange && null !== input.notInRange) || __typia_transform__assertGuard._assertGuard( _exceptionable, { method: "typia.createAssert", - path: _path + ".max", - expected: "(Date | bigint)", - value: input.max + path: _path + ".notInRange", + expected: "RangePersistent", + value: input.notInRange }, _errorFactory )) && - (undefined !== input.max || - __typia_transform__assertGuard._assertGuard( - _exceptionable, - { - method: "typia.createAssert", - path: _path + ".max", - expected: "(Date | bigint)", - value: input.max - }, - _errorFactory - )) && - ("bigint" === typeof input.max || - input.max instanceof Date || + _ao6(input.notInRange, _path + ".notInRange", true && _exceptionable)) || __typia_transform__assertGuard._assertGuard( _exceptionable, { method: "typia.createAssert", - path: _path + ".max", - expected: "(Date | bigint)", - value: input.max + path: _path + ".notInRange", + expected: "RangePersistent", + value: input.notInRange }, _errorFactory )); - const _ao7 = ( + const _ao8 = ( input: any, _path: string, _exceptionable: boolean = true @@ -668,113 +612,143 @@ export const assertPODSpec = (() => { }, _errorFactory )) && - ("notInRange" === input.type || + ("equalsEntry" === input.type || __typia_transform__assertGuard._assertGuard( _exceptionable, { method: "typia.createAssert", path: _path + ".type", - expected: '"notInRange"', + expected: '"equalsEntry"', value: input.type }, _errorFactory )) && - (((("object" === typeof input.notInRange && null !== input.notInRange) || + ("string" === typeof input.otherEntry || __typia_transform__assertGuard._assertGuard( _exceptionable, { method: "typia.createAssert", - path: _path + ".notInRange", - expected: "__type.o1", - value: input.notInRange + path: _path + ".otherEntry", + expected: "string", + value: input.otherEntry + }, + _errorFactory + )); + const _ao9 = ( + input: any, + _path: string, + _exceptionable: boolean = true + ): boolean => + ("string" === typeof input.entry || + __typia_transform__assertGuard._assertGuard( + _exceptionable, + { + method: "typia.createAssert", + path: _path + ".entry", + expected: "string", + value: input.entry }, _errorFactory )) && - _ao8(input.notInRange, _path + ".notInRange", true && _exceptionable)) || + ("notEqualsEntry" === input.type || __typia_transform__assertGuard._assertGuard( _exceptionable, { method: "typia.createAssert", - path: _path + ".notInRange", - expected: "__type.o1", - value: input.notInRange + path: _path + ".type", + expected: '"notEqualsEntry"', + value: input.type + }, + _errorFactory + )) && + ("string" === typeof input.otherEntry || + __typia_transform__assertGuard._assertGuard( + _exceptionable, + { + method: "typia.createAssert", + path: _path + ".otherEntry", + expected: "string", + value: input.otherEntry }, _errorFactory )); - const _ao8 = ( + const _ao10 = ( input: any, _path: string, _exceptionable: boolean = true ): boolean => - (null !== input.min || + ("string" === typeof input.entry || __typia_transform__assertGuard._assertGuard( _exceptionable, { method: "typia.createAssert", - path: _path + ".min", - expected: "(Date | bigint)", - value: input.min + path: _path + ".entry", + expected: "string", + value: input.entry }, _errorFactory )) && - (undefined !== input.min || + ("greaterThan" === input.type || __typia_transform__assertGuard._assertGuard( _exceptionable, { method: "typia.createAssert", - path: _path + ".min", - expected: "(Date | bigint)", - value: input.min + path: _path + ".type", + expected: '"greaterThan"', + value: input.type }, _errorFactory )) && - ("bigint" === typeof input.min || - input.min instanceof Date || + ("string" === typeof input.otherEntry || __typia_transform__assertGuard._assertGuard( _exceptionable, { method: "typia.createAssert", - path: _path + ".min", - expected: "(Date | bigint)", - value: input.min + path: _path + ".otherEntry", + expected: "string", + value: input.otherEntry + }, + _errorFactory + )); + const _ao11 = ( + input: any, + _path: string, + _exceptionable: boolean = true + ): boolean => + ("string" === typeof input.entry || + __typia_transform__assertGuard._assertGuard( + _exceptionable, + { + method: "typia.createAssert", + path: _path + ".entry", + expected: "string", + value: input.entry }, _errorFactory )) && - (null !== input.max || + ("greaterThanEq" === input.type || __typia_transform__assertGuard._assertGuard( _exceptionable, { method: "typia.createAssert", - path: _path + ".max", - expected: "(Date | bigint)", - value: input.max + path: _path + ".type", + expected: '"greaterThanEq"', + value: input.type }, _errorFactory )) && - (undefined !== input.max || - __typia_transform__assertGuard._assertGuard( - _exceptionable, - { - method: "typia.createAssert", - path: _path + ".max", - expected: "(Date | bigint)", - value: input.max - }, - _errorFactory - )) && - ("bigint" === typeof input.max || - input.max instanceof Date || + ("string" === typeof input.otherEntry || __typia_transform__assertGuard._assertGuard( _exceptionable, { method: "typia.createAssert", - path: _path + ".max", - expected: "(Date | bigint)", - value: input.max + path: _path + ".otherEntry", + expected: "string", + value: input.otherEntry }, _errorFactory )); - const _ao9 = ( + const _ao12 = ( input: any, _path: string, _exceptionable: boolean = true @@ -790,29 +764,29 @@ export const assertPODSpec = (() => { }, _errorFactory )) && - ("equalsEntry" === input.type || + ("lessThan" === input.type || __typia_transform__assertGuard._assertGuard( _exceptionable, { method: "typia.createAssert", path: _path + ".type", - expected: '"equalsEntry"', + expected: '"lessThan"', value: input.type }, _errorFactory )) && - ("string" === typeof input.equalsEntry || + ("string" === typeof input.otherEntry || __typia_transform__assertGuard._assertGuard( _exceptionable, { method: "typia.createAssert", - path: _path + ".equalsEntry", + path: _path + ".otherEntry", expected: "string", - value: input.equalsEntry + value: input.otherEntry }, _errorFactory )); - const _ao10 = ( + const _ao13 = ( input: any, _path: string, _exceptionable: boolean = true @@ -828,25 +802,25 @@ export const assertPODSpec = (() => { }, _errorFactory )) && - ("notEqualsEntry" === input.type || + ("lessThanEq" === input.type || __typia_transform__assertGuard._assertGuard( _exceptionable, { method: "typia.createAssert", path: _path + ".type", - expected: '"notEqualsEntry"', + expected: '"lessThanEq"', value: input.type }, _errorFactory )) && - ("string" === typeof input.notEqualsEntry || + ("string" === typeof input.otherEntry || __typia_transform__assertGuard._assertGuard( _exceptionable, { method: "typia.createAssert", - path: _path + ".notEqualsEntry", + path: _path + ".otherEntry", expected: "string", - value: input.notEqualsEntry + value: input.otherEntry }, _errorFactory )); @@ -864,10 +838,18 @@ export const assertPODSpec = (() => { return _ao5(input, _path, true && _exceptionable); else if ("notInRange" === input.type) return _ao7(input, _path, true && _exceptionable); - else if ("equalsEntry" === input.type) - return _ao9(input, _path, true && _exceptionable); - else if ("notEqualsEntry" === input.type) + 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, @@ -875,7 +857,7 @@ export const assertPODSpec = (() => { method: "typia.createAssert", path: _path, expected: - "(IsMemberOf> | IsNotMemberOf> | InRange | NotInRange | __type.o2 | __type.o3)", + "(IsMemberOf> | IsNotMemberOf> | InRange | NotInRange | LessThanEq | LessThan | GreaterThanEq | GreaterThan | NotEqualsEntry | EqualsEntry)", value: input }, _errorFactory diff --git a/packages/podspec/src/processors/validate.ts b/packages/podspec/src/processors/validate.ts index 2e60971..83ed89c 100644 --- a/packages/podspec/src/processors/validate.ts +++ b/packages/podspec/src/processors/validate.ts @@ -4,22 +4,23 @@ import { type PODValue, type PODContent, type PODStringValue, - type PODName + type PODName, + type PODIntValue } from "@pcd/pod"; import { PODSpecBuilder, type PODSpec } from "../builders/pod.js"; import type { EntryTypes } from "../builders/types/entries.js"; import type { StatementMap } from "../builders/types/statements.js"; import type { ValidateResult } from "./validate/types.js"; import { FAILURE, SUCCESS } from "./validate/result.js"; -import { - IssueCode, - type ValidationMissingEntryIssue, - type ValidationTypeMismatchIssue, - type ValidationUnexpectedInputEntryIssue -} from "./validate/issues.js"; import { checkIsMemberOf } from "./validate/checks/isMemberOf.js"; import { checkIsNotMemberOf } from "./validate/checks/isNotMemberOf.js"; import { assertPODSpec } from "../generated/podspec.js"; +import { EntrySourcePodSpec } from "./validate/EntrySource.js"; +import { assert } from "vitest"; +import { checkInRange } from "./validate/checks/inRange.js"; +import { checkNotInRange } from "./validate/checks/notInRange.js"; +import { checkEqualsEntry } from "./validate/checks/equalsEntry.js"; +import { checkNotEqualsEntry } from "./validate/checks/notEqualsEntry.js"; /** @TOOO @@ -51,7 +52,7 @@ type PODEntriesFromEntryTypes = { [K in keyof E]: Extract; }; -interface ValidateOptions { +export interface ValidateOptions { /** * If true, the validation will exit as soon as the first error is encountered. */ @@ -128,7 +129,7 @@ export function validatePOD( // 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) { @@ -137,49 +138,16 @@ export function validatePOD( } } - const podEntries = pod.content.asEntries(); - const issues = []; const path: string[] = []; - for (const [key, entryType] of Object.entries(spec.entries)) { - if (!(key in podEntries)) { - const issue = { - code: IssueCode.missing_entry, - path: [...path, key], - key - } satisfies ValidationMissingEntryIssue; - if (options.strict) { - return FAILURE([issue]); - } else { - issues.push(issue); - } - } - if (podEntries[key]?.type !== entryType) { - const issue = { - code: IssueCode.type_mismatch, - path: [...path, key], - expectedType: entryType - } satisfies ValidationTypeMismatchIssue; - if (options.strict) { - return FAILURE([issue]); - } else { - issues.push(issue); - } - } - } + const entrySource = new EntrySourcePodSpec(spec, pod); - if (options.strict) { - for (const key in podEntries) { - if (!(key in spec.entries)) { - const issue = { - code: IssueCode.unexpected_input_entry, - path: [...path, key], - key - } satisfies ValidationUnexpectedInputEntryIssue; - return FAILURE([issue]); - } - } + 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)) { @@ -190,8 +158,7 @@ export function validatePOD( statement, key, path, - podEntries, - spec.entries, + entrySource, options.exitOnError ?? false ) ); @@ -202,12 +169,56 @@ export function validatePOD( statement, key, path, - podEntries, - spec.entries, + 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; + default: // prettier-ignore statement.type satisfies never; @@ -232,7 +243,10 @@ if (import.meta.vitest) { const signPOD = (entries: PODEntries) => POD.sign(entries, privKey); test("validatePOD", () => { - const myPOD = signPOD({ foo: { type: "string", value: "foo" } }); + const myPOD = signPOD({ + foo: { type: "string", value: "foo" }, + num: { type: "int", value: 50n } + }); const myPodSpecBuilder = PODSpecBuilder.create() .entry("foo", "string") .isMemberOf(["foo"], ["foo", "bar"]); @@ -267,5 +281,19 @@ if (import.meta.vitest) { secondBuilder.omitStatements(["foo_isMemberOf_1"]).spec() ).isValid ).toBe(true); + + { + const result = validatePOD( + myPOD, + secondBuilder + .omitStatements(["foo_isMemberOf_1"]) + .entry("num", "int") + .inRange("num", { min: 0n, max: 100n }) + .spec() + ); + assert(result.isValid); + const pod = result.value; + pod.content.asEntries().num satisfies PODIntValue; + } }); } diff --git a/packages/podspec/src/processors/validate/EntrySource.ts b/packages/podspec/src/processors/validate/EntrySource.ts new file mode 100644 index 0000000..4c2c9bf --- /dev/null +++ b/packages/podspec/src/processors/validate/EntrySource.ts @@ -0,0 +1,195 @@ +import type { POD, PODEntries, PODName, PODValue } 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 type { NamedPODSpecs, PODGroupSpec } from "../../builders/group.js"; +import { + type ValidationBaseIssue, + type ValidationMissingEntryIssue, + type ValidationMissingPodIssue, + type ValidationTypeMismatchIssue, + type ValidationUnexpectedInputEntryIssue, + type ValidationUnexpectedInputPodIssue, + IssueCode +} from "./issues.js"; +import type { ValidateOptions } from "../validate.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 (!(key in podEntries)) { + 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 in podEntries) { + if (!(key in spec.entries)) { + 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 { + return this.pod.content.getValue(entryName); + } + + public getEntryTypeFromSpec(entryName: string): PODValue["type"] | undefined { + 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 (!(podName in this.podGroupSpec.pods)) { + 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; + } + + 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; + } + return this.podGroupSpec.pods[podName].entries[entryName]; + } +} diff --git a/packages/podspec/src/processors/validate/checks/equalsEntry.ts b/packages/podspec/src/processors/validate/checks/equalsEntry.ts new file mode 100644 index 0000000..af9f1e4 --- /dev/null +++ b/packages/podspec/src/processors/validate/checks/equalsEntry.ts @@ -0,0 +1,95 @@ +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 entry1 = entrySource.getEntry(statement.entry); + const entry2 = entrySource.getEntry(statement.otherEntry); + + 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.entry], + path: [...path, statementName] + }); + return issues; + } + if (entry2 === undefined) { + issues.push({ + code: IssueCode.invalid_statement, + statementName: statementName, + statementType: statement.type, + entries: [statement.otherEntry], + path: [...path, statementName] + }); + return issues; + } + + const entry1Type = entrySource.getEntryTypeFromSpec(statement.entry); + const entry2Type = entrySource.getEntryTypeFromSpec(statement.otherEntry); + + if (entry1Type !== entry2Type) { + issues.push({ + code: IssueCode.invalid_statement, + statementName: statementName, + statementType: statement.type, + entries: [statement.entry, statement.otherEntry], + path: [...path, statementName] + }); + return issues; + } + + if (entry1Type !== entry1.type) { + issues.push({ + code: IssueCode.invalid_statement, + statementName: statementName, + statementType: statement.type, + entries: [statement.entry], + path: [...path, statementName] + }); + return issues; + } + + if (entry1Type !== entry2.type) { + issues.push({ + code: IssueCode.invalid_statement, + statementName: statementName, + statementType: statement.type, + entries: [statement.otherEntry], + 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.entry, statement.otherEntry], + path: [...path, statementName] + } satisfies ValidationStatementNegativeResultIssue; + return [issue]; + } + + return []; +} diff --git a/packages/podspec/src/processors/validate/checks/inRange.ts b/packages/podspec/src/processors/validate/checks/inRange.ts index 71dce7a..8d32f16 100644 --- a/packages/podspec/src/processors/validate/checks/inRange.ts +++ b/packages/podspec/src/processors/validate/checks/inRange.ts @@ -1,26 +1,38 @@ -import { isPODArithmeticValue, type PODEntries } from "@pcd/pod"; +import { isPODArithmeticValue } from "@pcd/pod"; import type { InRange } from "../../../builders/types/statements.js"; import { IssueCode, type ValidationBaseIssue, + type ValidationInvalidStatementIssue, type ValidationStatementNegativeResultIssue } from "../issues.js"; -import type { EntryTypes } from "../../../builders/types/entries.js"; +import type { EntrySource } from "../EntrySource.js"; export function checkInRange( // eslint-disable-next-line @typescript-eslint/no-explicit-any statement: InRange, statementName: string, path: string[], - entries: PODEntries, - _specEntries: EntryTypes, + entrySource: EntrySource, _exitOnError: boolean ): ValidationBaseIssue[] { const entryName = statement.entry; - const entry = entries[entryName]!; + 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 diff --git a/packages/podspec/src/processors/validate/checks/isMemberOf.ts b/packages/podspec/src/processors/validate/checks/isMemberOf.ts index 6c3f26d..e13a1c0 100644 --- a/packages/podspec/src/processors/validate/checks/isMemberOf.ts +++ b/packages/podspec/src/processors/validate/checks/isMemberOf.ts @@ -1,4 +1,3 @@ -import type { PODEntries } from "@pcd/pod"; import type { ValidationBaseIssue, ValidationInvalidStatementIssue, @@ -6,17 +5,17 @@ import type { } from "../issues.js"; import { IssueCode } from "../issues.js"; import type { IsMemberOf } from "../../../builders/types/statements.js"; -import type { EntryTypes } from "../../../builders/types/entries.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[], - specEntries: EntryTypes + entrySource: EntrySource ): ValidationInvalidStatementIssue[] { - if (statement.entries.some((entry) => !(entry in specEntries))) { + if (statement.entries.some((entry) => !entrySource.getEntry(entry))) { return [ { code: IssueCode.invalid_statement, @@ -35,8 +34,7 @@ export function checkIsMemberOf( statement: IsMemberOf, statementName: string, path: string[], - podEntries: PODEntries, - specEntries: EntryTypes, + entrySource: EntrySource, exitOnError: boolean ): ValidationBaseIssue[] { // TODO Move this to a pre-processing step @@ -44,19 +42,23 @@ export function checkIsMemberOf( statement, statementName, path, - specEntries + entrySource ); if (issues.length > 0) { // Can't proceed if there are issues with the statement return issues; } - const tuple = statement.entries.map((entry) => podEntries[entry]?.value); + 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 + statement.entries.map( + (entry) => entrySource.getEntryTypeFromSpec(entry) as string + ) ); let match = false; diff --git a/packages/podspec/src/processors/validate/checks/isNotMemberOf.ts b/packages/podspec/src/processors/validate/checks/isNotMemberOf.ts index fea5c42..688c1a4 100644 --- a/packages/podspec/src/processors/validate/checks/isNotMemberOf.ts +++ b/packages/podspec/src/processors/validate/checks/isNotMemberOf.ts @@ -1,4 +1,3 @@ -import type { PODEntries } from "@pcd/pod"; import { IssueCode, type ValidationBaseIssue, @@ -6,17 +5,17 @@ import { type ValidationStatementNegativeResultIssue } from "../issues.js"; import type { IsNotMemberOf } from "../../../builders/types/statements.js"; -import type { EntryTypes } from "../../../builders/types/entries.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[], - specEntries: EntryTypes + entrySource: EntrySource ): ValidationInvalidStatementIssue[] { - if (statement.entries.some((entry) => !(entry in specEntries))) { + if (statement.entries.some((entry) => !entrySource.getEntry(entry))) { return [ { code: IssueCode.invalid_statement, @@ -35,8 +34,7 @@ export function checkIsNotMemberOf( statement: IsNotMemberOf, statementName: string, path: string[], - entries: PODEntries, - specEntries: EntryTypes, + entrySource: EntrySource, exitOnError: boolean ): ValidationBaseIssue[] { // TODO Move this to a pre-processing step @@ -44,7 +42,7 @@ export function checkIsNotMemberOf( statement, statementName, path, - specEntries + entrySource ); // Can't proceed if there are any issues with the statement @@ -52,11 +50,15 @@ export function checkIsNotMemberOf( return issues; } - const tuple = statement.entries.map((entry) => entries[entry]?.value); + 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 + statement.entries.map( + (entry) => entrySource.getEntryTypeFromSpec(entry) as string + ) ); let match = false; diff --git a/packages/podspec/src/processors/validate/checks/notEqualsEntry.ts b/packages/podspec/src/processors/validate/checks/notEqualsEntry.ts new file mode 100644 index 0000000..e059353 --- /dev/null +++ b/packages/podspec/src/processors/validate/checks/notEqualsEntry.ts @@ -0,0 +1,95 @@ +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 entry1 = entrySource.getEntry(statement.entry); + const entry2 = entrySource.getEntry(statement.otherEntry); + + 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.entry], + path: [...path, statementName] + }); + return issues; + } + if (entry2 === undefined) { + issues.push({ + code: IssueCode.invalid_statement, + statementName: statementName, + statementType: statement.type, + entries: [statement.otherEntry], + path: [...path, statementName] + }); + return issues; + } + + const entry1Type = entrySource.getEntryTypeFromSpec(statement.entry); + const entry2Type = entrySource.getEntryTypeFromSpec(statement.otherEntry); + + if (entry1Type !== entry2Type) { + issues.push({ + code: IssueCode.invalid_statement, + statementName: statementName, + statementType: statement.type, + entries: [statement.entry, statement.otherEntry], + path: [...path, statementName] + }); + return issues; + } + + if (entry1Type !== entry1.type) { + issues.push({ + code: IssueCode.invalid_statement, + statementName: statementName, + statementType: statement.type, + entries: [statement.entry], + path: [...path, statementName] + }); + return issues; + } + + if (entry1Type !== entry2.type) { + issues.push({ + code: IssueCode.invalid_statement, + statementName: statementName, + statementType: statement.type, + entries: [statement.otherEntry], + 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.entry, statement.otherEntry], + path: [...path, statementName] + } satisfies ValidationStatementNegativeResultIssue; + return [issue]; + } + + return []; +} diff --git a/packages/podspec/src/processors/validate/checks/notInRange.ts b/packages/podspec/src/processors/validate/checks/notInRange.ts index 864771b..aa7683b 100644 --- a/packages/podspec/src/processors/validate/checks/notInRange.ts +++ b/packages/podspec/src/processors/validate/checks/notInRange.ts @@ -1,26 +1,38 @@ -import { isPODArithmeticValue, type PODEntries } from "@pcd/pod"; +import { isPODArithmeticValue } from "@pcd/pod"; import type { NotInRange } from "../../../builders/types/statements.js"; import { IssueCode, type ValidationBaseIssue, + type ValidationInvalidStatementIssue, type ValidationStatementNegativeResultIssue } from "../issues.js"; -import type { EntryTypes } from "../../../builders/types/entries.js"; +import type { EntrySource } from "../EntrySource.js"; export function checkNotInRange( // eslint-disable-next-line @typescript-eslint/no-explicit-any statement: NotInRange, statementName: string, path: string[], - entries: PODEntries, - _specEntries: EntryTypes, + entrySource: EntrySource, _exitOnError: boolean ): ValidationBaseIssue[] { const entryName = statement.entry; - const entry = entries[entryName]!; + 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 diff --git a/packages/podspec/src/processors/validate/issues.ts b/packages/podspec/src/processors/validate/issues.ts index bbffce1..f162c09 100644 --- a/packages/podspec/src/processors/validate/issues.ts +++ b/packages/podspec/src/processors/validate/issues.ts @@ -4,10 +4,12 @@ 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; @@ -43,6 +45,14 @@ export interface ValidationMissingEntryIssue extends ValidationBaseIssue { 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. */ @@ -81,6 +91,15 @@ export interface ValidationUnexpectedInputEntryIssue 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. */ From ce119466f7c0b8efed2141b3b5900793ec16f213 Mon Sep 17 00:00:00 2001 From: Rob Knight Date: Sat, 1 Feb 2025 23:05:37 +0100 Subject: [PATCH 10/20] Added checks for all statements --- packages/podspec/package.json | 2 +- packages/podspec/src/processors/validate.ts | 66 ++++++++++-- .../{equalsEntry.ts => checkEqualsEntry.ts} | 0 .../validate/checks/checkGreaterThan.ts | 101 ++++++++++++++++++ .../validate/checks/checkGreaterThanEq.ts | 101 ++++++++++++++++++ .../checks/{inRange.ts => checkInRange.ts} | 28 +++-- .../{isMemberOf.ts => checkIsMemberOf.ts} | 0 ...isNotMemberOf.ts => checkIsNotMemberOf.ts} | 0 .../validate/checks/checkLessThan.ts | 101 ++++++++++++++++++ .../validate/checks/checkLessThanEq.ts | 101 ++++++++++++++++++ ...tEqualsEntry.ts => checkNotEqualsEntry.ts} | 0 .../{notInRange.ts => checkNotInRange.ts} | 0 12 files changed, 478 insertions(+), 22 deletions(-) rename packages/podspec/src/processors/validate/checks/{equalsEntry.ts => checkEqualsEntry.ts} (100%) create mode 100644 packages/podspec/src/processors/validate/checks/checkGreaterThan.ts create mode 100644 packages/podspec/src/processors/validate/checks/checkGreaterThanEq.ts rename packages/podspec/src/processors/validate/checks/{inRange.ts => checkInRange.ts} (73%) rename packages/podspec/src/processors/validate/checks/{isMemberOf.ts => checkIsMemberOf.ts} (100%) rename packages/podspec/src/processors/validate/checks/{isNotMemberOf.ts => checkIsNotMemberOf.ts} (100%) create mode 100644 packages/podspec/src/processors/validate/checks/checkLessThan.ts create mode 100644 packages/podspec/src/processors/validate/checks/checkLessThanEq.ts rename packages/podspec/src/processors/validate/checks/{notEqualsEntry.ts => checkNotEqualsEntry.ts} (100%) rename packages/podspec/src/processors/validate/checks/{notInRange.ts => checkNotInRange.ts} (100%) diff --git a/packages/podspec/package.json b/packages/podspec/package.json index 092bcf8..eed54f0 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", diff --git a/packages/podspec/src/processors/validate.ts b/packages/podspec/src/processors/validate.ts index 83ed89c..ae23f05 100644 --- a/packages/podspec/src/processors/validate.ts +++ b/packages/podspec/src/processors/validate.ts @@ -12,15 +12,19 @@ import type { EntryTypes } from "../builders/types/entries.js"; import type { StatementMap } from "../builders/types/statements.js"; import type { ValidateResult } from "./validate/types.js"; import { FAILURE, SUCCESS } from "./validate/result.js"; -import { checkIsMemberOf } from "./validate/checks/isMemberOf.js"; -import { checkIsNotMemberOf } from "./validate/checks/isNotMemberOf.js"; +import { checkIsMemberOf } from "./validate/checks/checkIsMemberOf.js"; +import { checkIsNotMemberOf } from "./validate/checks/checkIsNotMemberOf.js"; import { assertPODSpec } from "../generated/podspec.js"; import { EntrySourcePodSpec } from "./validate/EntrySource.js"; import { assert } from "vitest"; -import { checkInRange } from "./validate/checks/inRange.js"; -import { checkNotInRange } from "./validate/checks/notInRange.js"; -import { checkEqualsEntry } from "./validate/checks/equalsEntry.js"; -import { checkNotEqualsEntry } from "./validate/checks/notEqualsEntry.js"; +import { checkInRange } from "./validate/checks/checkInRange.js"; +import { checkNotInRange } from "./validate/checks/checkNotInRange.js"; +import { checkEqualsEntry } from "./validate/checks/checkEqualsEntry.js"; +import { checkNotEqualsEntry } from "./validate/checks/checkNotEqualsEntry.js"; +import { checkGreaterThan } from "./validate/checks/checkGreaterThan.js"; +import { checkGreaterThanEq } from "./validate/checks/checkGreaterThanEq.js"; +import { checkLessThan } from "./validate/checks/checkLessThan.js"; +import { checkLessThanEq } from "./validate/checks/checkLessThanEq.js"; /** @TOOO @@ -79,7 +83,7 @@ interface PODValidator { strictAssert(pod: POD): asserts pod is StrongPOD>; } -function validate( +export function validate( spec: PODSpec ): PODValidator { // @TODO maybe typia's clone is better @@ -218,11 +222,53 @@ export function validatePOD( ) ); 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.type satisfies never; - // maybe throw an exception here + statement satisfies never; } if (options.exitOnError && issues.length > 0) { return FAILURE(issues); diff --git a/packages/podspec/src/processors/validate/checks/equalsEntry.ts b/packages/podspec/src/processors/validate/checks/checkEqualsEntry.ts similarity index 100% rename from packages/podspec/src/processors/validate/checks/equalsEntry.ts rename to packages/podspec/src/processors/validate/checks/checkEqualsEntry.ts 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..383f4c3 --- /dev/null +++ b/packages/podspec/src/processors/validate/checks/checkGreaterThan.ts @@ -0,0 +1,101 @@ +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 entry1 = entrySource.getEntry(statement.entry); + const entry2 = entrySource.getEntry(statement.otherEntry); + + 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.entry], + path: [...path, statementName] + }); + return issues; + } + if (entry2 === undefined) { + issues.push({ + code: IssueCode.invalid_statement, + statementName: statementName, + statementType: statement.type, + entries: [statement.otherEntry], + path: [...path, statementName] + }); + return issues; + } + + const entry1Type = entrySource.getEntryTypeFromSpec(statement.entry); + const entry2Type = entrySource.getEntryTypeFromSpec(statement.otherEntry); + + // TODO this may be too restrictive + if (entry1Type !== entry2Type) { + issues.push({ + code: IssueCode.invalid_statement, + statementName: statementName, + statementType: statement.type, + entries: [statement.entry, statement.otherEntry], + path: [...path, statementName] + }); + return issues; + } + + if (entry1Type !== entry1.type) { + issues.push({ + code: IssueCode.invalid_statement, + statementName: statementName, + statementType: statement.type, + entries: [statement.entry], + path: [...path, statementName] + }); + return issues; + } + + if (entry1Type !== entry2.type) { + issues.push({ + code: IssueCode.invalid_statement, + statementName: statementName, + statementType: statement.type, + entries: [statement.otherEntry], + path: [...path, statementName] + }); + return issues; + } + + if (!isPODArithmeticValue(entry1) || !isPODArithmeticValue(entry2)) { + issues.push({ + code: IssueCode.invalid_statement, + statementName: statementName, + statementType: statement.type, + entries: [statement.entry, statement.otherEntry], + 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.entry, statement.otherEntry], + 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..53c1547 --- /dev/null +++ b/packages/podspec/src/processors/validate/checks/checkGreaterThanEq.ts @@ -0,0 +1,101 @@ +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 entry1 = entrySource.getEntry(statement.entry); + const entry2 = entrySource.getEntry(statement.otherEntry); + + 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.entry], + path: [...path, statementName] + }); + return issues; + } + if (entry2 === undefined) { + issues.push({ + code: IssueCode.invalid_statement, + statementName: statementName, + statementType: statement.type, + entries: [statement.otherEntry], + path: [...path, statementName] + }); + return issues; + } + + const entry1Type = entrySource.getEntryTypeFromSpec(statement.entry); + const entry2Type = entrySource.getEntryTypeFromSpec(statement.otherEntry); + + // TODO this may be too restrictive + if (entry1Type !== entry2Type) { + issues.push({ + code: IssueCode.invalid_statement, + statementName: statementName, + statementType: statement.type, + entries: [statement.entry, statement.otherEntry], + path: [...path, statementName] + }); + return issues; + } + + if (entry1Type !== entry1.type) { + issues.push({ + code: IssueCode.invalid_statement, + statementName: statementName, + statementType: statement.type, + entries: [statement.entry], + path: [...path, statementName] + }); + return issues; + } + + if (entry1Type !== entry2.type) { + issues.push({ + code: IssueCode.invalid_statement, + statementName: statementName, + statementType: statement.type, + entries: [statement.otherEntry], + path: [...path, statementName] + }); + return issues; + } + + if (!isPODArithmeticValue(entry1) || !isPODArithmeticValue(entry2)) { + issues.push({ + code: IssueCode.invalid_statement, + statementName: statementName, + statementType: statement.type, + entries: [statement.entry, statement.otherEntry], + 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.entry, statement.otherEntry], + path: [...path, statementName] + }); + } + return issues; +} diff --git a/packages/podspec/src/processors/validate/checks/inRange.ts b/packages/podspec/src/processors/validate/checks/checkInRange.ts similarity index 73% rename from packages/podspec/src/processors/validate/checks/inRange.ts rename to packages/podspec/src/processors/validate/checks/checkInRange.ts index 8d32f16..525426a 100644 --- a/packages/podspec/src/processors/validate/checks/inRange.ts +++ b/packages/podspec/src/processors/validate/checks/checkInRange.ts @@ -18,19 +18,18 @@ export function checkInRange( ): ValidationBaseIssue[] { const entryName = statement.entry; 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) { - const issues = [ - { - code: IssueCode.invalid_statement, - statementName: statementName, - statementType: statement.type, - entries: [entryName], - path: [...path, statementName] - } satisfies ValidationInvalidStatementIssue - ]; + issues.push({ + code: IssueCode.invalid_statement, + statementName: statementName, + statementType: statement.type, + entries: [entryName], + path: [...path, statementName] + } satisfies ValidationInvalidStatementIssue); return issues; } @@ -55,7 +54,14 @@ export function checkInRange( } satisfies ValidationStatementNegativeResultIssue as ValidationBaseIssue ]; } + } else { + issues.push({ + code: IssueCode.invalid_statement, + statementName: statementName, + statementType: statement.type, + entries: [entryName], + path: [...path, statementName] + } satisfies ValidationInvalidStatementIssue); } - - return []; + return issues; } diff --git a/packages/podspec/src/processors/validate/checks/isMemberOf.ts b/packages/podspec/src/processors/validate/checks/checkIsMemberOf.ts similarity index 100% rename from packages/podspec/src/processors/validate/checks/isMemberOf.ts rename to packages/podspec/src/processors/validate/checks/checkIsMemberOf.ts diff --git a/packages/podspec/src/processors/validate/checks/isNotMemberOf.ts b/packages/podspec/src/processors/validate/checks/checkIsNotMemberOf.ts similarity index 100% rename from packages/podspec/src/processors/validate/checks/isNotMemberOf.ts rename to packages/podspec/src/processors/validate/checks/checkIsNotMemberOf.ts 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..61b7f03 --- /dev/null +++ b/packages/podspec/src/processors/validate/checks/checkLessThan.ts @@ -0,0 +1,101 @@ +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 entry1 = entrySource.getEntry(statement.entry); + const entry2 = entrySource.getEntry(statement.otherEntry); + + 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.entry], + path: [...path, statementName] + }); + return issues; + } + if (entry2 === undefined) { + issues.push({ + code: IssueCode.invalid_statement, + statementName: statementName, + statementType: statement.type, + entries: [statement.otherEntry], + path: [...path, statementName] + }); + return issues; + } + + const entry1Type = entrySource.getEntryTypeFromSpec(statement.entry); + const entry2Type = entrySource.getEntryTypeFromSpec(statement.otherEntry); + + // TODO this may be too restrictive + if (entry1Type !== entry2Type) { + issues.push({ + code: IssueCode.invalid_statement, + statementName: statementName, + statementType: statement.type, + entries: [statement.entry, statement.otherEntry], + path: [...path, statementName] + }); + return issues; + } + + if (entry1Type !== entry1.type) { + issues.push({ + code: IssueCode.invalid_statement, + statementName: statementName, + statementType: statement.type, + entries: [statement.entry], + path: [...path, statementName] + }); + return issues; + } + + if (entry1Type !== entry2.type) { + issues.push({ + code: IssueCode.invalid_statement, + statementName: statementName, + statementType: statement.type, + entries: [statement.otherEntry], + path: [...path, statementName] + }); + return issues; + } + + if (!isPODArithmeticValue(entry1) || !isPODArithmeticValue(entry2)) { + issues.push({ + code: IssueCode.invalid_statement, + statementName: statementName, + statementType: statement.type, + entries: [statement.entry, statement.otherEntry], + 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.entry, statement.otherEntry], + 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..21fe549 --- /dev/null +++ b/packages/podspec/src/processors/validate/checks/checkLessThanEq.ts @@ -0,0 +1,101 @@ +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 entry1 = entrySource.getEntry(statement.entry); + const entry2 = entrySource.getEntry(statement.otherEntry); + + 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.entry], + path: [...path, statementName] + }); + return issues; + } + if (entry2 === undefined) { + issues.push({ + code: IssueCode.invalid_statement, + statementName: statementName, + statementType: statement.type, + entries: [statement.otherEntry], + path: [...path, statementName] + }); + return issues; + } + + const entry1Type = entrySource.getEntryTypeFromSpec(statement.entry); + const entry2Type = entrySource.getEntryTypeFromSpec(statement.otherEntry); + + // TODO this may be too restrictive + if (entry1Type !== entry2Type) { + issues.push({ + code: IssueCode.invalid_statement, + statementName: statementName, + statementType: statement.type, + entries: [statement.entry, statement.otherEntry], + path: [...path, statementName] + }); + return issues; + } + + if (entry1Type !== entry1.type) { + issues.push({ + code: IssueCode.invalid_statement, + statementName: statementName, + statementType: statement.type, + entries: [statement.entry], + path: [...path, statementName] + }); + return issues; + } + + if (entry1Type !== entry2.type) { + issues.push({ + code: IssueCode.invalid_statement, + statementName: statementName, + statementType: statement.type, + entries: [statement.otherEntry], + path: [...path, statementName] + }); + return issues; + } + + if (!isPODArithmeticValue(entry1) || !isPODArithmeticValue(entry2)) { + issues.push({ + code: IssueCode.invalid_statement, + statementName: statementName, + statementType: statement.type, + entries: [statement.entry, statement.otherEntry], + 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.entry, statement.otherEntry], + path: [...path, statementName] + }); + } + return issues; +} diff --git a/packages/podspec/src/processors/validate/checks/notEqualsEntry.ts b/packages/podspec/src/processors/validate/checks/checkNotEqualsEntry.ts similarity index 100% rename from packages/podspec/src/processors/validate/checks/notEqualsEntry.ts rename to packages/podspec/src/processors/validate/checks/checkNotEqualsEntry.ts diff --git a/packages/podspec/src/processors/validate/checks/notInRange.ts b/packages/podspec/src/processors/validate/checks/checkNotInRange.ts similarity index 100% rename from packages/podspec/src/processors/validate/checks/notInRange.ts rename to packages/podspec/src/processors/validate/checks/checkNotInRange.ts From f7748305fc78d46fef6ae0145fa78dd0b4b66613 Mon Sep 17 00:00:00 2001 From: Rob Knight Date: Sat, 1 Feb 2025 23:34:23 +0100 Subject: [PATCH 11/20] Begin reorganizing tests --- packages/podspec/package.json | 1 + packages/podspec/src/builders/group.ts | 124 +-- packages/podspec/src/builders/pod.ts | 143 --- packages/podspec/src/processors/validate.ts | 77 +- packages/podspec/src/type_inference.ts | 70 -- .../test/builders/PODGroupSpecBuilder.spec.ts | 82 ++ .../test/builders/PODSpecBuilder.spec.ts | 114 +++ packages/podspec/test/podspec.spec.ts | 896 ------------------ .../processors/validator/validator.spec.ts | 75 ++ packages/podspec/test/scratch.ts | 0 packages/podspec/test/serialization.spec.ts | 82 -- packages/podspec/test/ticket-example.spec.ts | 326 ------- pnpm-lock.yaml | 26 + 13 files changed, 302 insertions(+), 1714 deletions(-) delete mode 100644 packages/podspec/src/type_inference.ts create mode 100644 packages/podspec/test/builders/PODGroupSpecBuilder.spec.ts create mode 100644 packages/podspec/test/builders/PODSpecBuilder.spec.ts delete mode 100644 packages/podspec/test/podspec.spec.ts create mode 100644 packages/podspec/test/processors/validator/validator.spec.ts delete mode 100644 packages/podspec/test/scratch.ts delete mode 100644 packages/podspec/test/serialization.spec.ts delete mode 100644 packages/podspec/test/ticket-example.spec.ts diff --git a/packages/podspec/package.json b/packages/podspec/package.json index eed54f0..bcb3bcd 100644 --- a/packages/podspec/package.json +++ b/packages/podspec/package.json @@ -43,6 +43,7 @@ "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", diff --git a/packages/podspec/src/builders/group.ts b/packages/podspec/src/builders/group.ts index 433cba0..7813450 100644 --- a/packages/podspec/src/builders/group.ts +++ b/packages/podspec/src/builders/group.ts @@ -6,7 +6,6 @@ import { POD_INT_MIN, type PODName } from "@pcd/pod"; -import { type PODSpec, PODSpecBuilder } from "./pod.js"; import type { EntryTypes, VirtualEntries, @@ -35,6 +34,7 @@ import { supportsRangeChecks, validateRange } from "./shared.js"; +import type { PODSpec } from "./pod.js"; export type NamedPODSpecs = Record>; @@ -53,7 +53,7 @@ export type PODGroupSpec

= { statements: S; }; -type AllPODEntries

= { +export type AllPODEntries

= { [K in keyof P]: { [E in keyof (P[K]["entries"] & VirtualEntries) as `${K & string}.${E & string}`]: (P[K]["entries"] & VirtualEntries)[E]; @@ -731,123 +731,3 @@ export class PODGroupSpecBuilder< }); } } - -if (import.meta.vitest) { - const { it, expect, assertType } = import.meta.vitest; - - 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": "string", - "foo.$signature": "string" - }); - - 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": "string", - "foo.$signature": "string", - "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 - }); - - it("debug AllPODEntries types", () => { - const group = PODGroupSpecBuilder.create(); - const podBuilder = PODSpecBuilder.create() - .entry("my_string", "string") - .entry("my_other_string", "string") - .entry("my_num", "int"); - - const _groupWithPod = group.pod("foo", podBuilder.spec()); - - type TestPods = ReturnType["pods"]; - - // Verify type equivalence - type TestPodEntries = PodTest; - - // Check that entries are exactly the types we expect - type Test1 = TestPodEntries["foo.my_string"] extends "string" - ? true - : false; // should be true - type Test2 = "string" extends TestPodEntries["foo.my_string"] - ? true - : false; // should be true - type Test3 = TestPodEntries["foo.my_num"] extends "int" ? true : false; // should be true - type Test4 = "int" extends TestPodEntries["foo.my_num"] ? true : false; // should be true - - // Verify that the types are exactly equal - type Test5 = TestPodEntries["foo.my_string"] extends "string" - ? true - : false; // should be true - - assertType(true); - assertType(true); - assertType(true); - assertType(true); - assertType(true); - }); -} - -type PodTest

= { - [K in keyof P]: { - [E in keyof (P[K]["entries"] & VirtualEntries) as `${K & string}.${E & - string}`]: (P[K]["entries"] & VirtualEntries)[E]; - }; -}[keyof P]; diff --git a/packages/podspec/src/builders/pod.ts b/packages/podspec/src/builders/pod.ts index dba9ca3..219f330 100644 --- a/packages/podspec/src/builders/pod.ts +++ b/packages/podspec/src/builders/pod.ts @@ -77,14 +77,6 @@ export type PODSpec = { // This is a compile-time check that the PODSpec is JSON-safe true satisfies IsJsonSafe>; -/** - * Given a list of entry names, return the names of the entries that are not in the list - */ -type OmittedEntryNames = Exclude< - keyof E, - N[number] ->; - type NonOverlappingStatements = { [K in keyof S as S[K] extends // eslint-disable-next-line @typescript-eslint/no-explicit-any | IsMemberOf @@ -913,138 +905,3 @@ export class PODSpecBuilder< }); } } - -if (import.meta.vitest) { - const { it, expect } = import.meta.vitest; - it("PODSpecBuilder", () => { - const a = PODSpecBuilder.create(); - const b = a.entry("a", "string").entry("b", "int"); - expect(b.spec().entries).toEqual({ - a: "string", - b: "int" - }); - - 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: { - entry: "b", - type: "inRange", - inRange: { min: 10n, max: 100n } - } - }); - - const e = d.isMemberOf(["a", "b"], [["foo", 10n]]); - expect(e.spec().statements.a_b_isMemberOf.entries).toEqual(["a", "b"]); - - const f = e.pick(["b"]); - expect(f.spec().statements).toEqual({ - b_inRange: { - entry: "b", - type: "inRange", - inRange: { min: 10n, max: 100n } - } - }); - - 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: { - entry: "a", - type: "equalsEntry", - equalsEntry: "new" - } - }); - - expect(g.spec()).toEqual({ - entries: { - a: "string", - b: "int", - new: "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: { - entry: "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: { - entry: "a", - type: "equalsEntry", - otherEntry: "new" - } - } - } satisfies typeof _GSpec); - - const h = g.pickStatements(["a_isMemberOf"]); - expect(h.spec().statements).toEqual({ - a_isMemberOf: { - entries: ["a"], - type: "isMemberOf", - isMemberOf: [["foo"]] - } - }); - }); -} - -// Example entry list spec -type TestEntries = { - a: "string"; - b: "int"; - c: "int"; -}; - -// Example statement map -type TestStatements = { - a_isMemberOf: IsMemberOf; - b_inRange: InRange; - ac_isMemberOf: IsMemberOf; -}; - -// Let's test picking just 'a' and 'b' -type PickedKeys = ["b"]; - -// First, let's see what OmittedEntryNames gives us -type _TestOmitted = OmittedEntryNames; -// Should be: "c" - -// Now let's test NonOverlapping -type _TestNonOverlapping = NonOverlappingStatements; - -// Let's see what we get when picking just 'a' -type _TestPickA = NonOverlappingStatements; diff --git a/packages/podspec/src/processors/validate.ts b/packages/podspec/src/processors/validate.ts index ae23f05..1b1e492 100644 --- a/packages/podspec/src/processors/validate.ts +++ b/packages/podspec/src/processors/validate.ts @@ -1,13 +1,5 @@ -import { - POD, - type PODEntries, - type PODValue, - type PODContent, - type PODStringValue, - type PODName, - type PODIntValue -} from "@pcd/pod"; -import { PODSpecBuilder, type PODSpec } from "../builders/pod.js"; +import type { POD, PODEntries, PODValue, PODContent, PODName } 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 type { ValidateResult } from "./validate/types.js"; @@ -16,7 +8,6 @@ import { checkIsMemberOf } from "./validate/checks/checkIsMemberOf.js"; import { checkIsNotMemberOf } from "./validate/checks/checkIsNotMemberOf.js"; import { assertPODSpec } from "../generated/podspec.js"; import { EntrySourcePodSpec } from "./validate/EntrySource.js"; -import { assert } from "vitest"; import { checkInRange } from "./validate/checks/checkInRange.js"; import { checkNotInRange } from "./validate/checks/checkNotInRange.js"; import { checkEqualsEntry } from "./validate/checks/checkEqualsEntry.js"; @@ -279,67 +270,3 @@ export function validatePOD( ? FAILURE(issues) : SUCCESS(pod as StrongPOD>); } - -if (import.meta.vitest) { - const { test, expect } = import.meta.vitest; - - const privKey = - "f72c3def0a54280ded2990a66fabcf717130c6f2bb595004658ec77774b98924"; - - const signPOD = (entries: PODEntries) => POD.sign(entries, privKey); - - test("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(validatePOD(myPOD, myPodSpecBuilder.spec()).isValid).toBe(true); - - const result = validatePOD(myPOD, myPodSpecBuilder.spec()); - 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(validatePOD(myPOD, secondBuilder.spec()).isValid).toBe(false); - - // If we omit the new statement, it should pass - expect( - validatePOD( - myPOD, - secondBuilder.omitStatements(["foo_isMemberOf_1"]).spec() - ).isValid - ).toBe(true); - - { - const result = validatePOD( - myPOD, - secondBuilder - .omitStatements(["foo_isMemberOf_1"]) - .entry("num", "int") - .inRange("num", { min: 0n, max: 100n }) - .spec() - ); - assert(result.isValid); - const pod = result.value; - pod.content.asEntries().num satisfies PODIntValue; - } - }); -} 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/test/builders/PODGroupSpecBuilder.spec.ts b/packages/podspec/test/builders/PODGroupSpecBuilder.spec.ts new file mode 100644 index 0000000..8d40c44 --- /dev/null +++ b/packages/podspec/test/builders/PODGroupSpecBuilder.spec.ts @@ -0,0 +1,82 @@ +import { describe, it, expect, assertType } from "vitest"; +import { PODGroupSpecBuilder, PODSpecBuilder } from "../../src/index.js"; +import type { AllPODEntries } from "../../src/builders/group.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": "string", + "foo.$signature": "string" + }); + + 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": "string", + "foo.$signature": "string", + "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..db7afa2 --- /dev/null +++ b/packages/podspec/test/builders/PODSpecBuilder.spec.ts @@ -0,0 +1,114 @@ +import { describe, it, expect } from "vitest"; +import type { EqualsEntry } from "../../src/builders/types/statements.js"; +import { PODSpecBuilder } from "../../src/index.js"; + +describe("PODSpecBuilder", () => { + it("should be a test", () => { + expect(true).toBe(true); + }); + + it("PODSpecBuilder", () => { + const a = PODSpecBuilder.create(); + const b = a.entry("a", "string").entry("b", "int"); + expect(b.spec().entries).toEqual({ + a: "string", + b: "int" + }); + + 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: { + entry: "b", + type: "inRange", + inRange: { min: 10n, max: 100n } + } + }); + + const e = d.isMemberOf(["a", "b"], [["foo", 10n]]); + expect(e.spec().statements.a_b_isMemberOf.entries).toEqual(["a", "b"]); + + const f = e.pick(["b"]); + expect(f.spec().statements).toEqual({ + b_inRange: { + entry: "b", + type: "inRange", + inRange: { min: 10n, max: 100n } + } + }); + + 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: { + entry: "a", + type: "equalsEntry", + equalsEntry: "new" + } + }); + + expect(g.spec()).toEqual({ + entries: { + a: "string", + b: "int", + new: "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: { + entry: "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: { + entry: "a", + type: "equalsEntry", + otherEntry: "new" + } + } + } 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/podspec.spec.ts b/packages/podspec/test/podspec.spec.ts deleted file mode 100644 index f70d113..0000000 --- a/packages/podspec/test/podspec.spec.ts +++ /dev/null @@ -1,896 +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"; -import { merge } from "../src/index.js"; -import { PodSpecGroup } from "../src/group.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); - }); - - it("can merge pod specs", async function () { - const p1 = p.pod({ - entries: { - foo: { type: "string" }, - bar: { type: "int" } - } - }); - - const p2 = p.pod({ - entries: { - baz: { type: "string" }, - quux: { type: "int" } - } - }); - - const p3 = merge(p1, p2); - - assert(p3.schema.entries.foo === p1.schema.entries.foo); - assert(p3.schema.entries.bar === p1.schema.entries.bar); - assert(p3.schema.entries.baz === p2.schema.entries.baz); - assert(p3.schema.entries.quux === p2.schema.entries.quux); - - const result = p3.safeParseEntries( - { - foo: "test", - bar: 5n, - baz: "test", - quux: 10n - }, - { - coerce: true - } - ); - - assert(result.isValid); - assert(result.value.foo.type === "string"); - assert(result.value.foo.value === "test"); - assert(result.value.bar.type === "int"); - assert(result.value.bar.value === 5n); - assert(result.value.baz.type === "string"); - assert(result.value.baz.value === "test"); - assert(result.value.quux.type === "int"); - assert(result.value.quux.value === 10n); - }); - - it("group stuff", async function () { - const p1 = p.pod({ - entries: { foo: { type: "string" } } - }); - const p2 = p.pod({ - entries: { bar: { type: "string" } } - }); - const p3 = p.pod({ - entries: { baz: { type: "string" } } - }); - const group = new PodSpecGroup({ - pods: { p1, p2, p3 }, - tuples: [ - { - entries: ["p1.foo", "p2.bar"], - isMemberOf: [ - [ - { type: "string", value: "test" }, - { type: "string", value: "test" }, - { type: "string", value: "test" } - ] - ] - } - ] - }); - - const p1got = group.get("p1"); - assert(p1got !== undefined); - assert(p1got.schema.entries.foo !== undefined); - assert(p1got.schema.entries.foo.type === "string"); - }); -}); diff --git a/packages/podspec/test/processors/validator/validator.spec.ts b/packages/podspec/test/processors/validator/validator.spec.ts new file mode 100644 index 0000000..07b0cc4 --- /dev/null +++ b/packages/podspec/test/processors/validator/validator.spec.ts @@ -0,0 +1,75 @@ +import { + type PODEntries, + POD, + type PODValue, + type PODStringValue, + type PODIntValue +} from "@pcd/pod"; +import { describe, it, expect, assert } from "vitest"; +import { PODSpecBuilder, validatePOD } from "../../../src/index.js"; + +describe("validator", () => { + it("should be a test", () => { + expect(true).toBe(true); + }); + + const privKey = + "f72c3def0a54280ded2990a66fabcf717130c6f2bb595004658ec77774b98924"; + + const signPOD = (entries: PODEntries) => POD.sign(entries, privKey); + + 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(validatePOD(myPOD, myPodSpecBuilder.spec()).isValid).toBe(true); + + const result = validatePOD(myPOD, myPodSpecBuilder.spec()); + 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(validatePOD(myPOD, secondBuilder.spec()).isValid).toBe(false); + + // If we omit the new statement, it should pass + expect( + validatePOD( + myPOD, + secondBuilder.omitStatements(["foo_isMemberOf_1"]).spec() + ).isValid + ).toBe(true); + + { + const result = validatePOD( + myPOD, + secondBuilder + .omitStatements(["foo_isMemberOf_1"]) + .entry("num", "int") + .inRange("num", { min: 0n, max: 100n }) + .spec() + ); + assert(result.isValid); + const pod = result.value; + pod.content.asEntries().num satisfies PODIntValue; + } + }); +}); diff --git a/packages/podspec/test/scratch.ts b/packages/podspec/test/scratch.ts deleted file mode 100644 index e69de29..0000000 diff --git a/packages/podspec/test/serialization.spec.ts b/packages/podspec/test/serialization.spec.ts deleted file mode 100644 index a05bd34..0000000 --- a/packages/podspec/test/serialization.spec.ts +++ /dev/null @@ -1,82 +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(); - - 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/pnpm-lock.yaml b/pnpm-lock.yaml index e539b9f..a367be5 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -439,6 +439,9 @@ importers: 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.4(@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 @@ -1187,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'} @@ -3193,6 +3201,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==} @@ -4701,6 +4713,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'} @@ -6903,6 +6918,11 @@ snapshots: dependencies: '@expressive-code/core': 0.35.6 + '@fast-check/vitest@0.1.5(vitest@3.0.4(@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.4(@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': {} @@ -9351,6 +9371,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: {} @@ -11245,6 +11269,8 @@ snapshots: punycode@2.3.1: {} + pure-rand@6.1.0: {} + qs@6.13.0: dependencies: side-channel: 1.0.6 From be2c8572da190d102f017e28b650664dea76965a Mon Sep 17 00:00:00 2001 From: Rob Knight Date: Sun, 2 Feb 2025 10:15:57 +0100 Subject: [PATCH 12/20] Add validation to membership list tuples --- packages/podspec/src/builders/group.ts | 86 ++++++++++++------- packages/podspec/src/builders/pod.ts | 39 ++++++--- packages/podspec/src/builders/shared.ts | 30 ++++--- packages/podspec/src/processors/validate.ts | 42 ++++----- packages/podspec/src/utils.ts | 16 ---- .../test/builders/PODSpecBuilder.spec.ts | 6 +- 6 files changed, 126 insertions(+), 93 deletions(-) delete mode 100644 packages/podspec/src/utils.ts diff --git a/packages/podspec/src/builders/group.ts b/packages/podspec/src/builders/group.ts index 7813450..c581a58 100644 --- a/packages/podspec/src/builders/group.ts +++ b/packages/podspec/src/builders/group.ts @@ -34,7 +34,7 @@ import { supportsRangeChecks, validateRange } from "./shared.js"; -import type { PODSpec } from "./pod.js"; +import { virtualEntries, type PODSpec } from "./pod.js"; export type NamedPODSpecs = Record>; @@ -131,29 +131,43 @@ export class PODGroupSpecBuilder< ? PODValueTypeFromTypeName>>[] : PODValueTupleForNamedEntries, N>[] ): PODGroupSpecBuilder { - // Check that all names exist in entries - for (const name of names) { - const [podName, entryName] = name.split("."); - if ( - podName === undefined || - entryName === undefined || - !(podName in this.#spec.pods) || - !(entryName in this.#spec.pods[podName]!.entries) - ) { - 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 = 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 (!(name in allEntries)) { + throw new Error(`Entry "${name}" does not exist`); + } + } + const statement: IsMemberOf, N> = { entries: names, type: "isMemberOf", - isMemberOf: convertValuesToStringTuples(names, values) + isMemberOf: convertValuesToStringTuples( + names, + values, + allEntries as Record + ) }; const baseName = `${names.join("_")}_isMemberOf`; @@ -179,29 +193,43 @@ export class PODGroupSpecBuilder< ? PODValueTypeFromTypeName>>[] : PODValueTupleForNamedEntries, N>[] ): PODGroupSpecBuilder { - // Check that all names exist in entries - for (const name of names) { - const [podName, entryName] = name.split("."); - if ( - podName === undefined || - entryName === undefined || - !(podName in this.#spec.pods) || - !(entryName in this.#spec.pods[podName]!.entries) - ) { - 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 = 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 (!(name in allEntries)) { + throw new Error(`Entry "${name}" does not exist`); + } + } + const statement: IsNotMemberOf, N> = { entries: names, type: "isNotMemberOf", - isNotMemberOf: convertValuesToStringTuples(names, values) + isNotMemberOf: convertValuesToStringTuples( + names, + values, + allEntries as Record + ) }; const baseName = `${names.join("_")}_isNotMemberOf`; diff --git a/packages/podspec/src/builders/pod.ts b/packages/podspec/src/builders/pod.ts index 219f330..9e503bd 100644 --- a/packages/podspec/src/builders/pod.ts +++ b/packages/podspec/src/builders/pod.ts @@ -40,7 +40,7 @@ import type { IsJsonSafe } from "../shared/jsonSafe.js"; /** @todo - - [ ] add lessThan, greaterThan, lessThanEq, greaterThanEq + - [x] add lessThan, greaterThan, lessThanEq, greaterThanEq - [ ] add omit - [x] maybe add pick/omit for statements? - [x] add signerPublicKey support (done at type level, not run-time) @@ -49,7 +49,7 @@ import type { IsJsonSafe } from "../shared/jsonSafe.js"; - [ ] refactor types (also delete unused types in types dir) - [x] rename away from v2 suffix - [x] validate entry names - - [ ] validate isMemberOf/isNotMemberOf parameters + - [x] validate isMemberOf/isNotMemberOf parameters - [ ] handle multiple/incompatible range checks on the same entry - [x] switch to using value types rather than PODValues (everywhere? maybe not membership lists) - [ ] better error messages @@ -63,7 +63,7 @@ function canonicalizeJSON(input: unknown): string | undefined { ); } -const virtualEntries: VirtualEntries = { +export const virtualEntries: VirtualEntries = { $contentID: "string", $signature: "string", $signerPublicKey: "eddsa_pubkey" @@ -278,19 +278,23 @@ export class PODSpecBuilder< >; } > { - // Check that all names exist in entries - for (const name of names) { - if (!(name in this.#spec.entries) && !(name in virtualEntries)) { - 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 (!(name in allEntries)) { + 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 @@ -307,7 +311,7 @@ export class PODSpecBuilder< const statement: IsMemberOf = { entries: names, type: "isMemberOf", - isMemberOf: convertValuesToStringTuples(names, values) + isMemberOf: convertValuesToStringTuples(names, values, allEntries) }; const baseName = `${names.join("_")}_isMemberOf`; @@ -366,6 +370,17 @@ export class PODSpecBuilder< throw new Error("Duplicate entry names are not allowed"); } + const allEntries = { + ...this.#spec.entries, + ...virtualEntries + }; + + for (const name of names) { + if (!(name in allEntries)) { + 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 @@ -382,7 +397,7 @@ export class PODSpecBuilder< const statement: IsNotMemberOf = { entries: names, type: "isNotMemberOf", - isNotMemberOf: convertValuesToStringTuples(names, values) + isNotMemberOf: convertValuesToStringTuples(names, values, allEntries) }; const baseName = `${names.join("_")}_isNotMemberOf`; diff --git a/packages/podspec/src/builders/shared.ts b/packages/podspec/src/builders/shared.ts index 6a1b37d..9ce6ae5 100644 --- a/packages/podspec/src/builders/shared.ts +++ b/packages/podspec/src/builders/shared.ts @@ -89,18 +89,28 @@ function valueToString(value: PODValue["value"]): string { */ export function convertValuesToStringTuples( names: [...N], - values: N["length"] extends 1 ? PODValue["value"][] : PODValue["value"][][] + 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) => [valueToString(v)] as { [K in keyof N]: string } - ) - : (values as PODValue["value"][][]).map( - (tuple) => - tuple.map((v) => valueToString(v)) as { - [K in keyof N]: string; - } - ); + ? (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; diff --git a/packages/podspec/src/processors/validate.ts b/packages/podspec/src/processors/validate.ts index 1b1e492..3304858 100644 --- a/packages/podspec/src/processors/validate.ts +++ b/packages/podspec/src/processors/validate.ts @@ -74,12 +74,27 @@ interface PODValidator { strictAssert(pod: POD): asserts pod is StrongPOD>; } +const SpecValidatorState = new WeakMap< + PODSpec, + boolean +>(); + export function validate( spec: PODSpec ): PODValidator { - // @TODO maybe typia's clone is better - spec = structuredClone(spec); - assertPODSpec(spec); + 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) => validatePOD(pod, spec, {}), @@ -101,11 +116,6 @@ export function validate( }; } -const SpecValidatorState = new WeakMap< - PODSpec, - boolean ->(); - /** * Validate a POD against a PODSpec. * @@ -114,25 +124,11 @@ const SpecValidatorState = new WeakMap< * @param options - The options to use for validation. * @returns true if the POD is valid, false otherwise. */ -export function validatePOD( +function validatePOD( pod: POD, spec: PODSpec, options: ValidateOptions = DEFAULT_VALIDATE_OPTIONS ): ValidateResult>> { - 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; - } - } - const issues = []; const path: string[] = []; 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/PODSpecBuilder.spec.ts b/packages/podspec/test/builders/PODSpecBuilder.spec.ts index db7afa2..c90fbd2 100644 --- a/packages/podspec/test/builders/PODSpecBuilder.spec.ts +++ b/packages/podspec/test/builders/PODSpecBuilder.spec.ts @@ -34,7 +34,7 @@ describe("PODSpecBuilder", () => { b_inRange: { entry: "b", type: "inRange", - inRange: { min: 10n, max: 100n } + inRange: { min: "10", max: "100" } } }); @@ -46,7 +46,7 @@ describe("PODSpecBuilder", () => { b_inRange: { entry: "b", type: "inRange", - inRange: { min: 10n, max: 100n } + inRange: { min: "10", max: "100" } } }); @@ -64,7 +64,7 @@ describe("PODSpecBuilder", () => { a_new_equalsEntry: { entry: "a", type: "equalsEntry", - equalsEntry: "new" + otherEntry: "new" } }); From 84d2d64e74e58fb1a07511f83a0a0252e9c7d53d Mon Sep 17 00:00:00 2001 From: Rob Knight Date: Sun, 2 Feb 2025 10:31:07 +0100 Subject: [PATCH 13/20] Formatting fix --- .vscode/extensions.json | 3 + .vscode/settings.json | 2 +- biome.jsonc | 2 +- packages/podspec/src/builders/group.ts | 112 +++++----- packages/podspec/src/builders/pod.ts | 205 +++++++++++------- packages/podspec/src/builders/shared.ts | 10 +- .../podspec/src/builders/types/entries.ts | 4 +- .../podspec/src/builders/types/statements.ts | 36 +-- packages/podspec/src/generated/podspec.ts | 122 ++++++----- packages/podspec/src/index.ts | 2 +- packages/podspec/src/pod_value_utils.ts | 2 +- packages/podspec/src/processors/validate.ts | 6 +- .../src/processors/validate/EntrySource.ts | 12 +- .../validate/checks/checkEqualsEntry.ts | 14 +- .../validate/checks/checkGreaterThan.ts | 14 +- .../validate/checks/checkGreaterThanEq.ts | 14 +- .../validate/checks/checkInRange.ts | 10 +- .../validate/checks/checkIsMemberOf.ts | 8 +- .../validate/checks/checkIsNotMemberOf.ts | 8 +- .../validate/checks/checkLessThan.ts | 14 +- .../validate/checks/checkLessThanEq.ts | 14 +- .../validate/checks/checkNotEqualsEntry.ts | 14 +- .../validate/checks/checkNotInRange.ts | 10 +- .../podspec/src/processors/validate/issues.ts | 2 +- .../podspec/src/processors/validate/result.ts | 2 +- .../test/builders/PODGroupSpecBuilder.spec.ts | 16 +- .../test/builders/PODSpecBuilder.spec.ts | 40 ++-- .../processors/validator/validator.spec.ts | 4 +- packages/podspec/tsup.config.ts | 4 +- packages/podspec/vitest.config.ts | 4 +- 30 files changed, 379 insertions(+), 331 deletions(-) create mode 100644 .vscode/extensions.json diff --git a/.vscode/extensions.json b/.vscode/extensions.json new file mode 100644 index 0000000..699ed73 --- /dev/null +++ b/.vscode/extensions.json @@ -0,0 +1,3 @@ +{ + "recommendations": ["biomejs.biome"] +} diff --git a/.vscode/settings.json b/.vscode/settings.json index 6248b6a..dd1f487 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -12,7 +12,7 @@ "ts-library.json": "jsonc" }, "[typescript]": { - "editor.defaultFormatter": "esbenp.prettier-vscode", + "editor.defaultFormatter": "biomejs.biome", "editor.codeActionsOnSave": { "quickfix.biome": "explicit", "source.organizeImports.biome": "explicit" 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/packages/podspec/src/builders/group.ts b/packages/podspec/src/builders/group.ts index c581a58..6a99f16 100644 --- a/packages/podspec/src/builders/group.ts +++ b/packages/podspec/src/builders/group.ts @@ -4,7 +4,7 @@ import { POD_DATE_MIN, POD_INT_MAX, POD_INT_MIN, - type PODName + type PODName, } from "@pcd/pod"; import type { EntryTypes, @@ -13,7 +13,7 @@ import type { PODValueTypeFromTypeName, PODValueTupleForNamedEntries, PODValueType, - EntriesOfType + EntriesOfType, } from "./types/entries.js"; import type { StatementMap, @@ -27,12 +27,12 @@ import type { GreaterThanEq, LessThan, LessThanEq, - SupportsRangeChecks + SupportsRangeChecks, } from "./types/statements.js"; import { convertValuesToStringTuples, supportsRangeChecks, - validateRange + validateRange, } from "./shared.js"; import { virtualEntries, type PODSpec } from "./pod.js"; @@ -64,7 +64,7 @@ type MustBePODValueType = T extends PODValueType ? T : never; type EntryType< P extends NamedPODSpecs, - K extends keyof AllPODEntries

+ K extends keyof AllPODEntries

, > = MustBePODValueType[K]>; type Evaluate = T extends infer O ? { [K in keyof O]: O[K] } : never; @@ -72,7 +72,7 @@ type Evaluate = T extends infer O ? { [K in keyof O]: O[K] } : never; type AddPOD< PODs extends NamedPODSpecs, N extends PODName, - Spec extends PODSpec + Spec extends PODSpec, > = Evaluate<{ [K in keyof PODs | N]: K extends N ? Spec : PODs[K & keyof PODs]; }>; @@ -87,7 +87,7 @@ type AddPOD< // the first. export class PODGroupSpecBuilder< P extends NamedPODSpecs, - S extends StatementMap + S extends StatementMap, > { readonly #spec: PODGroupSpec; @@ -99,7 +99,7 @@ export class PODGroupSpecBuilder< public static create(): PODGroupSpecBuilder<{}, {}> { return new PODGroupSpecBuilder({ pods: {}, - statements: {} + statements: {}, }); } @@ -110,7 +110,7 @@ export class PODGroupSpecBuilder< public pod< N extends PODName, Spec extends PODSpec, - NewPods extends AddPOD + NewPods extends AddPOD, >(name: N, spec: Spec): PODGroupSpecBuilder { if (name in this.#spec.pods) { throw new Error(`POD "${name}" already exists`); @@ -121,7 +121,7 @@ export class PODGroupSpecBuilder< return new PODGroupSpecBuilder({ ...this.#spec, - pods: { ...this.#spec.pods, [name]: spec } as unknown as NewPods + pods: { ...this.#spec.pods, [name]: spec } as unknown as NewPods, }); } @@ -142,15 +142,15 @@ export class PODGroupSpecBuilder< ...Object.entries(podSpec.entries).map( ([entryName, entryType]): [string, PODValueType] => [ `${podName}.${entryName}`, - entryType + entryType, ] ), ...Object.entries(virtualEntries).map( ([entryName, entryType]): [string, PODValueType] => [ `${podName}.${entryName}`, - entryType + entryType, ] - ) + ), ]) ); @@ -167,7 +167,7 @@ export class PODGroupSpecBuilder< names, values, allEntries as Record - ) + ), }; const baseName = `${names.join("_")}_isMemberOf`; @@ -182,8 +182,8 @@ export class PODGroupSpecBuilder< ...this.#spec, statements: { ...this.#spec.statements, - [statementName]: statement - } + [statementName]: statement, + }, }); } @@ -204,15 +204,15 @@ export class PODGroupSpecBuilder< ...Object.entries(podSpec.entries).map( ([entryName, entryType]): [string, PODValueType] => [ `${podName}.${entryName}`, - entryType + entryType, ] ), ...Object.entries(virtualEntries).map( ([entryName, entryType]): [string, PODValueType] => [ `${podName}.${entryName}`, - entryType + entryType, ] - ) + ), ]) ); @@ -229,7 +229,7 @@ export class PODGroupSpecBuilder< names, values, allEntries as Record - ) + ), }; const baseName = `${names.join("_")}_isNotMemberOf`; @@ -244,14 +244,14 @@ export class PODGroupSpecBuilder< ...this.#spec, statements: { ...this.#spec.statements, - [statementName]: statement - } + [statementName]: statement, + }, }); } public inRange< N extends keyof EntriesOfType, SupportsRangeChecks> & - string + string, >( name: N, range: { @@ -312,8 +312,8 @@ export class PODGroupSpecBuilder< max: range.max instanceof Date ? range.max.getTime().toString() - : range.max.toString() - } + : range.max.toString(), + }, }; const baseName = `${name}_inRange`; @@ -328,14 +328,14 @@ export class PODGroupSpecBuilder< ...this.#spec, statements: { ...this.#spec.statements, - [statementName]: statement - } + [statementName]: statement, + }, }); } public notInRange< N extends keyof EntriesOfType, SupportsRangeChecks> & - string + string, >( name: N, range: { @@ -396,8 +396,8 @@ export class PODGroupSpecBuilder< max: range.max instanceof Date ? range.max.getTime().toString() - : range.max.toString() - } + : range.max.toString(), + }, }; const baseName = `${name}_notInRange`; @@ -412,14 +412,14 @@ export class PODGroupSpecBuilder< ...this.#spec, statements: { ...this.#spec.statements, - [statementName]: statement - } + [statementName]: statement, + }, }); } public greaterThan< N1 extends keyof AllPODEntries

& string, - N2 extends keyof EntriesOfType, EntryType> & string + N2 extends keyof EntriesOfType, EntryType> & string, >(name1: N1, name2: N2): PODGroupSpecBuilder { // Check that both entries exist const [pod1, entry1] = name1.split("."); @@ -454,7 +454,7 @@ export class PODGroupSpecBuilder< const statement: GreaterThan, N1, N2> = { entry: name1, type: "greaterThan", - otherEntry: name2 + otherEntry: name2, }; const baseName = `${name1}_${name2}_greaterThan`; @@ -469,14 +469,14 @@ export class PODGroupSpecBuilder< ...this.#spec, statements: { ...this.#spec.statements, - [statementName]: statement - } + [statementName]: statement, + }, }); } public greaterThanEq< N1 extends keyof AllPODEntries

& string, - N2 extends keyof EntriesOfType, EntryType> & string + N2 extends keyof EntriesOfType, EntryType> & string, >(name1: N1, name2: N2): PODGroupSpecBuilder { // Check that both entries exist const [pod1, entry1] = name1.split("."); @@ -511,7 +511,7 @@ export class PODGroupSpecBuilder< const statement: GreaterThanEq, N1, N2> = { entry: name1, type: "greaterThanEq", - otherEntry: name2 + otherEntry: name2, }; const baseName = `${name1}_${name2}_greaterThanEq`; @@ -526,14 +526,14 @@ export class PODGroupSpecBuilder< ...this.#spec, statements: { ...this.#spec.statements, - [statementName]: statement - } + [statementName]: statement, + }, }); } public lessThan< N1 extends keyof AllPODEntries

& string, - N2 extends keyof EntriesOfType, EntryType> & string + N2 extends keyof EntriesOfType, EntryType> & string, >(name1: N1, name2: N2): PODGroupSpecBuilder { // Check that both entries exist const [pod1, entry1] = name1.split("."); @@ -568,7 +568,7 @@ export class PODGroupSpecBuilder< const statement: LessThan, N1, N2> = { entry: name1, type: "lessThan", - otherEntry: name2 + otherEntry: name2, }; const baseName = `${name1}_${name2}_lessThan`; @@ -583,14 +583,14 @@ export class PODGroupSpecBuilder< ...this.#spec, statements: { ...this.#spec.statements, - [statementName]: statement - } + [statementName]: statement, + }, }); } public lessThanEq< N1 extends keyof AllPODEntries

& string, - N2 extends keyof EntriesOfType, EntryType> & string + N2 extends keyof EntriesOfType, EntryType> & string, >(name1: N1, name2: N2): PODGroupSpecBuilder { // Check that both entries exist const [pod1, entry1] = name1.split("."); @@ -625,7 +625,7 @@ export class PODGroupSpecBuilder< const statement: LessThanEq, N1, N2> = { entry: name1, type: "lessThanEq", - otherEntry: name2 + otherEntry: name2, }; const baseName = `${name1}_${name2}_lessThanEq`; @@ -640,14 +640,14 @@ export class PODGroupSpecBuilder< ...this.#spec, statements: { ...this.#spec.statements, - [statementName]: statement - } + [statementName]: statement, + }, }); } public equalsEntry< N1 extends keyof AllPODEntries

& string, - N2 extends keyof EntriesOfType, EntryType> & string + N2 extends keyof EntriesOfType, EntryType> & string, >(name1: N1, name2: N2): PODGroupSpecBuilder { // Check that both entries exist const [pod1, entry1] = name1.split("."); @@ -682,7 +682,7 @@ export class PODGroupSpecBuilder< const statement: EqualsEntry, N1, N2> = { entry: name1, type: "equalsEntry", - otherEntry: name2 + otherEntry: name2, }; const baseName = `${name1}_${name2}_equalsEntry`; @@ -697,14 +697,14 @@ export class PODGroupSpecBuilder< ...this.#spec, statements: { ...this.#spec.statements, - [statementName]: statement - } + [statementName]: statement, + }, }); } public notEqualsEntry< N1 extends keyof AllPODEntries

& string, - N2 extends keyof EntriesOfType, EntryType> & string + N2 extends keyof EntriesOfType, EntryType> & string, >(name1: N1, name2: N2): PODGroupSpecBuilder { // Check that both entries exist const [pod1, entry1] = name1.split("."); @@ -739,7 +739,7 @@ export class PODGroupSpecBuilder< const statement: NotEqualsEntry, N1, N2> = { entry: name1, type: "notEqualsEntry", - otherEntry: name2 + otherEntry: name2, }; const baseName = `${name1}_${name2}_notEqualsEntry`; @@ -754,8 +754,8 @@ export class PODGroupSpecBuilder< ...this.#spec, statements: { ...this.#spec.statements, - [statementName]: statement - } + [statementName]: statement, + }, }); } } diff --git a/packages/podspec/src/builders/pod.ts b/packages/podspec/src/builders/pod.ts index 9e503bd..fe97238 100644 --- a/packages/podspec/src/builders/pod.ts +++ b/packages/podspec/src/builders/pod.ts @@ -1,31 +1,18 @@ import { - checkPODName, POD_DATE_MAX, POD_DATE_MIN, POD_INT_MAX, - POD_INT_MIN + POD_INT_MIN, + checkPODName, } from "@pcd/pod"; +import type { IsJsonSafe } from "../shared/jsonSafe.js"; import { + canonicalizeJSON, convertValuesToStringTuples, deepFreeze, supportsRangeChecks, - validateRange + validateRange, } from "./shared.js"; -import type { - IsMemberOf, - IsNotMemberOf, - InRange, - NotInRange, - EqualsEntry, - NotEqualsEntry, - SupportsRangeChecks, - StatementName, - StatementMap, - GreaterThan, - GreaterThanEq, - LessThan, - LessThanEq -} from "./types/statements.js"; import type { EntriesOfType, EntryKeys, @@ -33,15 +20,28 @@ import type { PODValueTupleForNamedEntries, PODValueType, PODValueTypeFromTypeName, - VirtualEntries + VirtualEntries, } from "./types/entries.js"; -import canonicalize from "canonicalize/lib/canonicalize.js"; -import type { IsJsonSafe } from "../shared/jsonSafe.js"; +import type { + EqualsEntry, + GreaterThan, + GreaterThanEq, + InRange, + IsMemberOf, + IsNotMemberOf, + LessThan, + LessThanEq, + NotEqualsEntry, + NotInRange, + StatementMap, + StatementName, + SupportsRangeChecks, +} from "./types/statements.js"; /** @todo - [x] add lessThan, greaterThan, lessThanEq, greaterThanEq - - [ ] add omit + - [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 @@ -56,17 +56,10 @@ import type { IsJsonSafe } from "../shared/jsonSafe.js"; - [ ] consider adding a hash to the spec to prevent tampering */ -function canonicalizeJSON(input: unknown): string | undefined { - // Something is screwy with the typings for canonicalize - return (canonicalize as unknown as (input: unknown) => string | undefined)( - input - ); -} - export const virtualEntries: VirtualEntries = { $contentID: "string", - $signature: "string", - $signerPublicKey: "eddsa_pubkey" + //$signature: "string", + $signerPublicKey: "eddsa_pubkey", }; export type PODSpec = { @@ -113,13 +106,13 @@ type Concrete = T extends object ? { [K in keyof T]: T[K] } : T; type AddEntry< E extends EntryTypes, K extends keyof E, - V extends PODValueType + V extends PODValueType, > = Concrete; export class PODSpecBuilder< E extends EntryTypes, // eslint-disable-next-line @typescript-eslint/no-empty-object-type - S extends StatementMap = {} + S extends StatementMap = {}, > { readonly #spec: PODSpec; @@ -130,7 +123,7 @@ export class PODSpecBuilder< public static create() { return new PODSpecBuilder({ entries: {}, - statements: {} + statements: {}, }); } @@ -146,7 +139,7 @@ export class PODSpecBuilder< return JSON.stringify( { ...this.#spec, - hash: canonicalized /* TODO hashing! */ + hash: canonicalized /* TODO hashing! */, }, null, 2 @@ -156,7 +149,7 @@ export class PODSpecBuilder< public entry< K extends string, V extends PODValueType, - NewEntries extends AddEntry + NewEntries extends AddEntry, >(key: Exclude, type: V): PODSpecBuilder { if (key in this.#spec.entries) { throw new Error(`Entry "${key}" already exists`); @@ -169,16 +162,16 @@ export class PODSpecBuilder< ...this.#spec, entries: { ...this.#spec.entries, - [key]: type + [key]: type, } as NewEntries, - statements: this.#spec.statements + statements: this.#spec.statements, }); } /** * Pick entries by key */ - public pick( + public pickEntries( keys: K[] ): PODSpecBuilder, Concrete>> { return new PODSpecBuilder({ @@ -217,7 +210,51 @@ export class PODSpecBuilder< ); } }) - ) as Concrete> + ) as Concrete>, + }); + } + + public omitEntries( + keys: K[] + ): PODSpecBuilder, Concrete>> { + return new PODSpecBuilder({ + ...this.#spec, + entries: Object.fromEntries( + Object.entries(this.#spec.entries).filter( + ([key]) => !keys.includes(key as K) + ) + ) as Omit, + statements: Object.fromEntries( + Object.entries(this.#spec.statements).filter(([_key, statement]) => { + const statementType = statement.type; + switch (statementType) { + case "isMemberOf": + case "isNotMemberOf": + return (statement.entries as EntryKeys).every( + (entry) => !keys.includes(entry as K) + ); + case "inRange": + case "notInRange": + return keys.includes(statement.entry as K); + case "equalsEntry": + case "notEqualsEntry": + case "greaterThan": + case "greaterThanEq": + case "lessThan": + case "lessThanEq": + return ( + !keys.includes(statement.entry as K) && + !keys.includes(statement.otherEntry as K) + ); + + default: + const _exhaustiveCheck: never = statement; + throw new Error( + `Unsupported statement type: ${statementType as string}` + ); + } + }) + ) as Concrete>, }); } @@ -230,7 +267,7 @@ export class PODSpecBuilder< Object.entries(this.#spec.statements).filter(([key]) => keys.includes(key as K) ) - ) as Concrete> + ) as Concrete>, }); } @@ -243,7 +280,7 @@ export class PODSpecBuilder< Object.entries(this.#spec.statements).filter( ([key]) => !keys.includes(key as K) ) - ) as Concrete> + ) as Concrete>, }); } @@ -286,7 +323,7 @@ export class PODSpecBuilder< const allEntries = { ...this.#spec.entries, - ...virtualEntries + ...virtualEntries, }; for (const name of names) { @@ -311,7 +348,7 @@ export class PODSpecBuilder< const statement: IsMemberOf = { entries: names, type: "isMemberOf", - isMemberOf: convertValuesToStringTuples(names, values, allEntries) + isMemberOf: convertValuesToStringTuples(names, values, allEntries), }; const baseName = `${names.join("_")}_isMemberOf`; @@ -326,8 +363,8 @@ export class PODSpecBuilder< ...this.#spec, statements: { ...this.#spec.statements, - [statementName]: statement - } + [statementName]: statement, + }, }); } @@ -372,7 +409,7 @@ export class PODSpecBuilder< const allEntries = { ...this.#spec.entries, - ...virtualEntries + ...virtualEntries, }; for (const name of names) { @@ -397,7 +434,7 @@ export class PODSpecBuilder< const statement: IsNotMemberOf = { entries: names, type: "isNotMemberOf", - isNotMemberOf: convertValuesToStringTuples(names, values, allEntries) + isNotMemberOf: convertValuesToStringTuples(names, values, allEntries), }; const baseName = `${names.join("_")}_isNotMemberOf`; @@ -412,8 +449,8 @@ export class PODSpecBuilder< ...this.#spec, statements: { ...this.#spec.statements, - [statementName]: statement - } + [statementName]: statement, + }, }); } @@ -425,7 +462,7 @@ export class PODSpecBuilder< * @returns A new PODSpecBuilder with the statement added */ public inRange< - N extends keyof EntriesOfType & string + N extends keyof EntriesOfType & string, >( name: N, range: { @@ -483,8 +520,8 @@ export class PODSpecBuilder< max: range.max instanceof Date ? range.max.getTime().toString() - : range.max.toString() - } + : range.max.toString(), + }, }; const baseName = `${name}_inRange`; @@ -499,8 +536,8 @@ export class PODSpecBuilder< ...this.#spec, statements: { ...this.#spec.statements, - [statementName]: statement - } + [statementName]: statement, + }, }); } @@ -512,7 +549,7 @@ export class PODSpecBuilder< * @returns A new PODSpecBuilder with the statement added */ public notInRange< - N extends keyof EntriesOfType & string + N extends keyof EntriesOfType & string, >( name: N, range: { @@ -573,8 +610,8 @@ export class PODSpecBuilder< max: range.max instanceof Date ? range.max.getTime().toString() - : range.max.toString() - } + : range.max.toString(), + }, }; const baseName = `${name}_notInRange`; @@ -589,8 +626,8 @@ export class PODSpecBuilder< ...this.#spec, statements: { ...this.#spec.statements, - [statementName]: statement - } + [statementName]: statement, + }, }); } @@ -600,7 +637,7 @@ export class PODSpecBuilder< E & VirtualEntries, (E & VirtualEntries)[N1] > & - string + string, >( name1: N1, name2: Exclude @@ -627,7 +664,7 @@ export class PODSpecBuilder< const statement = { entry: name1, type: "equalsEntry", - otherEntry: name2 + otherEntry: name2, } satisfies EqualsEntry; const baseName = `${name1}_${name2}_equalsEntry`; @@ -642,8 +679,8 @@ export class PODSpecBuilder< ...this.#spec, statements: { ...this.#spec.statements, - [statementName]: statement - } + [statementName]: statement, + }, }); } @@ -653,7 +690,7 @@ export class PODSpecBuilder< E & VirtualEntries, (E & VirtualEntries)[N1] > & - string + string, >( name1: N1, name2: Exclude @@ -684,7 +721,7 @@ export class PODSpecBuilder< const statement = { entry: name1, type: "notEqualsEntry", - otherEntry: name2 + otherEntry: name2, } satisfies NotEqualsEntry; const baseName = `${name1}_${name2}_notEqualsEntry`; @@ -699,8 +736,8 @@ export class PODSpecBuilder< ...this.#spec, statements: { ...this.#spec.statements, - [statementName]: statement - } + [statementName]: statement, + }, }); } @@ -710,7 +747,7 @@ export class PODSpecBuilder< E & VirtualEntries, (E & VirtualEntries)[N1] > & - string + string, >( name1: N1, name2: Exclude @@ -737,7 +774,7 @@ export class PODSpecBuilder< const statement = { entry: name1, type: "greaterThan", - otherEntry: name2 + otherEntry: name2, } satisfies GreaterThan; const baseName = `${name1}_${name2}_greaterThan`; @@ -752,8 +789,8 @@ export class PODSpecBuilder< ...this.#spec, statements: { ...this.#spec.statements, - [statementName]: statement - } + [statementName]: statement, + }, }); } @@ -763,7 +800,7 @@ export class PODSpecBuilder< E & VirtualEntries, (E & VirtualEntries)[N1] > & - string + string, >( name1: N1, name2: Exclude @@ -794,7 +831,7 @@ export class PODSpecBuilder< const statement = { entry: name1, type: "greaterThanEq", - otherEntry: name2 + otherEntry: name2, } satisfies GreaterThanEq; const baseName = `${name1}_${name2}_greaterThanEq`; @@ -809,8 +846,8 @@ export class PODSpecBuilder< ...this.#spec, statements: { ...this.#spec.statements, - [statementName]: statement - } + [statementName]: statement, + }, }); } @@ -820,7 +857,7 @@ export class PODSpecBuilder< E & VirtualEntries, (E & VirtualEntries)[N1] > & - string + string, >( name1: N1, name2: Exclude @@ -847,7 +884,7 @@ export class PODSpecBuilder< const statement = { entry: name1, type: "lessThan", - otherEntry: name2 + otherEntry: name2, } satisfies LessThan; const baseName = `${name1}_${name2}_lessThan`; @@ -862,8 +899,8 @@ export class PODSpecBuilder< ...this.#spec, statements: { ...this.#spec.statements, - [statementName]: statement - } + [statementName]: statement, + }, }); } @@ -873,7 +910,7 @@ export class PODSpecBuilder< E & VirtualEntries, (E & VirtualEntries)[N1] > & - string + string, >( name1: N1, name2: Exclude @@ -900,7 +937,7 @@ export class PODSpecBuilder< const statement = { entry: name1, type: "lessThanEq", - otherEntry: name2 + otherEntry: name2, } satisfies LessThanEq; const baseName = `${name1}_${name2}_lessThanEq`; @@ -915,8 +952,8 @@ export class PODSpecBuilder< ...this.#spec, statements: { ...this.#spec.statements, - [statementName]: statement - } + [statementName]: statement, + }, }); } } diff --git a/packages/podspec/src/builders/shared.ts b/packages/podspec/src/builders/shared.ts index 9ce6ae5..52146ed 100644 --- a/packages/podspec/src/builders/shared.ts +++ b/packages/podspec/src/builders/shared.ts @@ -2,6 +2,7 @@ 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. @@ -54,7 +55,7 @@ export function deepFreeze(obj: T): T { // Get all properties, including non-enumerable ones const properties = [ ...Object.getOwnPropertyNames(obj), - ...Object.getOwnPropertySymbols(obj) + ...Object.getOwnPropertySymbols(obj), ]; properties.forEach((prop) => { @@ -130,3 +131,10 @@ export function supportsRangeChecks( 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 index 389546d..aa8494c 100644 --- a/packages/podspec/src/builders/types/entries.ts +++ b/packages/podspec/src/builders/types/entries.ts @@ -8,7 +8,7 @@ export type EntryKeys = (keyof E & string)[]; export type PODValueTupleForNamedEntries< E extends EntryTypes, - Names extends EntryKeys + Names extends EntryKeys, > = { [K in keyof Names]: PODValueTypeFromTypeName; }; @@ -24,6 +24,6 @@ export type EntriesOfType = { export type VirtualEntries = { $contentID: "string"; - $signature: "string"; + //$signature: "string"; $signerPublicKey: "eddsa_pubkey"; }; diff --git a/packages/podspec/src/builders/types/statements.ts b/packages/podspec/src/builders/types/statements.ts index b2a3024..935e1d3 100644 --- a/packages/podspec/src/builders/types/statements.ts +++ b/packages/podspec/src/builders/types/statements.ts @@ -3,7 +3,7 @@ import type { EntryKeys, PODValueTupleForNamedEntries, EntriesOfType, - VirtualEntries + VirtualEntries, } from "./entries.js"; /**************************************************************************** @@ -12,19 +12,19 @@ import type { export type MembershipListInput< E extends EntryTypes, - N extends EntryKeys + N extends EntryKeys, > = PODValueTupleForNamedEntries[]; export type MembershipListPersistent< E extends EntryTypes, - N extends EntryKeys + 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[] + N extends EntryKeys & string[], > = { entries: N; type: "isMemberOf"; @@ -33,7 +33,7 @@ export type IsMemberOf< export type IsNotMemberOf< E extends EntryTypes, - N extends EntryKeys & string[] + N extends EntryKeys & string[], > = { entries: N; type: "isNotMemberOf"; @@ -46,7 +46,7 @@ export type SupportsRangeChecks = "int" | "boolean" | "date"; export type RangeInput< E extends EntryTypes, N extends keyof EntriesOfType & - string + string, > = { min: E[N] extends "date" ? Date : bigint; max: E[N] extends "date" ? Date : bigint; @@ -60,7 +60,7 @@ export type RangePersistent = { export type InRange< E extends EntryTypes, N extends keyof EntriesOfType & - string + string, > = { entry: N; type: "inRange"; @@ -70,7 +70,7 @@ export type InRange< export type NotInRange< E extends EntryTypes, N extends keyof EntriesOfType & - string + string, > = { entry: N; type: "notInRange"; @@ -80,7 +80,7 @@ export type NotInRange< export type EqualsEntry< E extends EntryTypes, N1 extends keyof (E & VirtualEntries) & string, - N2 extends keyof (E & VirtualEntries) & string + N2 extends keyof (E & VirtualEntries) & string, > = { entry: N1; type: "equalsEntry"; @@ -90,7 +90,7 @@ export type EqualsEntry< export type NotEqualsEntry< E extends EntryTypes, N1 extends keyof (E & VirtualEntries) & string, - N2 extends keyof (E & VirtualEntries) & string + N2 extends keyof (E & VirtualEntries) & string, > = { entry: N1; type: "notEqualsEntry"; @@ -100,7 +100,7 @@ export type NotEqualsEntry< export type GreaterThan< E extends EntryTypes, N1 extends keyof (E & VirtualEntries) & string, - N2 extends keyof (E & VirtualEntries) & string + N2 extends keyof (E & VirtualEntries) & string, > = { entry: N1; type: "greaterThan"; @@ -110,7 +110,7 @@ export type GreaterThan< export type GreaterThanEq< E extends EntryTypes, N1 extends keyof (E & VirtualEntries) & string, - N2 extends keyof (E & VirtualEntries) & string + N2 extends keyof (E & VirtualEntries) & string, > = { entry: N1; type: "greaterThanEq"; @@ -120,7 +120,7 @@ export type GreaterThanEq< export type LessThan< E extends EntryTypes, N1 extends keyof (E & VirtualEntries) & string, - N2 extends keyof (E & VirtualEntries) & string + N2 extends keyof (E & VirtualEntries) & string, > = { entry: N1; type: "lessThan"; @@ -130,7 +130,7 @@ export type LessThan< export type LessThanEq< E extends EntryTypes, N1 extends keyof (E & VirtualEntries) & string, - N2 extends keyof (E & VirtualEntries) & string + N2 extends keyof (E & VirtualEntries) & string, > = { entry: N1; type: "lessThanEq"; @@ -168,7 +168,7 @@ export type StatementMap = Record; // Utility types for statement naming type JoinWithUnderscore = T extends readonly [ infer F extends string, - ...infer R extends string[] + ...infer R extends string[], ] ? R["length"] extends 0 ? F @@ -177,12 +177,12 @@ type JoinWithUnderscore = T extends readonly [ type BaseStatementName< N extends readonly string[], - S extends Statements["type"] + S extends Statements["type"], > = `${JoinWithUnderscore}_${S}`; type NextAvailableSuffix< Base extends string, - S extends StatementMap + S extends StatementMap, > = Base extends keyof S ? `${Base}_1` extends keyof S ? `${Base}_2` extends keyof S @@ -194,5 +194,5 @@ type NextAvailableSuffix< export type StatementName< N extends readonly string[], S extends Statements["type"], - Map extends StatementMap + Map extends StatementMap, > = NextAvailableSuffix, Map>; diff --git a/packages/podspec/src/generated/podspec.ts b/packages/podspec/src/generated/podspec.ts index 62c70ac..440eb45 100644 --- a/packages/podspec/src/generated/podspec.ts +++ b/packages/podspec/src/generated/podspec.ts @@ -11,8 +11,8 @@ export const assertPODSpec = (() => { false === Array.isArray(input.entries) && _io1(input.entries) && "object" === typeof input.statements && - null !== input.statements && - false === Array.isArray(input.statements) && + null !== input.statements && + false === Array.isArray(input.statements) && _io2(input.statements); const _io1 = (input: any): boolean => Object.keys(input).every((key: any) => { @@ -58,14 +58,16 @@ export const assertPODSpec = (() => { const _io5 = (input: any): boolean => "string" === typeof input.entry && "inRange" === input.type && - "object" === typeof input.inRange && null !== input.inRange && + "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 => "string" === typeof input.entry && "notInRange" === input.type && - "object" === typeof input.notInRange && null !== input.notInRange && + "object" === typeof input.notInRange && + null !== input.notInRange && _io6(input.notInRange); const _io8 = (input: any): boolean => "string" === typeof input.entry && @@ -119,7 +121,7 @@ export const assertPODSpec = (() => { method: "typia.createAssert", path: _path + ".entries", expected: "EntryTypes", - value: input.entries + value: input.entries, }, _errorFactory )) && @@ -130,7 +132,7 @@ export const assertPODSpec = (() => { method: "typia.createAssert", path: _path + ".entries", expected: "EntryTypes", - value: input.entries + value: input.entries, }, _errorFactory )) && @@ -143,7 +145,7 @@ export const assertPODSpec = (() => { method: "typia.createAssert", path: _path + ".statements", expected: "StatementMap", - value: input.statements + value: input.statements, }, _errorFactory )) && @@ -154,7 +156,7 @@ export const assertPODSpec = (() => { method: "typia.createAssert", path: _path + ".statements", expected: "StatementMap", - value: input.statements + value: input.statements, }, _errorFactory )); @@ -187,7 +189,7 @@ export const assertPODSpec = (() => { ), expected: '("boolean" | "bytes" | "cryptographic" | "date" | "eddsa_pubkey" | "int" | "null" | "string")', - value: value + value: value, }, _errorFactory ) @@ -215,7 +217,7 @@ export const assertPODSpec = (() => { ), expected: "(EqualsEntry | GreaterThan | GreaterThanEq | InRange | IsMemberOf> | IsNotMemberOf> | LessThan | LessThanEq | NotEqualsEntry | NotInRange)", - value: value + value: value, }, _errorFactory )) && @@ -238,7 +240,7 @@ export const assertPODSpec = (() => { ), expected: "(EqualsEntry | GreaterThan | GreaterThanEq | InRange | IsMemberOf> | IsNotMemberOf> | LessThan | LessThanEq | NotEqualsEntry | NotInRange)", - value: value + value: value, }, _errorFactory ) @@ -256,7 +258,7 @@ export const assertPODSpec = (() => { method: "typia.createAssert", path: _path + ".entries", expected: "Array", - value: input.entries + value: input.entries, }, _errorFactory )) && @@ -269,7 +271,7 @@ export const assertPODSpec = (() => { method: "typia.createAssert", path: _path + ".entries[" + _index7 + "]", expected: "string", - value: elem + value: elem, }, _errorFactory ) @@ -280,7 +282,7 @@ export const assertPODSpec = (() => { method: "typia.createAssert", path: _path + ".entries", expected: "Array", - value: input.entries + value: input.entries, }, _errorFactory )) && @@ -291,7 +293,7 @@ export const assertPODSpec = (() => { method: "typia.createAssert", path: _path + ".type", expected: '"isMemberOf"', - value: input.type + value: input.type, }, _errorFactory )) && @@ -302,7 +304,7 @@ export const assertPODSpec = (() => { method: "typia.createAssert", path: _path + ".isMemberOf", expected: "Array>", - value: input.isMemberOf + value: input.isMemberOf, }, _errorFactory )) && @@ -315,7 +317,7 @@ export const assertPODSpec = (() => { method: "typia.createAssert", path: _path + ".isMemberOf[" + _index8 + "]", expected: "Array", - value: elem + value: elem, }, _errorFactory )) && @@ -329,7 +331,7 @@ export const assertPODSpec = (() => { path: _path + ".isMemberOf[" + _index8 + "][" + _index9 + "]", expected: "string", - value: elem + value: elem, }, _errorFactory ) @@ -340,7 +342,7 @@ export const assertPODSpec = (() => { method: "typia.createAssert", path: _path + ".isMemberOf[" + _index8 + "]", expected: "Array", - value: elem + value: elem, }, _errorFactory ) @@ -351,7 +353,7 @@ export const assertPODSpec = (() => { method: "typia.createAssert", path: _path + ".isMemberOf", expected: "Array>", - value: input.isMemberOf + value: input.isMemberOf, }, _errorFactory )); @@ -367,7 +369,7 @@ export const assertPODSpec = (() => { method: "typia.createAssert", path: _path + ".entries", expected: "Array", - value: input.entries + value: input.entries, }, _errorFactory )) && @@ -380,7 +382,7 @@ export const assertPODSpec = (() => { method: "typia.createAssert", path: _path + ".entries[" + _index10 + "]", expected: "string", - value: elem + value: elem, }, _errorFactory ) @@ -391,7 +393,7 @@ export const assertPODSpec = (() => { method: "typia.createAssert", path: _path + ".entries", expected: "Array", - value: input.entries + value: input.entries, }, _errorFactory )) && @@ -402,7 +404,7 @@ export const assertPODSpec = (() => { method: "typia.createAssert", path: _path + ".type", expected: '"isNotMemberOf"', - value: input.type + value: input.type, }, _errorFactory )) && @@ -413,7 +415,7 @@ export const assertPODSpec = (() => { method: "typia.createAssert", path: _path + ".isNotMemberOf", expected: "Array>", - value: input.isNotMemberOf + value: input.isNotMemberOf, }, _errorFactory )) && @@ -426,7 +428,7 @@ export const assertPODSpec = (() => { method: "typia.createAssert", path: _path + ".isNotMemberOf[" + _index11 + "]", expected: "Array", - value: elem + value: elem, }, _errorFactory )) && @@ -445,7 +447,7 @@ export const assertPODSpec = (() => { _index12 + "]", expected: "string", - value: elem + value: elem, }, _errorFactory ) @@ -456,7 +458,7 @@ export const assertPODSpec = (() => { method: "typia.createAssert", path: _path + ".isNotMemberOf[" + _index11 + "]", expected: "Array", - value: elem + value: elem, }, _errorFactory ) @@ -467,7 +469,7 @@ export const assertPODSpec = (() => { method: "typia.createAssert", path: _path + ".isNotMemberOf", expected: "Array>", - value: input.isNotMemberOf + value: input.isNotMemberOf, }, _errorFactory )); @@ -483,7 +485,7 @@ export const assertPODSpec = (() => { method: "typia.createAssert", path: _path + ".entry", expected: "string", - value: input.entry + value: input.entry, }, _errorFactory )) && @@ -494,7 +496,7 @@ export const assertPODSpec = (() => { method: "typia.createAssert", path: _path + ".type", expected: '"inRange"', - value: input.type + value: input.type, }, _errorFactory )) && @@ -505,7 +507,7 @@ export const assertPODSpec = (() => { method: "typia.createAssert", path: _path + ".inRange", expected: "RangePersistent", - value: input.inRange + value: input.inRange, }, _errorFactory )) && @@ -516,7 +518,7 @@ export const assertPODSpec = (() => { method: "typia.createAssert", path: _path + ".inRange", expected: "RangePersistent", - value: input.inRange + value: input.inRange, }, _errorFactory )); @@ -532,7 +534,7 @@ export const assertPODSpec = (() => { method: "typia.createAssert", path: _path + ".min", expected: "string", - value: input.min + value: input.min, }, _errorFactory )) && @@ -543,7 +545,7 @@ export const assertPODSpec = (() => { method: "typia.createAssert", path: _path + ".max", expected: "string", - value: input.max + value: input.max, }, _errorFactory )); @@ -559,7 +561,7 @@ export const assertPODSpec = (() => { method: "typia.createAssert", path: _path + ".entry", expected: "string", - value: input.entry + value: input.entry, }, _errorFactory )) && @@ -570,7 +572,7 @@ export const assertPODSpec = (() => { method: "typia.createAssert", path: _path + ".type", expected: '"notInRange"', - value: input.type + value: input.type, }, _errorFactory )) && @@ -581,7 +583,7 @@ export const assertPODSpec = (() => { method: "typia.createAssert", path: _path + ".notInRange", expected: "RangePersistent", - value: input.notInRange + value: input.notInRange, }, _errorFactory )) && @@ -592,7 +594,7 @@ export const assertPODSpec = (() => { method: "typia.createAssert", path: _path + ".notInRange", expected: "RangePersistent", - value: input.notInRange + value: input.notInRange, }, _errorFactory )); @@ -608,7 +610,7 @@ export const assertPODSpec = (() => { method: "typia.createAssert", path: _path + ".entry", expected: "string", - value: input.entry + value: input.entry, }, _errorFactory )) && @@ -619,7 +621,7 @@ export const assertPODSpec = (() => { method: "typia.createAssert", path: _path + ".type", expected: '"equalsEntry"', - value: input.type + value: input.type, }, _errorFactory )) && @@ -630,7 +632,7 @@ export const assertPODSpec = (() => { method: "typia.createAssert", path: _path + ".otherEntry", expected: "string", - value: input.otherEntry + value: input.otherEntry, }, _errorFactory )); @@ -646,7 +648,7 @@ export const assertPODSpec = (() => { method: "typia.createAssert", path: _path + ".entry", expected: "string", - value: input.entry + value: input.entry, }, _errorFactory )) && @@ -657,7 +659,7 @@ export const assertPODSpec = (() => { method: "typia.createAssert", path: _path + ".type", expected: '"notEqualsEntry"', - value: input.type + value: input.type, }, _errorFactory )) && @@ -668,7 +670,7 @@ export const assertPODSpec = (() => { method: "typia.createAssert", path: _path + ".otherEntry", expected: "string", - value: input.otherEntry + value: input.otherEntry, }, _errorFactory )); @@ -684,7 +686,7 @@ export const assertPODSpec = (() => { method: "typia.createAssert", path: _path + ".entry", expected: "string", - value: input.entry + value: input.entry, }, _errorFactory )) && @@ -695,7 +697,7 @@ export const assertPODSpec = (() => { method: "typia.createAssert", path: _path + ".type", expected: '"greaterThan"', - value: input.type + value: input.type, }, _errorFactory )) && @@ -706,7 +708,7 @@ export const assertPODSpec = (() => { method: "typia.createAssert", path: _path + ".otherEntry", expected: "string", - value: input.otherEntry + value: input.otherEntry, }, _errorFactory )); @@ -722,7 +724,7 @@ export const assertPODSpec = (() => { method: "typia.createAssert", path: _path + ".entry", expected: "string", - value: input.entry + value: input.entry, }, _errorFactory )) && @@ -733,7 +735,7 @@ export const assertPODSpec = (() => { method: "typia.createAssert", path: _path + ".type", expected: '"greaterThanEq"', - value: input.type + value: input.type, }, _errorFactory )) && @@ -744,7 +746,7 @@ export const assertPODSpec = (() => { method: "typia.createAssert", path: _path + ".otherEntry", expected: "string", - value: input.otherEntry + value: input.otherEntry, }, _errorFactory )); @@ -760,7 +762,7 @@ export const assertPODSpec = (() => { method: "typia.createAssert", path: _path + ".entry", expected: "string", - value: input.entry + value: input.entry, }, _errorFactory )) && @@ -771,7 +773,7 @@ export const assertPODSpec = (() => { method: "typia.createAssert", path: _path + ".type", expected: '"lessThan"', - value: input.type + value: input.type, }, _errorFactory )) && @@ -782,7 +784,7 @@ export const assertPODSpec = (() => { method: "typia.createAssert", path: _path + ".otherEntry", expected: "string", - value: input.otherEntry + value: input.otherEntry, }, _errorFactory )); @@ -798,7 +800,7 @@ export const assertPODSpec = (() => { method: "typia.createAssert", path: _path + ".entry", expected: "string", - value: input.entry + value: input.entry, }, _errorFactory )) && @@ -809,7 +811,7 @@ export const assertPODSpec = (() => { method: "typia.createAssert", path: _path + ".type", expected: '"lessThanEq"', - value: input.type + value: input.type, }, _errorFactory )) && @@ -820,7 +822,7 @@ export const assertPODSpec = (() => { method: "typia.createAssert", path: _path + ".otherEntry", expected: "string", - value: input.otherEntry + value: input.otherEntry, }, _errorFactory )); @@ -858,7 +860,7 @@ export const assertPODSpec = (() => { path: _path, expected: "(IsMemberOf> | IsNotMemberOf> | InRange | NotInRange | LessThanEq | LessThan | GreaterThanEq | GreaterThan | NotEqualsEntry | EqualsEntry)", - value: input + value: input, }, _errorFactory ); @@ -880,7 +882,7 @@ export const assertPODSpec = (() => { method: "typia.createAssert", path: _path + "", expected: "PODSpec", - value: input + value: input, }, _errorFactory )) && @@ -891,7 +893,7 @@ export const assertPODSpec = (() => { method: "typia.createAssert", path: _path + "", expected: "PODSpec", - value: input + value: input, }, _errorFactory ))(input, "$input", true); diff --git a/packages/podspec/src/index.ts b/packages/podspec/src/index.ts index 423a27f..3782e81 100644 --- a/packages/podspec/src/index.ts +++ b/packages/podspec/src/index.ts @@ -7,5 +7,5 @@ export { PODSpecBuilder, type PODGroupSpec, PODGroupSpecBuilder, - validatePOD + validatePOD, }; 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/validate.ts b/packages/podspec/src/processors/validate.ts index 3304858..cafc045 100644 --- a/packages/podspec/src/processors/validate.ts +++ b/packages/podspec/src/processors/validate.ts @@ -60,7 +60,7 @@ export interface ValidateOptions { const DEFAULT_VALIDATE_OPTIONS: ValidateOptions = { exitOnError: false, - strict: false + strict: false, }; interface PODValidator { @@ -109,10 +109,10 @@ export function validate( strictAssert: (pod) => { const result = validatePOD(pod, spec, { strict: true, - exitOnError: true + exitOnError: true, }); if (!result.isValid) throw new Error("POD is not valid"); - } + }, }; } diff --git a/packages/podspec/src/processors/validate/EntrySource.ts b/packages/podspec/src/processors/validate/EntrySource.ts index 4c2c9bf..c10f700 100644 --- a/packages/podspec/src/processors/validate/EntrySource.ts +++ b/packages/podspec/src/processors/validate/EntrySource.ts @@ -10,7 +10,7 @@ import { type ValidationTypeMismatchIssue, type ValidationUnexpectedInputEntryIssue, type ValidationUnexpectedInputPodIssue, - IssueCode + IssueCode, } from "./issues.js"; import type { ValidateOptions } from "../validate.js"; @@ -33,7 +33,7 @@ function auditEntries( issues.push({ code: IssueCode.missing_entry, path: [...path, key], - key + key, } satisfies ValidationMissingEntryIssue); if (exitOnError) { return issues; @@ -43,7 +43,7 @@ function auditEntries( issues.push({ code: IssueCode.type_mismatch, path: [...path, key], - expectedType: entryType + expectedType: entryType, } satisfies ValidationTypeMismatchIssue); if (exitOnError) { return issues; @@ -57,7 +57,7 @@ function auditEntries( issues.push({ code: IssueCode.unexpected_input_entry, path: [...path, key], - key + key, } satisfies ValidationUnexpectedInputEntryIssue); if (exitOnError) { return issues; @@ -127,7 +127,7 @@ export class EntrySourcePodGroupSpec implements EntrySource { issues.push({ code: IssueCode.missing_pod, path: [...path, podName], - podName + podName, } satisfies ValidationMissingPodIssue); if (options.exitOnError) { return issues; @@ -155,7 +155,7 @@ export class EntrySourcePodGroupSpec implements EntrySource { issues.push({ code: IssueCode.unexpected_input_pod, path: [...path, podName], - podName + podName, } satisfies ValidationUnexpectedInputPodIssue); if (options.exitOnError) { return issues; diff --git a/packages/podspec/src/processors/validate/checks/checkEqualsEntry.ts b/packages/podspec/src/processors/validate/checks/checkEqualsEntry.ts index af9f1e4..bcafe00 100644 --- a/packages/podspec/src/processors/validate/checks/checkEqualsEntry.ts +++ b/packages/podspec/src/processors/validate/checks/checkEqualsEntry.ts @@ -2,7 +2,7 @@ import type { EqualsEntry } from "../../../builders/types/statements.js"; import type { EntrySource } from "../EntrySource.js"; import type { ValidationBaseIssue, - ValidationStatementNegativeResultIssue + ValidationStatementNegativeResultIssue, } from "../issues.js"; import { IssueCode } from "../issues.js"; import { valueIsEqual } from "../utils.js"; @@ -27,7 +27,7 @@ export function checkEqualsEntry( statementName: statementName, statementType: statement.type, entries: [statement.entry], - path: [...path, statementName] + path: [...path, statementName], }); return issues; } @@ -37,7 +37,7 @@ export function checkEqualsEntry( statementName: statementName, statementType: statement.type, entries: [statement.otherEntry], - path: [...path, statementName] + path: [...path, statementName], }); return issues; } @@ -51,7 +51,7 @@ export function checkEqualsEntry( statementName: statementName, statementType: statement.type, entries: [statement.entry, statement.otherEntry], - path: [...path, statementName] + path: [...path, statementName], }); return issues; } @@ -62,7 +62,7 @@ export function checkEqualsEntry( statementName: statementName, statementType: statement.type, entries: [statement.entry], - path: [...path, statementName] + path: [...path, statementName], }); return issues; } @@ -73,7 +73,7 @@ export function checkEqualsEntry( statementName: statementName, statementType: statement.type, entries: [statement.otherEntry], - path: [...path, statementName] + path: [...path, statementName], }); return issues; } @@ -86,7 +86,7 @@ export function checkEqualsEntry( statementName: statementName, statementType: statement.type, entries: [statement.entry, statement.otherEntry], - path: [...path, statementName] + path: [...path, statementName], } satisfies ValidationStatementNegativeResultIssue; return [issue]; } diff --git a/packages/podspec/src/processors/validate/checks/checkGreaterThan.ts b/packages/podspec/src/processors/validate/checks/checkGreaterThan.ts index 383f4c3..1b9c9dc 100644 --- a/packages/podspec/src/processors/validate/checks/checkGreaterThan.ts +++ b/packages/podspec/src/processors/validate/checks/checkGreaterThan.ts @@ -23,7 +23,7 @@ export function checkGreaterThan( statementName: statementName, statementType: statement.type, entries: [statement.entry], - path: [...path, statementName] + path: [...path, statementName], }); return issues; } @@ -33,7 +33,7 @@ export function checkGreaterThan( statementName: statementName, statementType: statement.type, entries: [statement.otherEntry], - path: [...path, statementName] + path: [...path, statementName], }); return issues; } @@ -48,7 +48,7 @@ export function checkGreaterThan( statementName: statementName, statementType: statement.type, entries: [statement.entry, statement.otherEntry], - path: [...path, statementName] + path: [...path, statementName], }); return issues; } @@ -59,7 +59,7 @@ export function checkGreaterThan( statementName: statementName, statementType: statement.type, entries: [statement.entry], - path: [...path, statementName] + path: [...path, statementName], }); return issues; } @@ -70,7 +70,7 @@ export function checkGreaterThan( statementName: statementName, statementType: statement.type, entries: [statement.otherEntry], - path: [...path, statementName] + path: [...path, statementName], }); return issues; } @@ -81,7 +81,7 @@ export function checkGreaterThan( statementName: statementName, statementType: statement.type, entries: [statement.entry, statement.otherEntry], - path: [...path, statementName] + path: [...path, statementName], }); return issues; } @@ -94,7 +94,7 @@ export function checkGreaterThan( statementName: statementName, statementType: statement.type, entries: [statement.entry, statement.otherEntry], - path: [...path, statementName] + 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 index 53c1547..b936418 100644 --- a/packages/podspec/src/processors/validate/checks/checkGreaterThanEq.ts +++ b/packages/podspec/src/processors/validate/checks/checkGreaterThanEq.ts @@ -23,7 +23,7 @@ export function checkGreaterThanEq( statementName: statementName, statementType: statement.type, entries: [statement.entry], - path: [...path, statementName] + path: [...path, statementName], }); return issues; } @@ -33,7 +33,7 @@ export function checkGreaterThanEq( statementName: statementName, statementType: statement.type, entries: [statement.otherEntry], - path: [...path, statementName] + path: [...path, statementName], }); return issues; } @@ -48,7 +48,7 @@ export function checkGreaterThanEq( statementName: statementName, statementType: statement.type, entries: [statement.entry, statement.otherEntry], - path: [...path, statementName] + path: [...path, statementName], }); return issues; } @@ -59,7 +59,7 @@ export function checkGreaterThanEq( statementName: statementName, statementType: statement.type, entries: [statement.entry], - path: [...path, statementName] + path: [...path, statementName], }); return issues; } @@ -70,7 +70,7 @@ export function checkGreaterThanEq( statementName: statementName, statementType: statement.type, entries: [statement.otherEntry], - path: [...path, statementName] + path: [...path, statementName], }); return issues; } @@ -81,7 +81,7 @@ export function checkGreaterThanEq( statementName: statementName, statementType: statement.type, entries: [statement.entry, statement.otherEntry], - path: [...path, statementName] + path: [...path, statementName], }); return issues; } @@ -94,7 +94,7 @@ export function checkGreaterThanEq( statementName: statementName, statementType: statement.type, entries: [statement.entry, statement.otherEntry], - path: [...path, statementName] + 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 index 525426a..69e3c00 100644 --- a/packages/podspec/src/processors/validate/checks/checkInRange.ts +++ b/packages/podspec/src/processors/validate/checks/checkInRange.ts @@ -4,7 +4,7 @@ import { IssueCode, type ValidationBaseIssue, type ValidationInvalidStatementIssue, - type ValidationStatementNegativeResultIssue + type ValidationStatementNegativeResultIssue, } from "../issues.js"; import type { EntrySource } from "../EntrySource.js"; @@ -28,7 +28,7 @@ export function checkInRange( statementName: statementName, statementType: statement.type, entries: [entryName], - path: [...path, statementName] + path: [...path, statementName], } satisfies ValidationInvalidStatementIssue); return issues; } @@ -50,8 +50,8 @@ export function checkInRange( statementName: statementName, statementType: statement.type, entries: [entryName], - path: [...path, statementName] - } satisfies ValidationStatementNegativeResultIssue as ValidationBaseIssue + path: [...path, statementName], + } satisfies ValidationStatementNegativeResultIssue as ValidationBaseIssue, ]; } } else { @@ -60,7 +60,7 @@ export function checkInRange( statementName: statementName, statementType: statement.type, entries: [entryName], - path: [...path, statementName] + 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 index e13a1c0..8b14eb0 100644 --- a/packages/podspec/src/processors/validate/checks/checkIsMemberOf.ts +++ b/packages/podspec/src/processors/validate/checks/checkIsMemberOf.ts @@ -1,7 +1,7 @@ import type { ValidationBaseIssue, ValidationInvalidStatementIssue, - ValidationStatementNegativeResultIssue + ValidationStatementNegativeResultIssue, } from "../issues.js"; import { IssueCode } from "../issues.js"; import type { IsMemberOf } from "../../../builders/types/statements.js"; @@ -22,8 +22,8 @@ function validateIsMemberOfStatement( statementName: statementName, statementType: statement.type, entries: statement.entries, - path: [...path, statementName] - } + path: [...path, statementName], + }, ]; } return []; @@ -79,7 +79,7 @@ export function checkIsMemberOf( statementName: statementName, statementType: statement.type, entries: statement.entries, - path: [...path, statementName] + path: [...path, statementName], } satisfies ValidationStatementNegativeResultIssue; if (exitOnError) { return [issue]; diff --git a/packages/podspec/src/processors/validate/checks/checkIsNotMemberOf.ts b/packages/podspec/src/processors/validate/checks/checkIsNotMemberOf.ts index 688c1a4..120047f 100644 --- a/packages/podspec/src/processors/validate/checks/checkIsNotMemberOf.ts +++ b/packages/podspec/src/processors/validate/checks/checkIsNotMemberOf.ts @@ -2,7 +2,7 @@ import { IssueCode, type ValidationBaseIssue, type ValidationInvalidStatementIssue, - type ValidationStatementNegativeResultIssue + type ValidationStatementNegativeResultIssue, } from "../issues.js"; import type { IsNotMemberOf } from "../../../builders/types/statements.js"; import { tupleToPODValueTypeValues, valueIsEqual } from "../utils.js"; @@ -22,8 +22,8 @@ function validateIsNotMemberOfStatement( statementName: statementName, statementType: statement.type, entries: statement.entries, - path: [...path, statementName] - } + path: [...path, statementName], + }, ]; } return []; @@ -80,7 +80,7 @@ export function checkIsNotMemberOf( statementName: statementName, statementType: statement.type, entries: statement.entries, - path: [...path, statementName] + path: [...path, statementName], } satisfies ValidationStatementNegativeResultIssue; if (exitOnError) { return [issue]; diff --git a/packages/podspec/src/processors/validate/checks/checkLessThan.ts b/packages/podspec/src/processors/validate/checks/checkLessThan.ts index 61b7f03..1a6e4a4 100644 --- a/packages/podspec/src/processors/validate/checks/checkLessThan.ts +++ b/packages/podspec/src/processors/validate/checks/checkLessThan.ts @@ -23,7 +23,7 @@ export function checkLessThan( statementName: statementName, statementType: statement.type, entries: [statement.entry], - path: [...path, statementName] + path: [...path, statementName], }); return issues; } @@ -33,7 +33,7 @@ export function checkLessThan( statementName: statementName, statementType: statement.type, entries: [statement.otherEntry], - path: [...path, statementName] + path: [...path, statementName], }); return issues; } @@ -48,7 +48,7 @@ export function checkLessThan( statementName: statementName, statementType: statement.type, entries: [statement.entry, statement.otherEntry], - path: [...path, statementName] + path: [...path, statementName], }); return issues; } @@ -59,7 +59,7 @@ export function checkLessThan( statementName: statementName, statementType: statement.type, entries: [statement.entry], - path: [...path, statementName] + path: [...path, statementName], }); return issues; } @@ -70,7 +70,7 @@ export function checkLessThan( statementName: statementName, statementType: statement.type, entries: [statement.otherEntry], - path: [...path, statementName] + path: [...path, statementName], }); return issues; } @@ -81,7 +81,7 @@ export function checkLessThan( statementName: statementName, statementType: statement.type, entries: [statement.entry, statement.otherEntry], - path: [...path, statementName] + path: [...path, statementName], }); return issues; } @@ -94,7 +94,7 @@ export function checkLessThan( statementName: statementName, statementType: statement.type, entries: [statement.entry, statement.otherEntry], - path: [...path, statementName] + 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 index 21fe549..0c64efd 100644 --- a/packages/podspec/src/processors/validate/checks/checkLessThanEq.ts +++ b/packages/podspec/src/processors/validate/checks/checkLessThanEq.ts @@ -23,7 +23,7 @@ export function checkLessThanEq( statementName: statementName, statementType: statement.type, entries: [statement.entry], - path: [...path, statementName] + path: [...path, statementName], }); return issues; } @@ -33,7 +33,7 @@ export function checkLessThanEq( statementName: statementName, statementType: statement.type, entries: [statement.otherEntry], - path: [...path, statementName] + path: [...path, statementName], }); return issues; } @@ -48,7 +48,7 @@ export function checkLessThanEq( statementName: statementName, statementType: statement.type, entries: [statement.entry, statement.otherEntry], - path: [...path, statementName] + path: [...path, statementName], }); return issues; } @@ -59,7 +59,7 @@ export function checkLessThanEq( statementName: statementName, statementType: statement.type, entries: [statement.entry], - path: [...path, statementName] + path: [...path, statementName], }); return issues; } @@ -70,7 +70,7 @@ export function checkLessThanEq( statementName: statementName, statementType: statement.type, entries: [statement.otherEntry], - path: [...path, statementName] + path: [...path, statementName], }); return issues; } @@ -81,7 +81,7 @@ export function checkLessThanEq( statementName: statementName, statementType: statement.type, entries: [statement.entry, statement.otherEntry], - path: [...path, statementName] + path: [...path, statementName], }); return issues; } @@ -94,7 +94,7 @@ export function checkLessThanEq( statementName: statementName, statementType: statement.type, entries: [statement.entry, statement.otherEntry], - path: [...path, statementName] + 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 index e059353..bd8cc0d 100644 --- a/packages/podspec/src/processors/validate/checks/checkNotEqualsEntry.ts +++ b/packages/podspec/src/processors/validate/checks/checkNotEqualsEntry.ts @@ -3,7 +3,7 @@ import type { EntrySource } from "../EntrySource.js"; import { IssueCode, type ValidationBaseIssue, - type ValidationStatementNegativeResultIssue + type ValidationStatementNegativeResultIssue, } from "../issues.js"; import { valueIsEqual } from "../utils.js"; @@ -27,7 +27,7 @@ export function checkNotEqualsEntry( statementName: statementName, statementType: statement.type, entries: [statement.entry], - path: [...path, statementName] + path: [...path, statementName], }); return issues; } @@ -37,7 +37,7 @@ export function checkNotEqualsEntry( statementName: statementName, statementType: statement.type, entries: [statement.otherEntry], - path: [...path, statementName] + path: [...path, statementName], }); return issues; } @@ -51,7 +51,7 @@ export function checkNotEqualsEntry( statementName: statementName, statementType: statement.type, entries: [statement.entry, statement.otherEntry], - path: [...path, statementName] + path: [...path, statementName], }); return issues; } @@ -62,7 +62,7 @@ export function checkNotEqualsEntry( statementName: statementName, statementType: statement.type, entries: [statement.entry], - path: [...path, statementName] + path: [...path, statementName], }); return issues; } @@ -73,7 +73,7 @@ export function checkNotEqualsEntry( statementName: statementName, statementType: statement.type, entries: [statement.otherEntry], - path: [...path, statementName] + path: [...path, statementName], }); return issues; } @@ -86,7 +86,7 @@ export function checkNotEqualsEntry( statementName: statementName, statementType: statement.type, entries: [statement.entry, statement.otherEntry], - path: [...path, statementName] + path: [...path, statementName], } satisfies ValidationStatementNegativeResultIssue; return [issue]; } diff --git a/packages/podspec/src/processors/validate/checks/checkNotInRange.ts b/packages/podspec/src/processors/validate/checks/checkNotInRange.ts index aa7683b..f066ea5 100644 --- a/packages/podspec/src/processors/validate/checks/checkNotInRange.ts +++ b/packages/podspec/src/processors/validate/checks/checkNotInRange.ts @@ -4,7 +4,7 @@ import { IssueCode, type ValidationBaseIssue, type ValidationInvalidStatementIssue, - type ValidationStatementNegativeResultIssue + type ValidationStatementNegativeResultIssue, } from "../issues.js"; import type { EntrySource } from "../EntrySource.js"; @@ -28,8 +28,8 @@ export function checkNotInRange( statementName: statementName, statementType: statement.type, entries: [entryName], - path: [...path, statementName] - } satisfies ValidationInvalidStatementIssue + path: [...path, statementName], + } satisfies ValidationInvalidStatementIssue, ]; return issues; } @@ -51,8 +51,8 @@ export function checkNotInRange( statementName: statementName, statementType: statement.type, entries: [entryName], - path: [...path, statementName] - } satisfies ValidationStatementNegativeResultIssue as ValidationBaseIssue + path: [...path, statementName], + } satisfies ValidationStatementNegativeResultIssue as ValidationBaseIssue, ]; } } diff --git a/packages/podspec/src/processors/validate/issues.ts b/packages/podspec/src/processors/validate/issues.ts index f162c09..8a1340a 100644 --- a/packages/podspec/src/processors/validate/issues.ts +++ b/packages/podspec/src/processors/validate/issues.ts @@ -10,7 +10,7 @@ export const IssueCode = { invalid_statement: "invalid_statement", unexpected_input_entry: "unexpected_input_entry", unexpected_input_pod: "unexpected_input_pod", - statement_negative_result: "statement_negative_result" + statement_negative_result: "statement_negative_result", } as const; /** diff --git a/packages/podspec/src/processors/validate/result.ts b/packages/podspec/src/processors/validate/result.ts index 554143c..f601637 100644 --- a/packages/podspec/src/processors/validate/result.ts +++ b/packages/podspec/src/processors/validate/result.ts @@ -20,6 +20,6 @@ export function FAILURE(errors: ValidationBaseIssue[]): ValidateFailure { export function SUCCESS(value: T): ValidateSuccess { return { isValid: true, - value + value, }; } diff --git a/packages/podspec/test/builders/PODGroupSpecBuilder.spec.ts b/packages/podspec/test/builders/PODGroupSpecBuilder.spec.ts index 8d40c44..8a131f5 100644 --- a/packages/podspec/test/builders/PODGroupSpecBuilder.spec.ts +++ b/packages/podspec/test/builders/PODGroupSpecBuilder.spec.ts @@ -22,14 +22,13 @@ describe("PODGroupSpecBuilder", () => { "foo.my_num": "int", "foo.$signerPublicKey": "eddsa_pubkey", "foo.$contentID": "string", - "foo.$signature": "string" }); expect(groupWithPod.spec()).toEqual({ pods: { - foo: podBuilder.spec() + foo: podBuilder.spec(), }, - statements: {} + statements: {}, }); const groupWithPodAndStatement = groupWithPod.isMemberOf( @@ -40,15 +39,15 @@ describe("PODGroupSpecBuilder", () => { expect(spec3).toEqual({ pods: { - foo: podBuilder.spec() + foo: podBuilder.spec(), }, statements: { "foo.my_string_isMemberOf": { entries: ["foo.my_string"], isMemberOf: [["hello"]], - type: "isMemberOf" - } - } + type: "isMemberOf", + }, + }, }); }); @@ -69,8 +68,7 @@ describe("PODGroupSpecBuilder", () => { "foo.my_num": "int", "foo.my_other_num": "int", "foo.$contentID": "string", - "foo.$signature": "string", - "foo.$signerPublicKey": "eddsa_pubkey" + "foo.$signerPublicKey": "eddsa_pubkey", }); groupWithPod.equalsEntry("foo.my_num", "foo.my_other_num"); diff --git a/packages/podspec/test/builders/PODSpecBuilder.spec.ts b/packages/podspec/test/builders/PODSpecBuilder.spec.ts index c90fbd2..118641c 100644 --- a/packages/podspec/test/builders/PODSpecBuilder.spec.ts +++ b/packages/podspec/test/builders/PODSpecBuilder.spec.ts @@ -12,7 +12,7 @@ describe("PODSpecBuilder", () => { const b = a.entry("a", "string").entry("b", "int"); expect(b.spec().entries).toEqual({ a: "string", - b: "int" + b: "int", }); const c = b.isMemberOf(["a"], ["foo"]); @@ -20,8 +20,8 @@ describe("PODSpecBuilder", () => { a_isMemberOf: { entries: ["a"], type: "isMemberOf", - isMemberOf: [["foo"]] - } + isMemberOf: [["foo"]], + }, }); const d = c.inRange("b", { min: 10n, max: 100n }); @@ -29,25 +29,25 @@ describe("PODSpecBuilder", () => { a_isMemberOf: { entries: ["a"], type: "isMemberOf", - isMemberOf: [["foo"]] + isMemberOf: [["foo"]], }, b_inRange: { entry: "b", type: "inRange", - inRange: { min: "10", max: "100" } - } + 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.pick(["b"]); + const f = e.pickEntries(["b"]); expect(f.spec().statements).toEqual({ b_inRange: { entry: "b", type: "inRange", - inRange: { min: "10", max: "100" } - } + inRange: { min: "10", max: "100" }, + }, }); const g = e.entry("new", "string").equalsEntry("a", "new"); @@ -64,42 +64,42 @@ describe("PODSpecBuilder", () => { a_new_equalsEntry: { entry: "a", type: "equalsEntry", - otherEntry: "new" - } + otherEntry: "new", + }, }); expect(g.spec()).toEqual({ entries: { a: "string", b: "int", - new: "string" + new: "string", }, statements: { a_isMemberOf: { entries: ["a"], type: "isMemberOf", - isMemberOf: [["foo"]] + 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"]] + isMemberOf: [["foo", "10"]], }, b_inRange: { entry: "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" } + inRange: { min: "10", max: "100" }, }, a_new_equalsEntry: { entry: "a", type: "equalsEntry", - otherEntry: "new" - } - } + otherEntry: "new", + }, + }, } satisfies typeof _GSpec); const h = g.pickStatements(["a_isMemberOf"]); @@ -107,8 +107,8 @@ describe("PODSpecBuilder", () => { a_isMemberOf: { entries: ["a"], type: "isMemberOf", - isMemberOf: [["foo"]] - } + isMemberOf: [["foo"]], + }, }); }); }); diff --git a/packages/podspec/test/processors/validator/validator.spec.ts b/packages/podspec/test/processors/validator/validator.spec.ts index 07b0cc4..5da76ad 100644 --- a/packages/podspec/test/processors/validator/validator.spec.ts +++ b/packages/podspec/test/processors/validator/validator.spec.ts @@ -3,7 +3,7 @@ import { POD, type PODValue, type PODStringValue, - type PODIntValue + type PODIntValue, } from "@pcd/pod"; import { describe, it, expect, assert } from "vitest"; import { PODSpecBuilder, validatePOD } from "../../../src/index.js"; @@ -21,7 +21,7 @@ describe("validator", () => { it("validatePOD", () => { const myPOD = signPOD({ foo: { type: "string", value: "foo" }, - num: { type: "int", value: 50n } + num: { type: "int", value: 50n }, }); const myPodSpecBuilder = PODSpecBuilder.create() .entry("foo", "string") diff --git a/packages/podspec/tsup.config.ts b/packages/podspec/tsup.config.ts index 1d39906..dc8dc4e 100644 --- a/packages/podspec/tsup.config.ts +++ b/packages/podspec/tsup.config.ts @@ -8,6 +8,6 @@ export default defineConfig({ splitting: false, minify: true, define: { - "import.meta.vitest": "undefined" - } + "import.meta.vitest": "undefined", + }, }); diff --git a/packages/podspec/vitest.config.ts b/packages/podspec/vitest.config.ts index a6b5390..47de263 100644 --- a/packages/podspec/vitest.config.ts +++ b/packages/podspec/vitest.config.ts @@ -2,6 +2,6 @@ import { defineConfig } from "vitest/config"; export default defineConfig({ test: { - includeSource: ["src/**/*.{js,ts}"] - } + includeSource: ["src/**/*.{js,ts}"], + }, }); From 64af8464b2ceb1f80771e3498522e93fd7169ebb Mon Sep 17 00:00:00 2001 From: Rob Knight Date: Sun, 2 Feb 2025 11:30:03 +0100 Subject: [PATCH 14/20] Basic property-based tests --- packages/eslint-config/eslint.base.config.mjs | 32 +-- packages/podspec/src/builders/group.ts | 248 ++++++++++++------ packages/podspec/src/builders/pod.ts | 168 ++++++++---- packages/podspec/src/index.ts | 11 +- .../src/processors/validate/EntrySource.ts | 16 +- .../test/builders/PODSpecBuilder.spec.ts | 158 ++++++++++- .../processors/validator/validator.spec.ts | 35 ++- 7 files changed, 491 insertions(+), 177 deletions(-) diff --git a/packages/eslint-config/eslint.base.config.mjs b/packages/eslint-config/eslint.base.config.mjs index 3ce407f..4ae4dff 100644 --- a/packages/eslint-config/eslint.base.config.mjs +++ b/packages/eslint-config/eslint.base.config.mjs @@ -19,37 +19,37 @@ export default tseslint.config( "**/vite.config.ts", "**/vitest.workspace.ts", "**/tailwind.config.ts", - "**/tsup.config.ts" - ] // global ignore with single ignore key + "**/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: { @@ -63,8 +63,8 @@ export default tseslint.config( argsIgnorePattern: "^_", varsIgnorePattern: "^_", destructuredArrayIgnorePattern: "^_", - caughtErrorsIgnorePattern: "^_" - } + caughtErrorsIgnorePattern: "^_", + }, ], "no-case-declarations": "off", "react-hooks/rules-of-hooks": "error", @@ -75,7 +75,9 @@ 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", "BinaryExpression[operator='in']"], + "guard-for-in": "error", + }, } ); diff --git a/packages/podspec/src/builders/group.ts b/packages/podspec/src/builders/group.ts index 6a99f16..9cbb9c8 100644 --- a/packages/podspec/src/builders/group.ts +++ b/packages/podspec/src/builders/group.ts @@ -1,40 +1,40 @@ import { - checkPODName, + type PODName, POD_DATE_MAX, POD_DATE_MIN, POD_INT_MAX, POD_INT_MIN, - type PODName, + checkPODName, } from "@pcd/pod"; +import { type PODSpec, virtualEntries } from "./pod.js"; +import { + convertValuesToStringTuples, + supportsRangeChecks, + validateRange, +} from "./shared.js"; import type { - EntryTypes, - VirtualEntries, + EntriesOfType, EntryKeys, - PODValueTypeFromTypeName, + EntryTypes, PODValueTupleForNamedEntries, PODValueType, - EntriesOfType, + PODValueTypeFromTypeName, + VirtualEntries, } from "./types/entries.js"; import type { - StatementMap, - IsMemberOf, - IsNotMemberOf, - InRange, - NotInRange, EqualsEntry, - NotEqualsEntry, GreaterThan, GreaterThanEq, + InRange, + IsMemberOf, + IsNotMemberOf, LessThan, LessThanEq, + NotEqualsEntry, + NotInRange, + StatementMap, SupportsRangeChecks, } from "./types/statements.js"; -import { - convertValuesToStringTuples, - supportsRangeChecks, - validateRange, -} from "./shared.js"; -import { virtualEntries, type PODSpec } from "./pod.js"; export type NamedPODSpecs = Record>; @@ -112,7 +112,7 @@ export class PODGroupSpecBuilder< Spec extends PODSpec, NewPods extends AddPOD, >(name: N, spec: Spec): PODGroupSpecBuilder { - if (name in this.#spec.pods) { + if (Object.prototype.hasOwnProperty.call(this.#spec.pods, name)) { throw new Error(`POD "${name}" already exists`); } @@ -129,7 +129,8 @@ export class PODGroupSpecBuilder< names: [...N], values: N["length"] extends 1 ? PODValueTypeFromTypeName>>[] - : PODValueTupleForNamedEntries, N>[] + : PODValueTupleForNamedEntries, N>[], + customStatementName?: string ): PODGroupSpecBuilder { // Check for duplicate names const uniqueNames = new Set(names); @@ -155,7 +156,7 @@ export class PODGroupSpecBuilder< ); for (const name of names) { - if (!(name in allEntries)) { + if (!Object.prototype.hasOwnProperty.call(allEntries, name)) { throw new Error(`Entry "${name}" does not exist`); } } @@ -170,11 +171,13 @@ export class PODGroupSpecBuilder< ), }; - const baseName = `${names.join("_")}_isMemberOf`; + const baseName = customStatementName ?? `${names.join("_")}_isMemberOf`; let statementName = baseName; let suffix = 1; - while (statementName in this.#spec.statements) { + while ( + Object.prototype.hasOwnProperty.call(this.#spec.statements, statementName) + ) { statementName = `${baseName}_${suffix++}`; } @@ -191,7 +194,8 @@ export class PODGroupSpecBuilder< names: [...N], values: N["length"] extends 1 ? PODValueTypeFromTypeName>>[] - : PODValueTupleForNamedEntries, N>[] + : PODValueTupleForNamedEntries, N>[], + customStatementName?: string ): PODGroupSpecBuilder { // Check for duplicate names const uniqueNames = new Set(names); @@ -217,7 +221,7 @@ export class PODGroupSpecBuilder< ); for (const name of names) { - if (!(name in allEntries)) { + if (!Object.prototype.hasOwnProperty.call(allEntries, name)) { throw new Error(`Entry "${name}" does not exist`); } } @@ -232,11 +236,13 @@ export class PODGroupSpecBuilder< ), }; - const baseName = `${names.join("_")}_isNotMemberOf`; + const baseName = customStatementName ?? `${names.join("_")}_isNotMemberOf`; let statementName = baseName; let suffix = 1; - while (statementName in this.#spec.statements) { + while ( + Object.prototype.hasOwnProperty.call(this.#spec.statements, statementName) + ) { statementName = `${baseName}_${suffix++}`; } @@ -257,15 +263,19 @@ export class PODGroupSpecBuilder< range: { min: AllPODEntries

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

[N] extends "date" ? Date : bigint; - } + }, + customStatementName?: string ): PODGroupSpecBuilder { // Check that the entry exists const [podName, entryName] = name.split("."); if ( podName === undefined || entryName === undefined || - !(podName in this.#spec.pods) || - !(entryName in this.#spec.pods[podName]!.entries) + !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`); } @@ -316,11 +326,13 @@ export class PODGroupSpecBuilder< }, }; - const baseName = `${name}_inRange`; + const baseName = customStatementName ?? `${name}_inRange`; let statementName = baseName; let suffix = 1; - while (statementName in this.#spec.statements) { + while ( + Object.prototype.hasOwnProperty.call(this.#spec.statements, statementName) + ) { statementName = `${baseName}_${suffix++}`; } @@ -341,15 +353,19 @@ export class PODGroupSpecBuilder< range: { min: AllPODEntries

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

[N] extends "date" ? Date : bigint; - } + }, + customStatementName?: string ): PODGroupSpecBuilder { // Check that the entry exists const [podName, entryName] = name.split("."); if ( podName === undefined || entryName === undefined || - !(podName in this.#spec.pods) || - !(entryName in this.#spec.pods[podName]!.entries) + !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`); } @@ -400,11 +416,13 @@ export class PODGroupSpecBuilder< }, }; - const baseName = `${name}_notInRange`; + const baseName = customStatementName ?? `${name}_notInRange`; let statementName = baseName; let suffix = 1; - while (statementName in this.#spec.statements) { + while ( + Object.prototype.hasOwnProperty.call(this.#spec.statements, statementName) + ) { statementName = `${baseName}_${suffix++}`; } @@ -420,23 +438,33 @@ export class PODGroupSpecBuilder< public greaterThan< N1 extends keyof AllPODEntries

& string, N2 extends keyof EntriesOfType, EntryType> & string, - >(name1: N1, name2: N2): PODGroupSpecBuilder { + >( + name1: N1, + name2: N2, + customStatementName?: string + ): PODGroupSpecBuilder { // Check that both entries exist const [pod1, entry1] = name1.split("."); const [pod2, entry2] = name2.split("."); if ( pod1 === undefined || entry1 === undefined || - !(pod1 in this.#spec.pods) || - !(entry1 in this.#spec.pods[pod1]!.entries) + !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 || - !(pod2 in this.#spec.pods) || - !(entry2 in this.#spec.pods[pod2]!.entries) + !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`); } @@ -457,11 +485,13 @@ export class PODGroupSpecBuilder< otherEntry: name2, }; - const baseName = `${name1}_${name2}_greaterThan`; + const baseName = customStatementName ?? `${name1}_${name2}_greaterThan`; let statementName = baseName; let suffix = 1; - while (statementName in this.#spec.statements) { + while ( + Object.prototype.hasOwnProperty.call(this.#spec.statements, statementName) + ) { statementName = `${baseName}_${suffix++}`; } @@ -477,23 +507,33 @@ export class PODGroupSpecBuilder< public greaterThanEq< N1 extends keyof AllPODEntries

& string, N2 extends keyof EntriesOfType, EntryType> & string, - >(name1: N1, name2: N2): PODGroupSpecBuilder { + >( + name1: N1, + name2: N2, + customStatementName?: string + ): PODGroupSpecBuilder { // Check that both entries exist const [pod1, entry1] = name1.split("."); const [pod2, entry2] = name2.split("."); if ( pod1 === undefined || entry1 === undefined || - !(pod1 in this.#spec.pods) || - !(entry1 in this.#spec.pods[pod1]!.entries) + !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 || - !(pod2 in this.#spec.pods) || - !(entry2 in this.#spec.pods[pod2]!.entries) + !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`); } @@ -514,11 +554,13 @@ export class PODGroupSpecBuilder< otherEntry: name2, }; - const baseName = `${name1}_${name2}_greaterThanEq`; + const baseName = customStatementName ?? `${name1}_${name2}_greaterThanEq`; let statementName = baseName; let suffix = 1; - while (statementName in this.#spec.statements) { + while ( + Object.prototype.hasOwnProperty.call(this.#spec.statements, statementName) + ) { statementName = `${baseName}_${suffix++}`; } @@ -534,23 +576,33 @@ export class PODGroupSpecBuilder< public lessThan< N1 extends keyof AllPODEntries

& string, N2 extends keyof EntriesOfType, EntryType> & string, - >(name1: N1, name2: N2): PODGroupSpecBuilder { + >( + name1: N1, + name2: N2, + customStatementName?: string + ): PODGroupSpecBuilder { // Check that both entries exist const [pod1, entry1] = name1.split("."); const [pod2, entry2] = name2.split("."); if ( pod1 === undefined || entry1 === undefined || - !(pod1 in this.#spec.pods) || - !(entry1 in this.#spec.pods[pod1]!.entries) + !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 || - !(pod2 in this.#spec.pods) || - !(entry2 in this.#spec.pods[pod2]!.entries) + !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`); } @@ -571,11 +623,13 @@ export class PODGroupSpecBuilder< otherEntry: name2, }; - const baseName = `${name1}_${name2}_lessThan`; + const baseName = customStatementName ?? `${name1}_${name2}_lessThan`; let statementName = baseName; let suffix = 1; - while (statementName in this.#spec.statements) { + while ( + Object.prototype.hasOwnProperty.call(this.#spec.statements, statementName) + ) { statementName = `${baseName}_${suffix++}`; } @@ -591,23 +645,33 @@ export class PODGroupSpecBuilder< public lessThanEq< N1 extends keyof AllPODEntries

& string, N2 extends keyof EntriesOfType, EntryType> & string, - >(name1: N1, name2: N2): PODGroupSpecBuilder { + >( + name1: N1, + name2: N2, + customStatementName?: string + ): PODGroupSpecBuilder { // Check that both entries exist const [pod1, entry1] = name1.split("."); const [pod2, entry2] = name2.split("."); if ( pod1 === undefined || entry1 === undefined || - !(pod1 in this.#spec.pods) || - !(entry1 in this.#spec.pods[pod1]!.entries) + !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 || - !(pod2 in this.#spec.pods) || - !(entry2 in this.#spec.pods[pod2]!.entries) + !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`); } @@ -628,11 +692,13 @@ export class PODGroupSpecBuilder< otherEntry: name2, }; - const baseName = `${name1}_${name2}_lessThanEq`; + const baseName = customStatementName ?? `${name1}_${name2}_lessThanEq`; let statementName = baseName; let suffix = 1; - while (statementName in this.#spec.statements) { + while ( + Object.prototype.hasOwnProperty.call(this.#spec.statements, statementName) + ) { statementName = `${baseName}_${suffix++}`; } @@ -648,23 +714,33 @@ export class PODGroupSpecBuilder< public equalsEntry< N1 extends keyof AllPODEntries

& string, N2 extends keyof EntriesOfType, EntryType> & string, - >(name1: N1, name2: N2): PODGroupSpecBuilder { + >( + name1: N1, + name2: N2, + customStatementName?: string + ): PODGroupSpecBuilder { // Check that both entries exist const [pod1, entry1] = name1.split("."); const [pod2, entry2] = name2.split("."); if ( pod1 === undefined || entry1 === undefined || - !(pod1 in this.#spec.pods) || - !(entry1 in this.#spec.pods[pod1]!.entries) + !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 || - !(pod2 in this.#spec.pods) || - !(entry2 in this.#spec.pods[pod2]!.entries) + !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`); } @@ -685,11 +761,13 @@ export class PODGroupSpecBuilder< otherEntry: name2, }; - const baseName = `${name1}_${name2}_equalsEntry`; + const baseName = customStatementName ?? `${name1}_${name2}_equalsEntry`; let statementName = baseName; let suffix = 1; - while (statementName in this.#spec.statements) { + while ( + Object.prototype.hasOwnProperty.call(this.#spec.statements, statementName) + ) { statementName = `${baseName}_${suffix++}`; } @@ -705,23 +783,33 @@ export class PODGroupSpecBuilder< public notEqualsEntry< N1 extends keyof AllPODEntries

& string, N2 extends keyof EntriesOfType, EntryType> & string, - >(name1: N1, name2: N2): PODGroupSpecBuilder { + >( + name1: N1, + name2: N2, + customStatementName?: string + ): PODGroupSpecBuilder { // Check that both entries exist const [pod1, entry1] = name1.split("."); const [pod2, entry2] = name2.split("."); if ( pod1 === undefined || entry1 === undefined || - !(pod1 in this.#spec.pods) || - !(entry1 in this.#spec.pods[pod1]!.entries) + !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 || - !(pod2 in this.#spec.pods) || - !(entry2 in this.#spec.pods[pod2]!.entries) + !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`); } @@ -742,11 +830,13 @@ export class PODGroupSpecBuilder< otherEntry: name2, }; - const baseName = `${name1}_${name2}_notEqualsEntry`; + const baseName = customStatementName ?? `${name1}_${name2}_notEqualsEntry`; let statementName = baseName; let suffix = 1; - while (statementName in this.#spec.statements) { + while ( + Object.prototype.hasOwnProperty.call(this.#spec.statements, statementName) + ) { statementName = `${baseName}_${suffix++}`; } diff --git a/packages/podspec/src/builders/pod.ts b/packages/podspec/src/builders/pod.ts index fe97238..bd8bf22 100644 --- a/packages/podspec/src/builders/pod.ts +++ b/packages/podspec/src/builders/pod.ts @@ -151,7 +151,7 @@ export class PODSpecBuilder< V extends PODValueType, NewEntries extends AddEntry, >(key: Exclude, type: V): PODSpecBuilder { - if (key in this.#spec.entries) { + if (Object.prototype.hasOwnProperty.call(this.#spec.entries, key)) { throw new Error(`Entry "${key}" already exists`); } @@ -305,7 +305,8 @@ export class PODSpecBuilder< ? PODValueTypeFromTypeName< (E & VirtualEntries)[N[0] & keyof (E & VirtualEntries)] >[] - : PODValueTupleForNamedEntries[] + : PODValueTupleForNamedEntries[], + customStatementName?: string ): PODSpecBuilder< E, S & { @@ -327,7 +328,7 @@ export class PODSpecBuilder< }; for (const name of names) { - if (!(name in allEntries)) { + if (!Object.prototype.hasOwnProperty.call(allEntries, name)) { throw new Error(`Entry "${name}" does not exist`); } } @@ -351,11 +352,13 @@ export class PODSpecBuilder< isMemberOf: convertValuesToStringTuples(names, values, allEntries), }; - const baseName = `${names.join("_")}_isMemberOf`; + const baseName = customStatementName ?? `${names.join("_")}_isMemberOf`; let statementName = baseName; let suffix = 1; - while (statementName in this.#spec.statements) { + while ( + Object.prototype.hasOwnProperty.call(this.#spec.statements, statementName) + ) { statementName = `${baseName}_${suffix++}`; } @@ -389,14 +392,15 @@ export class PODSpecBuilder< ? PODValueTypeFromTypeName< (E & VirtualEntries)[N[0] & keyof (E & VirtualEntries)] >[] - : PODValueTupleForNamedEntries[] + : PODValueTupleForNamedEntries[], + customStatementName?: string ): PODSpecBuilder< E, S & { [K in StatementName]: IsNotMemberOf } > { // Check that all names exist in entries for (const name of names) { - if (!(name in this.#spec.entries)) { + if (!Object.prototype.hasOwnProperty.call(this.#spec.entries, name)) { throw new Error(`Entry "${name}" does not exist`); } } @@ -413,7 +417,7 @@ export class PODSpecBuilder< }; for (const name of names) { - if (!(name in allEntries)) { + if (!Object.prototype.hasOwnProperty.call(allEntries, name)) { throw new Error(`Entry "${name}" does not exist`); } } @@ -437,11 +441,13 @@ export class PODSpecBuilder< isNotMemberOf: convertValuesToStringTuples(names, values, allEntries), }; - const baseName = `${names.join("_")}_isNotMemberOf`; + const baseName = customStatementName ?? `${names.join("_")}_isNotMemberOf`; let statementName = baseName; let suffix = 1; - while (statementName in this.#spec.statements) { + while ( + Object.prototype.hasOwnProperty.call(this.#spec.statements, statementName) + ) { statementName = `${baseName}_${suffix++}`; } @@ -468,13 +474,17 @@ export class PODSpecBuilder< range: { min: E[N] extends "date" ? Date : bigint; max: E[N] extends "date" ? Date : bigint; - } + }, + customStatementName?: string ): PODSpecBuilder< E, S & { [K in StatementName<[N & string], "inRange", S>]: InRange } > { // Check that the entry exists - if (!(name in this.#spec.entries) && !(name in virtualEntries)) { + if ( + !Object.prototype.hasOwnProperty.call(this.#spec.entries, name) && + !Object.prototype.hasOwnProperty.call(virtualEntries, name) + ) { throw new Error(`Entry "${name}" does not exist`); } @@ -524,11 +534,13 @@ export class PODSpecBuilder< }, }; - const baseName = `${name}_inRange`; + const baseName = customStatementName ?? `${name}_inRange`; let statementName = baseName; let suffix = 1; - while (statementName in this.#spec.statements) { + while ( + Object.prototype.hasOwnProperty.call(this.#spec.statements, statementName) + ) { statementName = `${baseName}_${suffix++}`; } @@ -555,7 +567,8 @@ export class PODSpecBuilder< range: { min: E[N] extends "date" ? Date : bigint; max: E[N] extends "date" ? Date : bigint; - } + }, + customStatementName?: string ): PODSpecBuilder< E, S & { @@ -563,7 +576,10 @@ export class PODSpecBuilder< } > { // Check that the entry exists - if (!(name in this.#spec.entries) && !(name in virtualEntries)) { + if ( + !Object.prototype.hasOwnProperty.call(this.#spec.entries, name) && + !Object.prototype.hasOwnProperty.call(virtualEntries, name) + ) { throw new Error(`Entry "${name}" does not exist`); } @@ -614,11 +630,13 @@ export class PODSpecBuilder< }, }; - const baseName = `${name}_notInRange`; + const baseName = customStatementName ?? `${name}_notInRange`; let statementName = baseName; let suffix = 1; - while (statementName in this.#spec.statements) { + while ( + Object.prototype.hasOwnProperty.call(this.#spec.statements, statementName) + ) { statementName = `${baseName}_${suffix++}`; } @@ -640,7 +658,8 @@ export class PODSpecBuilder< string, >( name1: N1, - name2: Exclude + name2: Exclude, + customStatementName?: string ): PODSpecBuilder< E, S & { @@ -648,10 +667,16 @@ export class PODSpecBuilder< } > { // Check that both names exist in entries - if (!(name1 in this.#spec.entries) && !(name1 in virtualEntries)) { + if ( + !Object.prototype.hasOwnProperty.call(this.#spec.entries, name1) && + !Object.prototype.hasOwnProperty.call(virtualEntries, name1) + ) { throw new Error(`Entry "${name1}" does not exist`); } - if (!(name2 in this.#spec.entries) && !(name2 in virtualEntries)) { + 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 as string) === (name2 as string)) { @@ -667,11 +692,13 @@ export class PODSpecBuilder< otherEntry: name2, } satisfies EqualsEntry; - const baseName = `${name1}_${name2}_equalsEntry`; + const baseName = customStatementName ?? `${name1}_${name2}_equalsEntry`; let statementName = baseName; let suffix = 1; - while (statementName in this.#spec.statements) { + while ( + Object.prototype.hasOwnProperty.call(this.#spec.statements, statementName) + ) { statementName = `${baseName}_${suffix++}`; } @@ -693,7 +720,8 @@ export class PODSpecBuilder< string, >( name1: N1, - name2: Exclude + name2: Exclude, + customStatementName?: string ): PODSpecBuilder< E, S & { @@ -705,10 +733,16 @@ export class PODSpecBuilder< } > { // Check that both names exist in entries - if (!(name1 in this.#spec.entries) && !(name1 in virtualEntries)) { + if ( + !Object.prototype.hasOwnProperty.call(this.#spec.entries, name1) && + !Object.prototype.hasOwnProperty.call(virtualEntries, name1) + ) { throw new Error(`Entry "${name1}" does not exist`); } - if (!(name2 in this.#spec.entries) && !(name2 in virtualEntries)) { + 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 as string) === (name2 as string)) { @@ -724,11 +758,13 @@ export class PODSpecBuilder< otherEntry: name2, } satisfies NotEqualsEntry; - const baseName = `${name1}_${name2}_notEqualsEntry`; + const baseName = customStatementName ?? `${name1}_${name2}_notEqualsEntry`; let statementName = baseName; let suffix = 1; - while (statementName in this.#spec.statements) { + while ( + Object.prototype.hasOwnProperty.call(this.#spec.statements, statementName) + ) { statementName = `${baseName}_${suffix++}`; } @@ -750,7 +786,8 @@ export class PODSpecBuilder< string, >( name1: N1, - name2: Exclude + name2: Exclude, + customStatementName?: string ): PODSpecBuilder< E, S & { @@ -758,10 +795,16 @@ export class PODSpecBuilder< } > { // Check that both names exist in entries - if (!(name1 in this.#spec.entries) && !(name1 in virtualEntries)) { + if ( + !Object.prototype.hasOwnProperty.call(this.#spec.entries, name1) && + !Object.prototype.hasOwnProperty.call(virtualEntries, name1) + ) { throw new Error(`Entry "${name1}" does not exist`); } - if (!(name2 in this.#spec.entries) && !(name2 in virtualEntries)) { + 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 as string) === (name2 as string)) { @@ -777,11 +820,13 @@ export class PODSpecBuilder< otherEntry: name2, } satisfies GreaterThan; - const baseName = `${name1}_${name2}_greaterThan`; + const baseName = customStatementName ?? `${name1}_${name2}_greaterThan`; let statementName = baseName; let suffix = 1; - while (statementName in this.#spec.statements) { + while ( + Object.prototype.hasOwnProperty.call(this.#spec.statements, statementName) + ) { statementName = `${baseName}_${suffix++}`; } @@ -803,7 +848,8 @@ export class PODSpecBuilder< string, >( name1: N1, - name2: Exclude + name2: Exclude, + customStatementName?: string ): PODSpecBuilder< E, S & { @@ -815,10 +861,16 @@ export class PODSpecBuilder< } > { // Check that both names exist in entries - if (!(name1 in this.#spec.entries) && !(name1 in virtualEntries)) { + if ( + !Object.prototype.hasOwnProperty.call(this.#spec.entries, name1) && + !Object.prototype.hasOwnProperty.call(virtualEntries, name1) + ) { throw new Error(`Entry "${name1}" does not exist`); } - if (!(name2 in this.#spec.entries) && !(name2 in virtualEntries)) { + 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 as string) === (name2 as string)) { @@ -834,11 +886,13 @@ export class PODSpecBuilder< otherEntry: name2, } satisfies GreaterThanEq; - const baseName = `${name1}_${name2}_greaterThanEq`; + const baseName = customStatementName ?? `${name1}_${name2}_greaterThanEq`; let statementName = baseName; let suffix = 1; - while (statementName in this.#spec.statements) { + while ( + Object.prototype.hasOwnProperty.call(this.#spec.statements, statementName) + ) { statementName = `${baseName}_${suffix++}`; } @@ -860,7 +914,8 @@ export class PODSpecBuilder< string, >( name1: N1, - name2: Exclude + name2: Exclude, + customStatementName?: string ): PODSpecBuilder< E, S & { @@ -868,10 +923,16 @@ export class PODSpecBuilder< } > { // Check that both names exist in entries - if (!(name1 in this.#spec.entries) && !(name1 in virtualEntries)) { + if ( + !Object.prototype.hasOwnProperty.call(this.#spec.entries, name1) && + !Object.prototype.hasOwnProperty.call(virtualEntries, name1) + ) { throw new Error(`Entry "${name1}" does not exist`); } - if (!(name2 in this.#spec.entries) && !(name2 in virtualEntries)) { + 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 as string) === (name2 as string)) { @@ -887,11 +948,13 @@ export class PODSpecBuilder< otherEntry: name2, } satisfies LessThan; - const baseName = `${name1}_${name2}_lessThan`; + const baseName = customStatementName ?? `${name1}_${name2}_lessThan`; let statementName = baseName; let suffix = 1; - while (statementName in this.#spec.statements) { + while ( + Object.prototype.hasOwnProperty.call(this.#spec.statements, statementName) + ) { statementName = `${baseName}_${suffix++}`; } @@ -913,7 +976,8 @@ export class PODSpecBuilder< string, >( name1: N1, - name2: Exclude + name2: Exclude, + customStatementName?: string ): PODSpecBuilder< E, S & { @@ -921,10 +985,16 @@ export class PODSpecBuilder< } > { // Check that both names exist in entries - if (!(name1 in this.#spec.entries) && !(name1 in virtualEntries)) { + if ( + !Object.prototype.hasOwnProperty.call(this.#spec.entries, name1) && + !Object.prototype.hasOwnProperty.call(virtualEntries, name1) + ) { throw new Error(`Entry "${name1}" does not exist`); } - if (!(name2 in this.#spec.entries) && !(name2 in virtualEntries)) { + 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 as string) === (name2 as string)) { @@ -940,11 +1010,13 @@ export class PODSpecBuilder< otherEntry: name2, } satisfies LessThanEq; - const baseName = `${name1}_${name2}_lessThanEq`; + const baseName = customStatementName ?? `${name1}_${name2}_lessThanEq`; let statementName = baseName; let suffix = 1; - while (statementName in this.#spec.statements) { + while ( + Object.prototype.hasOwnProperty.call(this.#spec.statements, statementName) + ) { statementName = `${baseName}_${suffix++}`; } diff --git a/packages/podspec/src/index.ts b/packages/podspec/src/index.ts index 3782e81..2c63b74 100644 --- a/packages/podspec/src/index.ts +++ b/packages/podspec/src/index.ts @@ -1,11 +1,4 @@ -import { type PODSpec, PODSpecBuilder } from "./builders/pod.js"; import { type PODGroupSpec, PODGroupSpecBuilder } from "./builders/group.js"; -import { validatePOD } from "./processors/validate.js"; +import { type PODSpec, PODSpecBuilder } from "./builders/pod.js"; -export { - type PODSpec, - PODSpecBuilder, - type PODGroupSpec, - PODGroupSpecBuilder, - validatePOD, -}; +export { type PODSpec, PODSpecBuilder, type PODGroupSpec, PODGroupSpecBuilder }; diff --git a/packages/podspec/src/processors/validate/EntrySource.ts b/packages/podspec/src/processors/validate/EntrySource.ts index c10f700..af3e216 100644 --- a/packages/podspec/src/processors/validate/EntrySource.ts +++ b/packages/podspec/src/processors/validate/EntrySource.ts @@ -1,18 +1,18 @@ 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 type { NamedPODSpecs, PODGroupSpec } from "../../builders/group.js"; +import type { ValidateOptions } from "../validate.js"; import { + IssueCode, type ValidationBaseIssue, type ValidationMissingEntryIssue, type ValidationMissingPodIssue, type ValidationTypeMismatchIssue, type ValidationUnexpectedInputEntryIssue, type ValidationUnexpectedInputPodIssue, - IssueCode, } from "./issues.js"; -import type { ValidateOptions } from "../validate.js"; export interface EntrySource { getEntry(entryName: string): PODValue | undefined; @@ -29,7 +29,7 @@ function auditEntries( const issues = []; for (const [key, entryType] of Object.entries(spec.entries)) { - if (!(key in podEntries)) { + if (!Object.prototype.hasOwnProperty.call(podEntries, key)) { issues.push({ code: IssueCode.missing_entry, path: [...path, key], @@ -52,8 +52,8 @@ function auditEntries( } if (strict) { - for (const key in podEntries) { - if (!(key in spec.entries)) { + 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], @@ -151,7 +151,9 @@ export class EntrySourcePodGroupSpec implements EntrySource { if (options.strict) { for (const podName of Object.keys(this.pods)) { - if (!(podName in this.podGroupSpec.pods)) { + if ( + !Object.prototype.hasOwnProperty.call(this.podGroupSpec.pods, podName) + ) { issues.push({ code: IssueCode.unexpected_input_pod, path: [...path, podName], diff --git a/packages/podspec/test/builders/PODSpecBuilder.spec.ts b/packages/podspec/test/builders/PODSpecBuilder.spec.ts index 118641c..ad0c878 100644 --- a/packages/podspec/test/builders/PODSpecBuilder.spec.ts +++ b/packages/podspec/test/builders/PODSpecBuilder.spec.ts @@ -1,7 +1,32 @@ -import { describe, it, expect } from "vitest"; +import { fc, test } from "@fast-check/vitest"; +import { describe, expect, it } from "vitest"; +import type { PODValueType } from "../../src/builders/types/entries.js"; 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 + - [ ] 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 +*/ + describe("PODSpecBuilder", () => { it("should be a test", () => { expect(true).toBe(true); @@ -112,3 +137,134 @@ describe("PODSpecBuilder", () => { }); }); }); + +describe("PODSpecBuilder - Property tests", () => { + // Arbitraries for generating test data + const validEntryName = fc + .string() + .filter((s) => /^[a-zA-Z][a-zA-Z0-9_]*$/.test(s)) + .filter((s) => s.length <= 32); + + const podType = fc.constantFrom( + "string", + "int", + "boolean", + "date", + "eddsa_pubkey" + ); + + const entryConfigs = fc + .uniqueArray(validEntryName, { + minLength: 1, + maxLength: 10, + }) + .map((names) => + names.map((name) => ({ + name, + type: fc.sample(podType, 1)[0] as PODValueType, + })) + ); + + // Test that adding entries is commutative + test("should be commutative when adding entries", () => { + fc.assert( + fc.property(entryConfigs, (entries) => { + // Add entries in original order + console.log(entries); + const builder1 = entries.reduce( + (b, { name, type }) => b.entry(name, type), + PODSpecBuilder.create() + ); + + // Add entries in reverse order + const builder2 = [...entries] + .reverse() + .reduce( + (b, { name, type }) => b.entry(name, type), + PODSpecBuilder.create() + ); + + // The resulting specs should be equivalent + expect(builder1.spec()).toEqual(builder2.spec()); + }) + ); + }); + + // Test that picking and then omitting entries is consistent + test("should maintain consistency when picking and omitting entries", () => { + fc.assert( + fc.property( + entryConfigs, + fc.array(fc.nat(), { minLength: 1, maxLength: 5 }), // indices to pick + (entries, pickIndices) => { + const builder = entries.reduce( + (b, { name, type }) => b.entry(name, type), + PODSpecBuilder.create() + ); + const entryNames = entries.map((e) => e.name); + const pickedNames = pickIndices + .map((i) => entryNames[i % entryNames.length]) + .filter((name): name is string => name !== undefined); + + // @ts-expect-error ignore this + const picked = builder.pickEntries(pickedNames); + // @ts-expect-error ignore this + const omitted = picked.omitEntries(pickedNames); + + // Omitting all picked entries should result in empty entries + expect(Object.keys(omitted.spec().entries)).toHaveLength(0); + } + ) + ); + }); + + // Test that range checks maintain valid bounds + test("should maintain valid bounds for range checks", () => { + const dateArb = fc.date({ + min: new Date(0), + max: new Date(2100, 0, 1), + }); + + fc.assert( + fc.property(validEntryName, dateArb, dateArb, (name, date1, date2) => { + const min = date1 < date2 ? date1 : date2; + const max = date1 < date2 ? date2 : date1; + + const builder = PODSpecBuilder.create() + .entry(name, "date") + .inRange(name, { min, max }); + + const spec = builder.spec(); + const statement = Object.values(spec.statements)[0]; + + // Range bounds should be ordered correctly in the spec + if (statement?.type === "inRange") { + const minTime = BigInt(statement.inRange.min); + const maxTime = BigInt(statement.inRange.max); + expect(minTime).toBeLessThanOrEqual(maxTime); + } + }) + ); + }); + + // Test that custom statement names are always unique + test("should generate unique statement names", () => { + fc.assert( + fc.property(entryConfigs, fc.string(), (entries, customName) => { + const builder = entries.reduce( + (b, { name }) => + b + .entry(name, "int") + .inRange(name, { min: 0n, max: 10n }, customName), + PODSpecBuilder.create() + ); + + const spec = builder.spec(); + const statementNames = Object.keys(spec.statements); + const uniqueNames = new Set(statementNames); + + expect(statementNames.length).toBe(uniqueNames.size); + }) + ); + }); +}); diff --git a/packages/podspec/test/processors/validator/validator.spec.ts b/packages/podspec/test/processors/validator/validator.spec.ts index 5da76ad..66dc0ef 100644 --- a/packages/podspec/test/processors/validator/validator.spec.ts +++ b/packages/podspec/test/processors/validator/validator.spec.ts @@ -1,22 +1,23 @@ import { - type PODEntries, POD, - type PODValue, - type PODStringValue, + type PODEntries, type PODIntValue, + type PODStringValue, + type PODValue, } from "@pcd/pod"; -import { describe, it, expect, assert } from "vitest"; -import { PODSpecBuilder, validatePOD } from "../../../src/index.js"; +import { assert, describe, expect, it } from "vitest"; +import { PODSpecBuilder } from "../../../src/index.js"; +import { validate } from "../../../src/processors/validate.js"; +import { generateKeyPair } from "../../utils.js"; describe("validator", () => { it("should be a test", () => { expect(true).toBe(true); }); - const privKey = - "f72c3def0a54280ded2990a66fabcf717130c6f2bb595004658ec77774b98924"; + const { privateKey } = generateKeyPair(); - const signPOD = (entries: PODEntries) => POD.sign(entries, privKey); + const signPOD = (entries: PODEntries) => POD.sign(entries, privateKey); it("validatePOD", () => { const myPOD = signPOD({ @@ -28,9 +29,9 @@ describe("validator", () => { .isMemberOf(["foo"], ["foo", "bar"]); // This should pass because the entry "foo" is in the list ["foo", "bar"] - expect(validatePOD(myPOD, myPodSpecBuilder.spec()).isValid).toBe(true); + expect(validate(myPodSpecBuilder.spec()).check(myPOD)).toBe(true); - const result = validatePOD(myPOD, myPodSpecBuilder.spec()); + const result = validate(myPodSpecBuilder.spec()).validate(myPOD); if (result.isValid) { const pod = result.value; // After validation, the entries are strongly typed @@ -48,25 +49,23 @@ describe("validator", () => { // This should fail because the entry "foo" is not in the list ["baz", "quux"] const secondBuilder = myPodSpecBuilder.isMemberOf(["foo"], ["baz", "quux"]); - expect(validatePOD(myPOD, secondBuilder.spec()).isValid).toBe(false); + expect(validate(secondBuilder.spec()).check(myPOD)).toBe(false); // If we omit the new statement, it should pass expect( - validatePOD( - myPOD, - secondBuilder.omitStatements(["foo_isMemberOf_1"]).spec() - ).isValid + validate(secondBuilder.omitStatements(["foo_isMemberOf_1"]).spec()).check( + myPOD + ) ).toBe(true); { - const result = validatePOD( - myPOD, + const result = validate( 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; From 9923e16d79020ac99f3e0537ede2b907ab501180 Mon Sep 17 00:00:00 2001 From: Rob Knight Date: Mon, 3 Feb 2025 19:35:46 +0100 Subject: [PATCH 15/20] more wip --- .vscode/extensions.json | 2 +- .../app-connector/src/connection_state.ts | 2 +- packages/eslint-config/eslint.base.config.mjs | 24 +- packages/podspec/src/builders/group.ts | 80 +-- packages/podspec/src/builders/pod.ts | 197 +++++--- .../podspec/src/builders/types/statements.ts | 39 +- packages/podspec/src/generated/podspec.ts | 476 ++++++++++++++---- packages/podspec/src/processors/validate.ts | 16 +- .../validate/checks/checkEqualsEntry.ts | 21 +- .../validate/checks/checkGreaterThan.ts | 23 +- .../validate/checks/checkGreaterThanEq.ts | 23 +- .../validate/checks/checkInRange.ts | 4 +- .../validate/checks/checkLessThan.ts | 23 +- .../validate/checks/checkLessThanEq.ts | 23 +- .../validate/checks/checkNotEqualsEntry.ts | 21 +- .../validate/checks/checkNotInRange.ts | 4 +- .../podspec/src/processors/validate/issues.ts | 2 +- packages/podspec/src/shared/types.ts | 5 + .../test/builders/PODGroupSpecBuilder.spec.ts | 225 ++++++++- .../test/builders/PODSpecBuilder.spec.ts | 47 +- 20 files changed, 921 insertions(+), 336 deletions(-) create mode 100644 packages/podspec/src/shared/types.ts diff --git a/.vscode/extensions.json b/.vscode/extensions.json index 699ed73..2b16bbc 100644 --- a/.vscode/extensions.json +++ b/.vscode/extensions.json @@ -1,3 +1,3 @@ { - "recommendations": ["biomejs.biome"] + "recommendations": ["biomejs.biome", "dbaeumer.vscode-eslint"] } 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 4ae4dff..bec5d64 100644 --- a/packages/eslint-config/eslint.base.config.mjs +++ b/packages/eslint-config/eslint.base.config.mjs @@ -76,8 +76,28 @@ export default tseslint.config( "@typescript-eslint/no-import-type-side-effects": "error", "no-unexpected-multiline": "off", "no-restricted-globals": ["error", "origin"], - "no-restricted-syntax": ["error", "BinaryExpression[operator='in']"], - "guard-for-in": "error", + "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/src/builders/group.ts b/packages/podspec/src/builders/group.ts index 9cbb9c8..c8e1b37 100644 --- a/packages/podspec/src/builders/group.ts +++ b/packages/podspec/src/builders/group.ts @@ -6,7 +6,7 @@ import { POD_INT_MIN, checkPODName, } from "@pcd/pod"; -import { type PODSpec, virtualEntries } from "./pod.js"; +import { type PODSpec, PODSpecBuilder, virtualEntries } from "./pod.js"; import { convertValuesToStringTuples, supportsRangeChecks, @@ -22,6 +22,7 @@ import type { VirtualEntries, } from "./types/entries.js"; import type { + EntriesWithRangeChecks, EqualsEntry, GreaterThan, GreaterThanEq, @@ -38,15 +39,12 @@ import type { export type NamedPODSpecs = Record>; -// @TODO add group constraints, where instead of extending EntryListSpec, -// we have some kind of group entry list, with each entry name prefixed -// by the name of the POD it belongs to. - -// type GroupIsMemberOf = { -// entry: N[number]; -// type: "isMemberOf"; -// isMemberOf: N[number]; -// }; +/** + @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; @@ -96,10 +94,10 @@ export class PODGroupSpecBuilder< } // eslint-disable-next-line @typescript-eslint/no-empty-object-type - public static create(): PODGroupSpecBuilder<{}, {}> { + public static create(): PODGroupSpecBuilder { return new PODGroupSpecBuilder({ - pods: {}, - statements: {}, + pods: {} as NamedPODSpecs, + statements: {} as StatementMap, }); } @@ -261,8 +259,16 @@ export class PODGroupSpecBuilder< >( name: N, range: { - min: AllPODEntries

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

[N] extends "date" ? Date : bigint; + 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?: string ): PODGroupSpecBuilder { @@ -312,7 +318,7 @@ export class PODGroupSpecBuilder< } const statement: InRange, N> = { - entry: name, + entries: [name], type: "inRange", inRange: { min: @@ -402,7 +408,7 @@ export class PODGroupSpecBuilder< } const statement: NotInRange, N> = { - entry: name, + entries: [name], type: "notInRange", notInRange: { min: @@ -480,9 +486,8 @@ export class PODGroupSpecBuilder< } const statement: GreaterThan, N1, N2> = { - entry: name1, + entries: [name1, name2], type: "greaterThan", - otherEntry: name2, }; const baseName = customStatementName ?? `${name1}_${name2}_greaterThan`; @@ -549,9 +554,8 @@ export class PODGroupSpecBuilder< } const statement: GreaterThanEq, N1, N2> = { - entry: name1, + entries: [name1, name2], type: "greaterThanEq", - otherEntry: name2, }; const baseName = customStatementName ?? `${name1}_${name2}_greaterThanEq`; @@ -618,9 +622,8 @@ export class PODGroupSpecBuilder< } const statement: LessThan, N1, N2> = { - entry: name1, + entries: [name1, name2], type: "lessThan", - otherEntry: name2, }; const baseName = customStatementName ?? `${name1}_${name2}_lessThan`; @@ -687,9 +690,8 @@ export class PODGroupSpecBuilder< } const statement: LessThanEq, N1, N2> = { - entry: name1, + entries: [name1, name2], type: "lessThanEq", - otherEntry: name2, }; const baseName = customStatementName ?? `${name1}_${name2}_lessThanEq`; @@ -756,9 +758,8 @@ export class PODGroupSpecBuilder< } const statement: EqualsEntry, N1, N2> = { - entry: name1, + entries: [name1, name2], type: "equalsEntry", - otherEntry: name2, }; const baseName = customStatementName ?? `${name1}_${name2}_equalsEntry`; @@ -825,9 +826,8 @@ export class PODGroupSpecBuilder< } const statement: NotEqualsEntry, N1, N2> = { - entry: name1, + entries: [name1, name2], type: "notEqualsEntry", - otherEntry: name2, }; const baseName = customStatementName ?? `${name1}_${name2}_notEqualsEntry`; @@ -849,3 +849,25 @@ export class PODGroupSpecBuilder< }); } } + +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()); + // .inRange("foo.my_int", { min: 0n, max: 10n }); + + const _builder2 = PODGroupSpecBuilder.create() + .pod("mystery" as string, pod.spec()) + .pod("mystery2" as string, pod2.spec()); + }); + }); +} diff --git a/packages/podspec/src/builders/pod.ts b/packages/podspec/src/builders/pod.ts index bd8bf22..b3c2f58 100644 --- a/packages/podspec/src/builders/pod.ts +++ b/packages/podspec/src/builders/pod.ts @@ -6,6 +6,7 @@ import { checkPODName, } from "@pcd/pod"; import type { IsJsonSafe } from "../shared/jsonSafe.js"; +import type { IsLiteral } from "../shared/types.js"; import { canonicalizeJSON, convertValuesToStringTuples, @@ -23,6 +24,7 @@ import type { VirtualEntries, } from "./types/entries.js"; import type { + EntriesWithRangeChecks, EqualsEntry, GreaterThan, GreaterThanEq, @@ -35,7 +37,6 @@ import type { NotInRange, StatementMap, StatementName, - SupportsRangeChecks, } from "./types/statements.js"; /** @@ -54,6 +55,10 @@ import type { - [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 + - [ ] untyped entries? + - [ ] optional entries? + - [ ] solve the problem whereby some methods are incorrectly typed if we've + added loosely-typed entries (this seems to be a problem with omit/pick?) */ export const virtualEntries: VirtualEntries = { @@ -113,6 +118,7 @@ export class PODSpecBuilder< E extends EntryTypes, // eslint-disable-next-line @typescript-eslint/no-empty-object-type S extends StatementMap = {}, + LiteralMode extends boolean = true, > { readonly #spec: PODSpec; @@ -150,7 +156,14 @@ export class PODSpecBuilder< K extends string, V extends PODValueType, NewEntries extends AddEntry, - >(key: Exclude, type: V): PODSpecBuilder { + // If the key is not a string literal, we need to exit literal mode + NewLiteralMode extends boolean = IsLiteral extends false + ? false + : LiteralMode, + >( + key: NewLiteralMode extends true ? Exclude : string, + type: V + ): PODSpecBuilder { if (Object.prototype.hasOwnProperty.call(this.#spec.entries, key)) { throw new Error(`Entry "${key}" already exists`); } @@ -158,7 +171,7 @@ export class PODSpecBuilder< // Will throw if not a valid POD entry name checkPODName(key); - return new PODSpecBuilder({ + return new PODSpecBuilder({ ...this.#spec, entries: { ...this.#spec.entries, @@ -171,7 +184,9 @@ export class PODSpecBuilder< /** * Pick entries by key */ - public pickEntries( + public pickEntries< + K extends (keyof E extends never ? string : keyof E) & string, + >( keys: K[] ): PODSpecBuilder, Concrete>> { return new PODSpecBuilder({ @@ -182,39 +197,17 @@ export class PODSpecBuilder< ) as Pick, statements: Object.fromEntries( Object.entries(this.#spec.statements).filter(([_key, statement]) => { - const statementType = statement.type; - switch (statementType) { - case "isMemberOf": - case "isNotMemberOf": - return (statement.entries as EntryKeys).every((entry) => - keys.includes(entry as K) - ); - case "inRange": - case "notInRange": - return keys.includes(statement.entry as K); - case "equalsEntry": - case "notEqualsEntry": - case "greaterThan": - case "greaterThanEq": - case "lessThan": - case "lessThanEq": - return ( - keys.includes(statement.entry as K) && - keys.includes(statement.otherEntry as K) - ); - - default: - const _exhaustiveCheck: never = statement; - throw new Error( - `Unsupported statement type: ${statementType as string}` - ); - } + return (statement.entries as EntryKeys).every((entry) => + keys.includes(entry as K) + ); }) ) as Concrete>, }); } - public omitEntries( + public omitEntries< + K extends (keyof E extends never ? string : keyof E) & string, + >( keys: K[] ): PODSpecBuilder, Concrete>> { return new PODSpecBuilder({ @@ -226,33 +219,9 @@ export class PODSpecBuilder< ) as Omit, statements: Object.fromEntries( Object.entries(this.#spec.statements).filter(([_key, statement]) => { - const statementType = statement.type; - switch (statementType) { - case "isMemberOf": - case "isNotMemberOf": - return (statement.entries as EntryKeys).every( - (entry) => !keys.includes(entry as K) - ); - case "inRange": - case "notInRange": - return keys.includes(statement.entry as K); - case "equalsEntry": - case "notEqualsEntry": - case "greaterThan": - case "greaterThanEq": - case "lessThan": - case "lessThanEq": - return ( - !keys.includes(statement.entry as K) && - !keys.includes(statement.otherEntry as K) - ); - - default: - const _exhaustiveCheck: never = statement; - throw new Error( - `Unsupported statement type: ${statementType as string}` - ); - } + return (statement.entries as EntryKeys).every( + (entry) => !keys.includes(entry as K) + ); }) ) as Concrete>, }); @@ -468,7 +437,7 @@ export class PODSpecBuilder< * @returns A new PODSpecBuilder with the statement added */ public inRange< - N extends keyof EntriesOfType & string, + N extends keyof EntriesWithRangeChecks & string, >( name: N, range: { @@ -478,7 +447,12 @@ export class PODSpecBuilder< customStatementName?: string ): PODSpecBuilder< E, - S & { [K in StatementName<[N & string], "inRange", S>]: InRange } + S & { + [K in StatementName<[N & string], "inRange", S>]: InRange< + E & VirtualEntries, + N + >; + } > { // Check that the entry exists if ( @@ -519,8 +493,8 @@ export class PODSpecBuilder< throw new Error(`Unsupported entry type: ${name}`); } - const statement: InRange = { - entry: name, + const statement: InRange = { + entries: [name], type: "inRange", inRange: { min: @@ -561,7 +535,7 @@ export class PODSpecBuilder< * @returns A new PODSpecBuilder with the statement added */ public notInRange< - N extends keyof EntriesOfType & string, + N extends keyof EntriesWithRangeChecks & string, >( name: N, range: { @@ -572,7 +546,10 @@ export class PODSpecBuilder< ): PODSpecBuilder< E, S & { - [K in StatementName<[N & string], "notInRange", S>]: NotInRange; + [K in StatementName<[N & string], "notInRange", S>]: NotInRange< + E & VirtualEntries, + N + >; } > { // Check that the entry exists @@ -615,8 +592,8 @@ export class PODSpecBuilder< throw new Error(`Unsupported entry type: ${name}`); } - const statement: NotInRange = { - entry: name, + const statement: NotInRange = { + entries: [name], type: "notInRange", notInRange: { min: @@ -687,9 +664,8 @@ export class PODSpecBuilder< } const statement = { - entry: name1, + entries: [name1, name2], type: "equalsEntry", - otherEntry: name2, } satisfies EqualsEntry; const baseName = customStatementName ?? `${name1}_${name2}_equalsEntry`; @@ -753,9 +729,8 @@ export class PODSpecBuilder< } const statement = { - entry: name1, + entries: [name1, name2], type: "notEqualsEntry", - otherEntry: name2, } satisfies NotEqualsEntry; const baseName = customStatementName ?? `${name1}_${name2}_notEqualsEntry`; @@ -815,9 +790,8 @@ export class PODSpecBuilder< } const statement = { - entry: name1, + entries: [name1, name2], type: "greaterThan", - otherEntry: name2, } satisfies GreaterThan; const baseName = customStatementName ?? `${name1}_${name2}_greaterThan`; @@ -881,9 +855,8 @@ export class PODSpecBuilder< } const statement = { - entry: name1, + entries: [name1, name2], type: "greaterThanEq", - otherEntry: name2, } satisfies GreaterThanEq; const baseName = customStatementName ?? `${name1}_${name2}_greaterThanEq`; @@ -943,9 +916,8 @@ export class PODSpecBuilder< } const statement = { - entry: name1, + entries: [name1, name2], type: "lessThan", - otherEntry: name2, } satisfies LessThan; const baseName = customStatementName ?? `${name1}_${name2}_lessThan`; @@ -1005,9 +977,8 @@ export class PODSpecBuilder< } const statement = { - entry: name1, + entries: [name1, name2], type: "lessThanEq", - otherEntry: name2, } satisfies LessThanEq; const baseName = customStatementName ?? `${name1}_${name2}_lessThanEq`; @@ -1029,3 +1000,71 @@ export class PODSpecBuilder< }); } } + +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", + ]); + + builderWithEntries + // @ts-expect-error entry does not exist + .isMemberOf(["non_existent_entry"], ["foo", "bar"]); + }); + }); +} diff --git a/packages/podspec/src/builders/types/statements.ts b/packages/podspec/src/builders/types/statements.ts index 935e1d3..261938f 100644 --- a/packages/podspec/src/builders/types/statements.ts +++ b/packages/podspec/src/builders/types/statements.ts @@ -1,8 +1,8 @@ import type { - EntryTypes, + EntriesOfType, EntryKeys, + EntryTypes, PODValueTupleForNamedEntries, - EntriesOfType, VirtualEntries, } from "./entries.js"; @@ -42,11 +42,14 @@ export type 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 EntriesOfType & - string, + N extends keyof EntriesWithRangeChecks & string, > = { min: E[N] extends "date" ? Date : bigint; max: E[N] extends "date" ? Date : bigint; @@ -59,20 +62,18 @@ export type RangePersistent = { export type InRange< E extends EntryTypes, - N extends keyof EntriesOfType & - string, + N extends keyof EntriesWithRangeChecks & string, > = { - entry: N; + entries: [entry: N]; type: "inRange"; inRange: RangePersistent; }; export type NotInRange< E extends EntryTypes, - N extends keyof EntriesOfType & - string, + N extends keyof EntriesWithRangeChecks & string, > = { - entry: N; + entries: [entry: N]; type: "notInRange"; notInRange: RangePersistent; }; @@ -82,9 +83,8 @@ export type EqualsEntry< N1 extends keyof (E & VirtualEntries) & string, N2 extends keyof (E & VirtualEntries) & string, > = { - entry: N1; + entries: [entry: N1, otherEntry: N2]; type: "equalsEntry"; - otherEntry: N2; }; export type NotEqualsEntry< @@ -92,9 +92,8 @@ export type NotEqualsEntry< N1 extends keyof (E & VirtualEntries) & string, N2 extends keyof (E & VirtualEntries) & string, > = { - entry: N1; + entries: [entry: N1, otherEntry: N2]; type: "notEqualsEntry"; - otherEntry: N2; }; export type GreaterThan< @@ -102,9 +101,8 @@ export type GreaterThan< N1 extends keyof (E & VirtualEntries) & string, N2 extends keyof (E & VirtualEntries) & string, > = { - entry: N1; + entries: [entry: N1, otherEntry: N2]; type: "greaterThan"; - otherEntry: N2; }; export type GreaterThanEq< @@ -112,9 +110,8 @@ export type GreaterThanEq< N1 extends keyof (E & VirtualEntries) & string, N2 extends keyof (E & VirtualEntries) & string, > = { - entry: N1; + entries: [entry: N1, otherEntry: N2]; type: "greaterThanEq"; - otherEntry: N2; }; export type LessThan< @@ -122,9 +119,8 @@ export type LessThan< N1 extends keyof (E & VirtualEntries) & string, N2 extends keyof (E & VirtualEntries) & string, > = { - entry: N1; + entries: [entry: N1, otherEntry: N2]; type: "lessThan"; - otherEntry: N2; }; export type LessThanEq< @@ -132,9 +128,8 @@ export type LessThanEq< N1 extends keyof (E & VirtualEntries) & string, N2 extends keyof (E & VirtualEntries) & string, > = { - entry: N1; + entries: [entry: N1, otherEntry: N2]; type: "lessThanEq"; - otherEntry: N2; }; export type Statements = diff --git a/packages/podspec/src/generated/podspec.ts b/packages/podspec/src/generated/podspec.ts index 440eb45..02728e8 100644 --- a/packages/podspec/src/generated/podspec.ts +++ b/packages/podspec/src/generated/podspec.ts @@ -11,8 +11,8 @@ export const assertPODSpec = (() => { false === Array.isArray(input.entries) && _io1(input.entries) && "object" === typeof input.statements && - null !== input.statements && - false === Array.isArray(input.statements) && + null !== input.statements && + false === Array.isArray(input.statements) && _io2(input.statements); const _io1 = (input: any): boolean => Object.keys(input).every((key: any) => { @@ -56,43 +56,51 @@ export const assertPODSpec = (() => { elem.every((elem: any) => "string" === typeof elem) ); const _io5 = (input: any): boolean => - "string" === typeof input.entry && + Array.isArray(input.entries) && + input.entries.length === 1 && + "string" === typeof input.entries[0] && "inRange" === input.type && - "object" === typeof input.inRange && - null !== input.inRange && + "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 => - "string" === typeof input.entry && + Array.isArray(input.entries) && + input.entries.length === 1 && + "string" === typeof input.entries[0] && "notInRange" === input.type && - "object" === typeof input.notInRange && - null !== input.notInRange && + "object" === typeof input.notInRange && null !== input.notInRange && _io6(input.notInRange); const _io8 = (input: any): boolean => - "string" === typeof input.entry && - "equalsEntry" === input.type && - "string" === typeof input.otherEntry; + 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 => - "string" === typeof input.entry && - "notEqualsEntry" === input.type && - "string" === typeof input.otherEntry; + 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 => - "string" === typeof input.entry && - "greaterThan" === input.type && - "string" === typeof input.otherEntry; + 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 => - "string" === typeof input.entry && - "greaterThanEq" === input.type && - "string" === typeof input.otherEntry; + 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 => - "string" === typeof input.entry && - "lessThan" === input.type && - "string" === typeof input.otherEntry; + 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 => - "string" === typeof input.entry && - "lessThanEq" === input.type && - "string" === typeof input.otherEntry; + 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); @@ -478,14 +486,46 @@ export const assertPODSpec = (() => { _path: string, _exceptionable: boolean = true ): boolean => - ("string" === typeof input.entry || + (((Array.isArray(input.entries) || __typia_transform__assertGuard._assertGuard( _exceptionable, { method: "typia.createAssert", - path: _path + ".entry", - expected: "string", - value: input.entry, + 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 )) && @@ -554,14 +594,46 @@ export const assertPODSpec = (() => { _path: string, _exceptionable: boolean = true ): boolean => - ("string" === typeof input.entry || + (((Array.isArray(input.entries) || __typia_transform__assertGuard._assertGuard( _exceptionable, { method: "typia.createAssert", - path: _path + ".entry", - expected: "string", - value: input.entry, + 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 )) && @@ -603,36 +675,68 @@ export const assertPODSpec = (() => { _path: string, _exceptionable: boolean = true ): boolean => - ("string" === typeof input.entry || + (((Array.isArray(input.entries) || __typia_transform__assertGuard._assertGuard( _exceptionable, { method: "typia.createAssert", - path: _path + ".entry", - expected: "string", - value: input.entry, + path: _path + ".entries", + expected: "[entry: string, otherEntry: string]", + value: input.entries, }, _errorFactory )) && - ("equalsEntry" === input.type || + (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 + ".type", - expected: '"equalsEntry"', - value: input.type, + path: _path + ".entries", + expected: "[entry: string, otherEntry: string]", + value: input.entries, }, _errorFactory )) && - ("string" === typeof input.otherEntry || + ("equalsEntry" === input.type || __typia_transform__assertGuard._assertGuard( _exceptionable, { method: "typia.createAssert", - path: _path + ".otherEntry", - expected: "string", - value: input.otherEntry, + path: _path + ".type", + expected: '"equalsEntry"', + value: input.type, }, _errorFactory )); @@ -641,36 +745,68 @@ export const assertPODSpec = (() => { _path: string, _exceptionable: boolean = true ): boolean => - ("string" === typeof input.entry || + (((Array.isArray(input.entries) || __typia_transform__assertGuard._assertGuard( _exceptionable, { method: "typia.createAssert", - path: _path + ".entry", - expected: "string", - value: input.entry, + path: _path + ".entries", + expected: "[entry: string, otherEntry: string].o1", + value: input.entries, }, _errorFactory )) && - ("notEqualsEntry" === input.type || + (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 + ".type", - expected: '"notEqualsEntry"', - value: input.type, + path: _path + ".entries", + expected: "[entry: string, otherEntry: string].o1", + value: input.entries, }, _errorFactory )) && - ("string" === typeof input.otherEntry || + ("notEqualsEntry" === input.type || __typia_transform__assertGuard._assertGuard( _exceptionable, { method: "typia.createAssert", - path: _path + ".otherEntry", - expected: "string", - value: input.otherEntry, + path: _path + ".type", + expected: '"notEqualsEntry"', + value: input.type, }, _errorFactory )); @@ -679,36 +815,68 @@ export const assertPODSpec = (() => { _path: string, _exceptionable: boolean = true ): boolean => - ("string" === typeof input.entry || + (((Array.isArray(input.entries) || __typia_transform__assertGuard._assertGuard( _exceptionable, { method: "typia.createAssert", - path: _path + ".entry", - expected: "string", - value: input.entry, + path: _path + ".entries", + expected: "[entry: string, otherEntry: string].o2", + value: input.entries, }, _errorFactory )) && - ("greaterThan" === input.type || + (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 + ".type", - expected: '"greaterThan"', - value: input.type, + path: _path + ".entries", + expected: "[entry: string, otherEntry: string].o2", + value: input.entries, }, _errorFactory )) && - ("string" === typeof input.otherEntry || + ("greaterThan" === input.type || __typia_transform__assertGuard._assertGuard( _exceptionable, { method: "typia.createAssert", - path: _path + ".otherEntry", - expected: "string", - value: input.otherEntry, + path: _path + ".type", + expected: '"greaterThan"', + value: input.type, }, _errorFactory )); @@ -717,36 +885,68 @@ export const assertPODSpec = (() => { _path: string, _exceptionable: boolean = true ): boolean => - ("string" === typeof input.entry || + (((Array.isArray(input.entries) || __typia_transform__assertGuard._assertGuard( _exceptionable, { method: "typia.createAssert", - path: _path + ".entry", - expected: "string", - value: input.entry, + path: _path + ".entries", + expected: "[entry: string, otherEntry: string].o3", + value: input.entries, }, _errorFactory )) && - ("greaterThanEq" === input.type || + (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 + ".type", - expected: '"greaterThanEq"', - value: input.type, + path: _path + ".entries", + expected: "[entry: string, otherEntry: string].o3", + value: input.entries, }, _errorFactory )) && - ("string" === typeof input.otherEntry || + ("greaterThanEq" === input.type || __typia_transform__assertGuard._assertGuard( _exceptionable, { method: "typia.createAssert", - path: _path + ".otherEntry", - expected: "string", - value: input.otherEntry, + path: _path + ".type", + expected: '"greaterThanEq"', + value: input.type, }, _errorFactory )); @@ -755,36 +955,68 @@ export const assertPODSpec = (() => { _path: string, _exceptionable: boolean = true ): boolean => - ("string" === typeof input.entry || + (((Array.isArray(input.entries) || __typia_transform__assertGuard._assertGuard( _exceptionable, { method: "typia.createAssert", - path: _path + ".entry", - expected: "string", - value: input.entry, + path: _path + ".entries", + expected: "[entry: string, otherEntry: string].o4", + value: input.entries, }, _errorFactory )) && - ("lessThan" === input.type || + (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 + ".type", - expected: '"lessThan"', - value: input.type, + path: _path + ".entries", + expected: "[entry: string, otherEntry: string].o4", + value: input.entries, }, _errorFactory )) && - ("string" === typeof input.otherEntry || + ("lessThan" === input.type || __typia_transform__assertGuard._assertGuard( _exceptionable, { method: "typia.createAssert", - path: _path + ".otherEntry", - expected: "string", - value: input.otherEntry, + path: _path + ".type", + expected: '"lessThan"', + value: input.type, }, _errorFactory )); @@ -793,36 +1025,68 @@ export const assertPODSpec = (() => { _path: string, _exceptionable: boolean = true ): boolean => - ("string" === typeof input.entry || + (((Array.isArray(input.entries) || __typia_transform__assertGuard._assertGuard( _exceptionable, { method: "typia.createAssert", - path: _path + ".entry", - expected: "string", - value: input.entry, + path: _path + ".entries", + expected: "[entry: string, otherEntry: string].o5", + value: input.entries, }, _errorFactory )) && - ("lessThanEq" === input.type || + (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 + ".type", - expected: '"lessThanEq"', - value: input.type, + path: _path + ".entries", + expected: "[entry: string, otherEntry: string].o5", + value: input.entries, }, _errorFactory )) && - ("string" === typeof input.otherEntry || + ("lessThanEq" === input.type || __typia_transform__assertGuard._assertGuard( _exceptionable, { method: "typia.createAssert", - path: _path + ".otherEntry", - expected: "string", - value: input.otherEntry, + path: _path + ".type", + expected: '"lessThanEq"', + value: input.type, }, _errorFactory )); diff --git a/packages/podspec/src/processors/validate.ts b/packages/podspec/src/processors/validate.ts index cafc045..fd4df2e 100644 --- a/packages/podspec/src/processors/validate.ts +++ b/packages/podspec/src/processors/validate.ts @@ -1,21 +1,21 @@ -import type { POD, PODEntries, PODValue, PODContent, PODName } from "@pcd/pod"; +import type { POD, PODContent, PODEntries, PODName, PODValue } 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 type { ValidateResult } from "./validate/types.js"; -import { FAILURE, SUCCESS } from "./validate/result.js"; -import { checkIsMemberOf } from "./validate/checks/checkIsMemberOf.js"; -import { checkIsNotMemberOf } from "./validate/checks/checkIsNotMemberOf.js"; import { assertPODSpec } from "../generated/podspec.js"; import { EntrySourcePodSpec } from "./validate/EntrySource.js"; -import { checkInRange } from "./validate/checks/checkInRange.js"; -import { checkNotInRange } from "./validate/checks/checkNotInRange.js"; import { checkEqualsEntry } from "./validate/checks/checkEqualsEntry.js"; -import { checkNotEqualsEntry } from "./validate/checks/checkNotEqualsEntry.js"; import { checkGreaterThan } from "./validate/checks/checkGreaterThan.js"; import { checkGreaterThanEq } from "./validate/checks/checkGreaterThanEq.js"; +import { checkInRange } from "./validate/checks/checkInRange.js"; +import { checkIsMemberOf } from "./validate/checks/checkIsMemberOf.js"; +import { checkIsNotMemberOf } from "./validate/checks/checkIsNotMemberOf.js"; import { checkLessThan } from "./validate/checks/checkLessThan.js"; import { checkLessThanEq } from "./validate/checks/checkLessThanEq.js"; +import { checkNotEqualsEntry } from "./validate/checks/checkNotEqualsEntry.js"; +import { checkNotInRange } from "./validate/checks/checkNotInRange.js"; +import { FAILURE, SUCCESS } from "./validate/result.js"; +import type { ValidateResult } from "./validate/types.js"; /** @TOOO diff --git a/packages/podspec/src/processors/validate/checks/checkEqualsEntry.ts b/packages/podspec/src/processors/validate/checks/checkEqualsEntry.ts index bcafe00..82c9029 100644 --- a/packages/podspec/src/processors/validate/checks/checkEqualsEntry.ts +++ b/packages/podspec/src/processors/validate/checks/checkEqualsEntry.ts @@ -15,8 +15,9 @@ export function checkEqualsEntry( entrySource: EntrySource, _exitOnError: boolean ): ValidationBaseIssue[] { - const entry1 = entrySource.getEntry(statement.entry); - const entry2 = entrySource.getEntry(statement.otherEntry); + const [leftEntry, rightEntry] = statement.entries; + const entry1 = entrySource.getEntry(leftEntry); + const entry2 = entrySource.getEntry(rightEntry); const issues = []; @@ -26,7 +27,7 @@ export function checkEqualsEntry( code: IssueCode.invalid_statement, statementName: statementName, statementType: statement.type, - entries: [statement.entry], + entries: statement.entries, path: [...path, statementName], }); return issues; @@ -36,21 +37,21 @@ export function checkEqualsEntry( code: IssueCode.invalid_statement, statementName: statementName, statementType: statement.type, - entries: [statement.otherEntry], + entries: statement.entries, path: [...path, statementName], }); return issues; } - const entry1Type = entrySource.getEntryTypeFromSpec(statement.entry); - const entry2Type = entrySource.getEntryTypeFromSpec(statement.otherEntry); + 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.entry, statement.otherEntry], + entries: statement.entries, path: [...path, statementName], }); return issues; @@ -61,7 +62,7 @@ export function checkEqualsEntry( code: IssueCode.invalid_statement, statementName: statementName, statementType: statement.type, - entries: [statement.entry], + entries: statement.entries, path: [...path, statementName], }); return issues; @@ -72,7 +73,7 @@ export function checkEqualsEntry( code: IssueCode.invalid_statement, statementName: statementName, statementType: statement.type, - entries: [statement.otherEntry], + entries: statement.entries, path: [...path, statementName], }); return issues; @@ -85,7 +86,7 @@ export function checkEqualsEntry( code: IssueCode.statement_negative_result, statementName: statementName, statementType: statement.type, - entries: [statement.entry, statement.otherEntry], + entries: statement.entries, path: [...path, statementName], } satisfies ValidationStatementNegativeResultIssue; return [issue]; diff --git a/packages/podspec/src/processors/validate/checks/checkGreaterThan.ts b/packages/podspec/src/processors/validate/checks/checkGreaterThan.ts index 1b9c9dc..75ae8aa 100644 --- a/packages/podspec/src/processors/validate/checks/checkGreaterThan.ts +++ b/packages/podspec/src/processors/validate/checks/checkGreaterThan.ts @@ -11,8 +11,9 @@ export function checkGreaterThan( entrySource: EntrySource, _exitOnError: boolean ) { - const entry1 = entrySource.getEntry(statement.entry); - const entry2 = entrySource.getEntry(statement.otherEntry); + const [leftEntry, rightEntry] = statement.entries; + const entry1 = entrySource.getEntry(leftEntry); + const entry2 = entrySource.getEntry(rightEntry); const issues = []; @@ -22,7 +23,7 @@ export function checkGreaterThan( code: IssueCode.invalid_statement, statementName: statementName, statementType: statement.type, - entries: [statement.entry], + entries: statement.entries, path: [...path, statementName], }); return issues; @@ -32,14 +33,14 @@ export function checkGreaterThan( code: IssueCode.invalid_statement, statementName: statementName, statementType: statement.type, - entries: [statement.otherEntry], + entries: statement.entries, path: [...path, statementName], }); return issues; } - const entry1Type = entrySource.getEntryTypeFromSpec(statement.entry); - const entry2Type = entrySource.getEntryTypeFromSpec(statement.otherEntry); + const entry1Type = entrySource.getEntryTypeFromSpec(leftEntry); + const entry2Type = entrySource.getEntryTypeFromSpec(rightEntry); // TODO this may be too restrictive if (entry1Type !== entry2Type) { @@ -47,7 +48,7 @@ export function checkGreaterThan( code: IssueCode.invalid_statement, statementName: statementName, statementType: statement.type, - entries: [statement.entry, statement.otherEntry], + entries: statement.entries, path: [...path, statementName], }); return issues; @@ -58,7 +59,7 @@ export function checkGreaterThan( code: IssueCode.invalid_statement, statementName: statementName, statementType: statement.type, - entries: [statement.entry], + entries: statement.entries, path: [...path, statementName], }); return issues; @@ -69,7 +70,7 @@ export function checkGreaterThan( code: IssueCode.invalid_statement, statementName: statementName, statementType: statement.type, - entries: [statement.otherEntry], + entries: statement.entries, path: [...path, statementName], }); return issues; @@ -80,7 +81,7 @@ export function checkGreaterThan( code: IssueCode.invalid_statement, statementName: statementName, statementType: statement.type, - entries: [statement.entry, statement.otherEntry], + entries: statement.entries, path: [...path, statementName], }); return issues; @@ -93,7 +94,7 @@ export function checkGreaterThan( code: IssueCode.statement_negative_result, statementName: statementName, statementType: statement.type, - entries: [statement.entry, statement.otherEntry], + entries: statement.entries, path: [...path, statementName], }); } diff --git a/packages/podspec/src/processors/validate/checks/checkGreaterThanEq.ts b/packages/podspec/src/processors/validate/checks/checkGreaterThanEq.ts index b936418..faa123b 100644 --- a/packages/podspec/src/processors/validate/checks/checkGreaterThanEq.ts +++ b/packages/podspec/src/processors/validate/checks/checkGreaterThanEq.ts @@ -11,8 +11,9 @@ export function checkGreaterThanEq( entrySource: EntrySource, _exitOnError: boolean ) { - const entry1 = entrySource.getEntry(statement.entry); - const entry2 = entrySource.getEntry(statement.otherEntry); + const [leftEntry, rightEntry] = statement.entries; + const entry1 = entrySource.getEntry(leftEntry); + const entry2 = entrySource.getEntry(rightEntry); const issues = []; @@ -22,7 +23,7 @@ export function checkGreaterThanEq( code: IssueCode.invalid_statement, statementName: statementName, statementType: statement.type, - entries: [statement.entry], + entries: statement.entries, path: [...path, statementName], }); return issues; @@ -32,14 +33,14 @@ export function checkGreaterThanEq( code: IssueCode.invalid_statement, statementName: statementName, statementType: statement.type, - entries: [statement.otherEntry], + entries: statement.entries, path: [...path, statementName], }); return issues; } - const entry1Type = entrySource.getEntryTypeFromSpec(statement.entry); - const entry2Type = entrySource.getEntryTypeFromSpec(statement.otherEntry); + const entry1Type = entrySource.getEntryTypeFromSpec(leftEntry); + const entry2Type = entrySource.getEntryTypeFromSpec(rightEntry); // TODO this may be too restrictive if (entry1Type !== entry2Type) { @@ -47,7 +48,7 @@ export function checkGreaterThanEq( code: IssueCode.invalid_statement, statementName: statementName, statementType: statement.type, - entries: [statement.entry, statement.otherEntry], + entries: statement.entries, path: [...path, statementName], }); return issues; @@ -58,7 +59,7 @@ export function checkGreaterThanEq( code: IssueCode.invalid_statement, statementName: statementName, statementType: statement.type, - entries: [statement.entry], + entries: statement.entries, path: [...path, statementName], }); return issues; @@ -69,7 +70,7 @@ export function checkGreaterThanEq( code: IssueCode.invalid_statement, statementName: statementName, statementType: statement.type, - entries: [statement.otherEntry], + entries: statement.entries, path: [...path, statementName], }); return issues; @@ -80,7 +81,7 @@ export function checkGreaterThanEq( code: IssueCode.invalid_statement, statementName: statementName, statementType: statement.type, - entries: [statement.entry, statement.otherEntry], + entries: statement.entries, path: [...path, statementName], }); return issues; @@ -93,7 +94,7 @@ export function checkGreaterThanEq( code: IssueCode.statement_negative_result, statementName: statementName, statementType: statement.type, - entries: [statement.entry, statement.otherEntry], + entries: statement.entries, path: [...path, statementName], }); } diff --git a/packages/podspec/src/processors/validate/checks/checkInRange.ts b/packages/podspec/src/processors/validate/checks/checkInRange.ts index 69e3c00..9101a34 100644 --- a/packages/podspec/src/processors/validate/checks/checkInRange.ts +++ b/packages/podspec/src/processors/validate/checks/checkInRange.ts @@ -1,12 +1,12 @@ 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"; -import type { EntrySource } from "../EntrySource.js"; export function checkInRange( // eslint-disable-next-line @typescript-eslint/no-explicit-any @@ -16,7 +16,7 @@ export function checkInRange( entrySource: EntrySource, _exitOnError: boolean ): ValidationBaseIssue[] { - const entryName = statement.entry; + const [entryName] = statement.entries; const entry = entrySource.getEntry(entryName); const issues = []; diff --git a/packages/podspec/src/processors/validate/checks/checkLessThan.ts b/packages/podspec/src/processors/validate/checks/checkLessThan.ts index 1a6e4a4..898c816 100644 --- a/packages/podspec/src/processors/validate/checks/checkLessThan.ts +++ b/packages/podspec/src/processors/validate/checks/checkLessThan.ts @@ -11,8 +11,9 @@ export function checkLessThan( entrySource: EntrySource, _exitOnError: boolean ) { - const entry1 = entrySource.getEntry(statement.entry); - const entry2 = entrySource.getEntry(statement.otherEntry); + const [leftEntry, rightEntry] = statement.entries; + const entry1 = entrySource.getEntry(leftEntry); + const entry2 = entrySource.getEntry(rightEntry); const issues = []; @@ -22,7 +23,7 @@ export function checkLessThan( code: IssueCode.invalid_statement, statementName: statementName, statementType: statement.type, - entries: [statement.entry], + entries: statement.entries, path: [...path, statementName], }); return issues; @@ -32,14 +33,14 @@ export function checkLessThan( code: IssueCode.invalid_statement, statementName: statementName, statementType: statement.type, - entries: [statement.otherEntry], + entries: statement.entries, path: [...path, statementName], }); return issues; } - const entry1Type = entrySource.getEntryTypeFromSpec(statement.entry); - const entry2Type = entrySource.getEntryTypeFromSpec(statement.otherEntry); + const entry1Type = entrySource.getEntryTypeFromSpec(leftEntry); + const entry2Type = entrySource.getEntryTypeFromSpec(rightEntry); // TODO this may be too restrictive if (entry1Type !== entry2Type) { @@ -47,7 +48,7 @@ export function checkLessThan( code: IssueCode.invalid_statement, statementName: statementName, statementType: statement.type, - entries: [statement.entry, statement.otherEntry], + entries: statement.entries, path: [...path, statementName], }); return issues; @@ -58,7 +59,7 @@ export function checkLessThan( code: IssueCode.invalid_statement, statementName: statementName, statementType: statement.type, - entries: [statement.entry], + entries: statement.entries, path: [...path, statementName], }); return issues; @@ -69,7 +70,7 @@ export function checkLessThan( code: IssueCode.invalid_statement, statementName: statementName, statementType: statement.type, - entries: [statement.otherEntry], + entries: statement.entries, path: [...path, statementName], }); return issues; @@ -80,7 +81,7 @@ export function checkLessThan( code: IssueCode.invalid_statement, statementName: statementName, statementType: statement.type, - entries: [statement.entry, statement.otherEntry], + entries: statement.entries, path: [...path, statementName], }); return issues; @@ -93,7 +94,7 @@ export function checkLessThan( code: IssueCode.statement_negative_result, statementName: statementName, statementType: statement.type, - entries: [statement.entry, statement.otherEntry], + entries: statement.entries, path: [...path, statementName], }); } diff --git a/packages/podspec/src/processors/validate/checks/checkLessThanEq.ts b/packages/podspec/src/processors/validate/checks/checkLessThanEq.ts index 0c64efd..12bcc84 100644 --- a/packages/podspec/src/processors/validate/checks/checkLessThanEq.ts +++ b/packages/podspec/src/processors/validate/checks/checkLessThanEq.ts @@ -11,8 +11,9 @@ export function checkLessThanEq( entrySource: EntrySource, _exitOnError: boolean ) { - const entry1 = entrySource.getEntry(statement.entry); - const entry2 = entrySource.getEntry(statement.otherEntry); + const [leftEntry, rightEntry] = statement.entries; + const entry1 = entrySource.getEntry(leftEntry); + const entry2 = entrySource.getEntry(rightEntry); const issues = []; @@ -22,7 +23,7 @@ export function checkLessThanEq( code: IssueCode.invalid_statement, statementName: statementName, statementType: statement.type, - entries: [statement.entry], + entries: statement.entries, path: [...path, statementName], }); return issues; @@ -32,14 +33,14 @@ export function checkLessThanEq( code: IssueCode.invalid_statement, statementName: statementName, statementType: statement.type, - entries: [statement.otherEntry], + entries: statement.entries, path: [...path, statementName], }); return issues; } - const entry1Type = entrySource.getEntryTypeFromSpec(statement.entry); - const entry2Type = entrySource.getEntryTypeFromSpec(statement.otherEntry); + const entry1Type = entrySource.getEntryTypeFromSpec(leftEntry); + const entry2Type = entrySource.getEntryTypeFromSpec(rightEntry); // TODO this may be too restrictive if (entry1Type !== entry2Type) { @@ -47,7 +48,7 @@ export function checkLessThanEq( code: IssueCode.invalid_statement, statementName: statementName, statementType: statement.type, - entries: [statement.entry, statement.otherEntry], + entries: statement.entries, path: [...path, statementName], }); return issues; @@ -58,7 +59,7 @@ export function checkLessThanEq( code: IssueCode.invalid_statement, statementName: statementName, statementType: statement.type, - entries: [statement.entry], + entries: statement.entries, path: [...path, statementName], }); return issues; @@ -69,7 +70,7 @@ export function checkLessThanEq( code: IssueCode.invalid_statement, statementName: statementName, statementType: statement.type, - entries: [statement.otherEntry], + entries: statement.entries, path: [...path, statementName], }); return issues; @@ -80,7 +81,7 @@ export function checkLessThanEq( code: IssueCode.invalid_statement, statementName: statementName, statementType: statement.type, - entries: [statement.entry, statement.otherEntry], + entries: statement.entries, path: [...path, statementName], }); return issues; @@ -93,7 +94,7 @@ export function checkLessThanEq( code: IssueCode.statement_negative_result, statementName: statementName, statementType: statement.type, - entries: [statement.entry, statement.otherEntry], + entries: statement.entries, path: [...path, statementName], }); } diff --git a/packages/podspec/src/processors/validate/checks/checkNotEqualsEntry.ts b/packages/podspec/src/processors/validate/checks/checkNotEqualsEntry.ts index bd8cc0d..d1881f9 100644 --- a/packages/podspec/src/processors/validate/checks/checkNotEqualsEntry.ts +++ b/packages/podspec/src/processors/validate/checks/checkNotEqualsEntry.ts @@ -15,8 +15,9 @@ export function checkNotEqualsEntry( entrySource: EntrySource, _exitOnError: boolean ): ValidationBaseIssue[] { - const entry1 = entrySource.getEntry(statement.entry); - const entry2 = entrySource.getEntry(statement.otherEntry); + const [leftEntry, rightEntry] = statement.entries; + const entry1 = entrySource.getEntry(leftEntry); + const entry2 = entrySource.getEntry(rightEntry); const issues = []; @@ -26,7 +27,7 @@ export function checkNotEqualsEntry( code: IssueCode.invalid_statement, statementName: statementName, statementType: statement.type, - entries: [statement.entry], + entries: statement.entries, path: [...path, statementName], }); return issues; @@ -36,21 +37,21 @@ export function checkNotEqualsEntry( code: IssueCode.invalid_statement, statementName: statementName, statementType: statement.type, - entries: [statement.otherEntry], + entries: statement.entries, path: [...path, statementName], }); return issues; } - const entry1Type = entrySource.getEntryTypeFromSpec(statement.entry); - const entry2Type = entrySource.getEntryTypeFromSpec(statement.otherEntry); + 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.entry, statement.otherEntry], + entries: statement.entries, path: [...path, statementName], }); return issues; @@ -61,7 +62,7 @@ export function checkNotEqualsEntry( code: IssueCode.invalid_statement, statementName: statementName, statementType: statement.type, - entries: [statement.entry], + entries: statement.entries, path: [...path, statementName], }); return issues; @@ -72,7 +73,7 @@ export function checkNotEqualsEntry( code: IssueCode.invalid_statement, statementName: statementName, statementType: statement.type, - entries: [statement.otherEntry], + entries: statement.entries, path: [...path, statementName], }); return issues; @@ -85,7 +86,7 @@ export function checkNotEqualsEntry( code: IssueCode.statement_negative_result, statementName: statementName, statementType: statement.type, - entries: [statement.entry, statement.otherEntry], + entries: statement.entries, path: [...path, statementName], } satisfies ValidationStatementNegativeResultIssue; return [issue]; diff --git a/packages/podspec/src/processors/validate/checks/checkNotInRange.ts b/packages/podspec/src/processors/validate/checks/checkNotInRange.ts index f066ea5..241d2cf 100644 --- a/packages/podspec/src/processors/validate/checks/checkNotInRange.ts +++ b/packages/podspec/src/processors/validate/checks/checkNotInRange.ts @@ -1,12 +1,12 @@ 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"; -import type { EntrySource } from "../EntrySource.js"; export function checkNotInRange( // eslint-disable-next-line @typescript-eslint/no-explicit-any @@ -16,7 +16,7 @@ export function checkNotInRange( entrySource: EntrySource, _exitOnError: boolean ): ValidationBaseIssue[] { - const entryName = statement.entry; + const [entryName] = statement.entries; const entry = entrySource.getEntry(entryName); // TODO need an issue type for statement referring to a non-existent entry diff --git a/packages/podspec/src/processors/validate/issues.ts b/packages/podspec/src/processors/validate/issues.ts index 8a1340a..46c32be 100644 --- a/packages/podspec/src/processors/validate/issues.ts +++ b/packages/podspec/src/processors/validate/issues.ts @@ -117,7 +117,7 @@ export interface ValidationStatementNegativeResultIssue export class ValidationError extends Error { issues: ValidationBaseIssue[] = []; - public get errors(): ValidationBaseIssue[] { + public errors(): ValidationBaseIssue[] { return this.issues; } diff --git a/packages/podspec/src/shared/types.ts b/packages/podspec/src/shared/types.ts new file mode 100644 index 0000000..d22e9a2 --- /dev/null +++ b/packages/podspec/src/shared/types.ts @@ -0,0 +1,5 @@ +export type IsLiteral = string extends T + ? false + : T extends string + ? true + : false; diff --git a/packages/podspec/test/builders/PODGroupSpecBuilder.spec.ts b/packages/podspec/test/builders/PODGroupSpecBuilder.spec.ts index 8a131f5..d318bdf 100644 --- a/packages/podspec/test/builders/PODGroupSpecBuilder.spec.ts +++ b/packages/podspec/test/builders/PODGroupSpecBuilder.spec.ts @@ -1,6 +1,15 @@ -import { describe, it, expect, assertType } from "vitest"; -import { PODGroupSpecBuilder, PODSpecBuilder } from "../../src/index.js"; +import { fc, test } from "@fast-check/vitest"; +import { assertType, describe, expect, it } from "vitest"; import type { AllPODEntries } from "../../src/builders/group.js"; +import type { + EntryName, + PODValueType, +} from "../../src/builders/types/entries.js"; +import type { + EntriesWithRangeChecks, + Statements, +} from "../../src/builders/types/statements.js"; +import { PODGroupSpecBuilder, PODSpecBuilder } from "../../src/index.js"; describe("PODGroupSpecBuilder", () => { it("should be a test", () => { @@ -78,3 +87,215 @@ describe("PODGroupSpecBuilder", () => { type _T2 = Parameters[1]; // Second parameter type }); }); + +describe("PODGroupSpecBuilder - Property tests", () => { + // Arbitraries for generating test data + const validPodName = fc + .string() + .filter((s) => /^[a-zA-Z][a-zA-Z0-9_]*$/.test(s)) + .filter((s) => s.length <= 32); + + const validEntryName = fc + .string() + .filter((s) => /^[a-zA-Z][a-zA-Z0-9_]*$/.test(s)) + .filter((s) => s.length <= 32); + + const podType = fc.constantFrom( + "string", + "int", + "boolean", + "date", + "eddsa_pubkey" + ) as fc.Arbitrary; + + const entryConfig = fc.record({ + name: validEntryName, + type: podType, + }); + + const podConfig = fc.record({ + name: validPodName, + entries: fc.uniqueArray(entryConfig, { + minLength: 1, + maxLength: 5, + comparator: (a, b) => a.name === b.name, + }), + }); + + const podConfigs = fc.uniqueArray(podConfig, { + minLength: 1, + maxLength: 3, + comparator: (a, b) => a.name === b.name, + }); + + // Helper function to create a POD spec from entries + const createPodSpec = (entries: { name: string; type: PODValueType }[]) => { + return entries.reduce( + (builder, { name, type }) => builder.entry(name, type), + PODSpecBuilder.create() + ); + }; + + // Test that adding PODs is commutative + test("should be commutative when adding PODs", () => { + fc.assert( + fc.property(podConfigs, (pods) => { + // Add PODs in original order + const builder1 = pods.reduce( + (builder, { name, entries }) => + builder.pod(name, createPodSpec(entries).spec()), + PODGroupSpecBuilder.create() + ); + + // Add PODs in reverse order + const builder2 = [...pods] + .reverse() + .reduce( + (builder, { name, entries }) => + builder.pod(name, createPodSpec(entries).spec()), + PODGroupSpecBuilder.create() + ); + + // The resulting specs should be equivalent + expect(builder1.spec()).toEqual(builder2.spec()); + }) + ); + }); + + // Test that range checks maintain valid bounds across PODs + test("should maintain valid bounds for range checks across PODs", () => { + const dateArb = fc.date({ + min: new Date(0), + max: new Date(2100, 0, 1), + }); + + fc.assert( + fc.property( + podConfigs, + validPodName, + validEntryName, + dateArb, + dateArb, + (pods, podName, entryName, date1, date2) => { + // Create a POD with a date entry + const podWithDate = { + name: podName, + entries: [{ name: entryName, type: "date" as const }], + }; + + const min = date1 < date2 ? date1 : date2; + const max = date1 < date2 ? date2 : date1; + + const builder = pods + .reduce( + (builder, { name, entries }) => + builder.pod(name, createPodSpec(entries).spec()), + PODGroupSpecBuilder.create() + ) + .pod(podWithDate.name, createPodSpec(podWithDate.entries).spec()) + .inRange(`${podWithDate.name}.${entryName}`, { min, max }); + + const spec = builder.spec(); + const statement = Object.values(spec.statements)[0] as Statements; + + // Range bounds should be ordered correctly in the spec + if (statement?.type === "inRange") { + const minTime = BigInt(statement.inRange.min); + const maxTime = BigInt(statement.inRange.max); + expect(minTime).toBeLessThanOrEqual(maxTime); + } + } + ) + ); + }); + + // Test that automatic statement names are always unique + test("should generate unique automatic statement names", () => { + fc.assert( + fc.property(podConfigs, (pods) => { + const builder = pods.reduce((b, { name, entries }) => { + return b.pod(name, createPodSpec(entries).spec()); + }, PODGroupSpecBuilder.create()); + + // Add some statements using the first entry of each POD + const builderWithStatements = pods.reduce((b, { name, entries }) => { + if (entries[0] && entries[0].type === "int") { + return b.inRange(`${name}.${entries[0].name}`, { + min: 0n, + max: 10n, + }); + } + return b; + }, builder); + + const spec = builderWithStatements.spec(); + const statementNames = Object.keys(spec.statements); + const uniqueNames = new Set(statementNames); + + expect(statementNames.length).toBe(uniqueNames.size); + }) + ); + }); + + // Test that entry equality checks work across PODs + test("should maintain type safety for equalsEntry across PODs", () => { + fc.assert( + fc.property(podConfigs, (pods) => { + // Find two PODs with matching entry types + const podsWithMatchingEntries = pods.filter( + (pod) => pod.entries[0]?.type === pods[0]?.entries[0]?.type + ); + + if (podsWithMatchingEntries.length >= 2) { + const pod1 = podsWithMatchingEntries[0]; + const pod2 = podsWithMatchingEntries[1]; + + const builder = pods.reduce( + (builder, { name, entries }) => + builder.pod(name, createPodSpec(entries).spec()), + PODGroupSpecBuilder.create() + ); + + if (pod1 && pod2 && pod1.entries[0] && pod2.entries[0]) { + const builderWithEquals = builder.equalsEntry( + `${pod1.name}.${pod1.entries[0].name}`, + `${pod2.name}.${pod2.entries[0].name}` + ); + + const spec = builderWithEquals.spec(); + const statement = Object.values(spec.statements)[0] as Statements; + + expect(statement?.type).toBe("equalsEntry"); + expect(statement?.entries).toHaveLength(2); + } + } + }) + ); + }); +}); + +// Let's start with an empty PODGroupSpecBuilder +type EmptyPODs = {}; + +// Check AllPODEntries +type Step1 = AllPODEntries; + +// Check EntriesWithRangeChecks +type Step2 = EntriesWithRangeChecks; + +// Check EntryName +type Step3 = EntryName; + +// Check the full constraint +type Step4 = EntryName>>; + +type Debug1 = AllPODEntries<{}>; +type Debug2 = EntriesWithRangeChecks; +type Debug3 = EntryName; + +// The conditional type from range: +type Step5 = N extends keyof EntriesWithRangeChecks> + ? AllPODEntries[N] extends "date" + ? Date + : bigint + : Date | bigint; diff --git a/packages/podspec/test/builders/PODSpecBuilder.spec.ts b/packages/podspec/test/builders/PODSpecBuilder.spec.ts index ad0c878..51b36f5 100644 --- a/packages/podspec/test/builders/PODSpecBuilder.spec.ts +++ b/packages/podspec/test/builders/PODSpecBuilder.spec.ts @@ -17,7 +17,7 @@ import { PODSpecBuilder } from "../../src/index.js"; - [ ] greaterThanEq - [ ] lessThan - [ ] lessThanEq - - [ ] Custom statement names + - [x] Custom statement names - [ ] Pick entries - [ ] Pick statements - [ ] Omit entries @@ -28,18 +28,16 @@ import { PODSpecBuilder } from "../../src/index.js"; */ describe("PODSpecBuilder", () => { - it("should be a test", () => { - expect(true).toBe(true); - }); - it("PODSpecBuilder", () => { - const a = PODSpecBuilder.create(); + 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: { @@ -57,7 +55,7 @@ describe("PODSpecBuilder", () => { isMemberOf: [["foo"]], }, b_inRange: { - entry: "b", + entries: ["b"], type: "inRange", inRange: { min: "10", max: "100" }, }, @@ -69,7 +67,7 @@ describe("PODSpecBuilder", () => { const f = e.pickEntries(["b"]); expect(f.spec().statements).toEqual({ b_inRange: { - entry: "b", + entries: ["b"], type: "inRange", inRange: { min: "10", max: "100" }, }, @@ -87,9 +85,8 @@ describe("PODSpecBuilder", () => { expect(g.spec().statements).toMatchObject({ a_new_equalsEntry: { - entry: "a", + entries: ["a", "new"], type: "equalsEntry", - otherEntry: "new", }, }); @@ -98,6 +95,7 @@ describe("PODSpecBuilder", () => { a: "string", b: "int", new: "string", + zzz: "string", }, statements: { a_isMemberOf: { @@ -113,16 +111,15 @@ describe("PODSpecBuilder", () => { isMemberOf: [["foo", "10"]], }, b_inRange: { - entry: "b", + 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: { - entry: "a", + entries: ["a", "new"], type: "equalsEntry", - otherEntry: "new", }, }, } satisfies typeof _GSpec); @@ -170,7 +167,6 @@ describe("PODSpecBuilder - Property tests", () => { fc.assert( fc.property(entryConfigs, (entries) => { // Add entries in original order - console.log(entries); const builder1 = entries.reduce( (b, { name, type }) => b.entry(name, type), PODSpecBuilder.create() @@ -206,9 +202,7 @@ describe("PODSpecBuilder - Property tests", () => { .map((i) => entryNames[i % entryNames.length]) .filter((name): name is string => name !== undefined); - // @ts-expect-error ignore this const picked = builder.pickEntries(pickedNames); - // @ts-expect-error ignore this const omitted = picked.omitEntries(pickedNames); // Omitting all picked entries should result in empty entries @@ -247,8 +241,27 @@ describe("PODSpecBuilder - Property tests", () => { ); }); + // Test that automatic statement names are always unique + test("should generate unique automatic statement names", () => { + fc.assert( + fc.property(entryConfigs, (entries) => { + const builder = entries.reduce( + (b, { name }) => + b.entry(name, "int").inRange(name, { min: 0n, max: 10n }), + PODSpecBuilder.create() + ); + + const spec = builder.spec(); + const statementNames = Object.keys(spec.statements); + const uniqueNames = new Set(statementNames); + + expect(statementNames.length).toBe(uniqueNames.size); + }) + ); + }); + // Test that custom statement names are always unique - test("should generate unique statement names", () => { + test("should generate unique custom statement names", () => { fc.assert( fc.property(entryConfigs, fc.string(), (entries, customName) => { const builder = entries.reduce( From 6d29fbc9c2ae4f5d438e82fcc38859b4710ce1a3 Mon Sep 17 00:00:00 2001 From: Rob Knight Date: Tue, 4 Feb 2025 15:19:40 +0100 Subject: [PATCH 16/20] Basically working validation and querying --- packages/podspec/src/builders/group.ts | 92 +- packages/podspec/src/builders/pod.ts | 21 +- .../podspec/src/builders/types/entries.ts | 2 +- packages/podspec/src/generated/podspec.ts | 1286 ++++++++++++++++- packages/podspec/src/processors/db/podDB.ts | 198 +++ .../src/processors/validate/EntrySource.ts | 46 +- .../src/processors/validate/groupValidator.ts | 234 +++ .../podspec/src/processors/validate/issues.ts | 11 + .../{validate.ts => validate/podValidator.ts} | 78 +- .../podspec/src/processors/validate/types.ts | 4 +- packages/podspec/src/shared/types.ts | 18 +- packages/podspec/src/spec/types.ts | 32 + packages/podspec/src/typia/podspec.ts | 9 +- .../test/builders/PODGroupSpecBuilder.spec.ts | 225 +-- .../test/builders/PODSpecBuilder.spec.ts | 150 +- .../podspec/test/processors/db/db.spec.ts | 46 + .../processors/validator/EntrySource.spec.ts | 103 ++ .../validator/groupValidator.spec.ts | 91 ++ ...validator.spec.ts => podValidator.spec.ts} | 16 +- 19 files changed, 2181 insertions(+), 481 deletions(-) create mode 100644 packages/podspec/src/processors/db/podDB.ts create mode 100644 packages/podspec/src/processors/validate/groupValidator.ts rename packages/podspec/src/processors/{validate.ts => validate/podValidator.ts} (71%) create mode 100644 packages/podspec/src/spec/types.ts create mode 100644 packages/podspec/test/processors/db/db.spec.ts create mode 100644 packages/podspec/test/processors/validator/EntrySource.spec.ts create mode 100644 packages/podspec/test/processors/validator/groupValidator.spec.ts rename packages/podspec/test/processors/validator/{validator.spec.ts => podValidator.spec.ts} (81%) diff --git a/packages/podspec/src/builders/group.ts b/packages/podspec/src/builders/group.ts index c8e1b37..509d30a 100644 --- a/packages/podspec/src/builders/group.ts +++ b/packages/podspec/src/builders/group.ts @@ -6,6 +6,7 @@ import { POD_INT_MIN, checkPODName, } from "@pcd/pod"; +import type { IsSingleLiteralString } from "../shared/types.js"; import { type PODSpec, PODSpecBuilder, virtualEntries } from "./pod.js"; import { convertValuesToStringTuples, @@ -51,12 +52,25 @@ export type PODGroupSpec

= { statements: S; }; -export type AllPODEntries

= { - [K in keyof P]: { - [E in keyof (P[K]["entries"] & VirtualEntries) as `${K & string}.${E & - string}`]: (P[K]["entries"] & VirtualEntries)[E]; - }; -}[keyof P]; +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; @@ -93,11 +107,10 @@ export class PODGroupSpecBuilder< this.#spec = spec; } - // eslint-disable-next-line @typescript-eslint/no-empty-object-type - public static create(): PODGroupSpecBuilder { + public static create() { return new PODGroupSpecBuilder({ - pods: {} as NamedPODSpecs, - statements: {} as StatementMap, + pods: {}, + statements: {}, }); } @@ -109,7 +122,10 @@ export class PODGroupSpecBuilder< N extends PODName, Spec extends PODSpec, NewPods extends AddPOD, - >(name: N, spec: Spec): PODGroupSpecBuilder { + >( + name: IsSingleLiteralString extends true ? N : never, + spec: Spec + ): PODGroupSpecBuilder { if (Object.prototype.hasOwnProperty.call(this.#spec.pods, name)) { throw new Error(`POD "${name}" already exists`); } @@ -728,10 +744,11 @@ export class PODGroupSpecBuilder< pod1 === undefined || entry1 === undefined || !Object.prototype.hasOwnProperty.call(this.#spec.pods, pod1) || - !Object.prototype.hasOwnProperty.call( + (!Object.prototype.hasOwnProperty.call( this.#spec.pods[pod1]!.entries, entry1 - ) + ) && + !Object.prototype.hasOwnProperty.call(virtualEntries, entry1)) ) { throw new Error(`Entry "${name1}" does not exist`); } @@ -739,10 +756,11 @@ export class PODGroupSpecBuilder< pod2 === undefined || entry2 === undefined || !Object.prototype.hasOwnProperty.call(this.#spec.pods, pod2) || - !Object.prototype.hasOwnProperty.call( + (!Object.prototype.hasOwnProperty.call( this.#spec.pods[pod2]!.entries, entry2 - ) + ) && + !Object.prototype.hasOwnProperty.call(virtualEntries, entry2)) ) { throw new Error(`Entry "${name2}" does not exist`); } @@ -860,14 +878,46 @@ if (import.meta.vitest) { .entry("my_int", "int") .entry("mystery_name", "string"); - const pod2 = PODSpecBuilder.create().entry("something_else", "boolean"); + const _pod2 = PODSpecBuilder.create().entry("something_else", "boolean"); - const _builder = PODGroupSpecBuilder.create().pod("foo", pod.spec()); - // .inRange("foo.my_int", { min: 0n, max: 10n }); + const _builder = PODGroupSpecBuilder.create() + .pod("foo", pod.spec()) + .pod("bar", pod.spec()) + .inRange("foo.my_int", { min: 0n, max: 10n }); + }); + }); +} - const _builder2 = PODGroupSpecBuilder.create() - .pod("mystery" as string, pod.spec()) - .pod("mystery2" as string, pod2.spec()); +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 index b3c2f58..137d2aa 100644 --- a/packages/podspec/src/builders/pod.ts +++ b/packages/podspec/src/builders/pod.ts @@ -6,9 +6,8 @@ import { checkPODName, } from "@pcd/pod"; import type { IsJsonSafe } from "../shared/jsonSafe.js"; -import type { IsLiteral } from "../shared/types.js"; +import type { IsSingleLiteralString } from "../shared/types.js"; import { - canonicalizeJSON, convertValuesToStringTuples, deepFreeze, supportsRangeChecks, @@ -62,7 +61,7 @@ import type { */ export const virtualEntries: VirtualEntries = { - $contentID: "string", + $contentID: "cryptographic", //$signature: "string", $signerPublicKey: "eddsa_pubkey", }; @@ -118,7 +117,6 @@ export class PODSpecBuilder< E extends EntryTypes, // eslint-disable-next-line @typescript-eslint/no-empty-object-type S extends StatementMap = {}, - LiteralMode extends boolean = true, > { readonly #spec: PODSpec; @@ -138,14 +136,9 @@ export class PODSpecBuilder< } public toJSON(): string { - const canonicalized = canonicalizeJSON(this.#spec); - if (!canonicalized) { - throw new Error("Failed to canonicalize PODSpec"); - } return JSON.stringify( { ...this.#spec, - hash: canonicalized /* TODO hashing! */, }, null, 2 @@ -156,14 +149,10 @@ export class PODSpecBuilder< K extends string, V extends PODValueType, NewEntries extends AddEntry, - // If the key is not a string literal, we need to exit literal mode - NewLiteralMode extends boolean = IsLiteral extends false - ? false - : LiteralMode, >( - key: NewLiteralMode extends true ? Exclude : string, + key: IsSingleLiteralString extends true ? Exclude : never, type: V - ): PODSpecBuilder { + ): PODSpecBuilder { if (Object.prototype.hasOwnProperty.call(this.#spec.entries, key)) { throw new Error(`Entry "${key}" already exists`); } @@ -171,7 +160,7 @@ export class PODSpecBuilder< // Will throw if not a valid POD entry name checkPODName(key); - return new PODSpecBuilder({ + return new PODSpecBuilder({ ...this.#spec, entries: { ...this.#spec.entries, diff --git a/packages/podspec/src/builders/types/entries.ts b/packages/podspec/src/builders/types/entries.ts index aa8494c..8c17ea5 100644 --- a/packages/podspec/src/builders/types/entries.ts +++ b/packages/podspec/src/builders/types/entries.ts @@ -23,7 +23,7 @@ export type EntriesOfType = { }; export type VirtualEntries = { - $contentID: "string"; + $contentID: "cryptographic"; //$signature: "string"; $signerPublicKey: "eddsa_pubkey"; }; diff --git a/packages/podspec/src/generated/podspec.ts b/packages/podspec/src/generated/podspec.ts index 02728e8..4616886 100644 --- a/packages/podspec/src/generated/podspec.ts +++ b/packages/podspec/src/generated/podspec.ts @@ -1,9 +1,11 @@ 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 { StatementMap } from "../builders/types/statements.js"; -import type { EntryTypes } from "../builders/types/entries.js"; +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 && @@ -1165,3 +1167,1283 @@ export const assertPODSpec = (() => { 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/processors/db/podDB.ts b/packages/podspec/src/processors/db/podDB.ts new file mode 100644 index 0000000..3d7f11c --- /dev/null +++ b/packages/podspec/src/processors/db/podDB.ts @@ -0,0 +1,198 @@ +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 } 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): Set { + 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 = new Set(); + 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.add(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, 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/validate/EntrySource.ts b/packages/podspec/src/processors/validate/EntrySource.ts index af3e216..0df390d 100644 --- a/packages/podspec/src/processors/validate/EntrySource.ts +++ b/packages/podspec/src/processors/validate/EntrySource.ts @@ -3,7 +3,6 @@ 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 { ValidateOptions } from "../validate.js"; import { IssueCode, type ValidationBaseIssue, @@ -13,6 +12,7 @@ import { type ValidationUnexpectedInputEntryIssue, type ValidationUnexpectedInputPodIssue, } from "./issues.js"; +import type { ValidateOptions } from "./podValidator.js"; export interface EntrySource { getEntry(entryName: string): PODValue | undefined; @@ -90,11 +90,29 @@ export class EntrySourcePodSpec implements EntrySource { } public getEntry(entryName: string): PODValue | undefined { - return this.pod.content.getValue(entryName); + 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 { - return this.podSpec.entries[entryName]; + if (entryName === "$signerPublicKey") { + return "eddsa_pubkey"; + } else if (entryName === "$contentID") { + return "cryptographic"; + } else { + return this.podSpec.entries[entryName]; + } } } @@ -178,7 +196,19 @@ export class EntrySourcePodGroupSpec implements EntrySource { return undefined; } - return this.pods[podName].content.getValue(entryName); + 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( @@ -192,6 +222,12 @@ export class EntrySourcePodGroupSpec implements EntrySource { ) { return undefined; } - return this.podGroupSpec.pods[podName].entries[entryName]; + 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/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 index 46c32be..014e84e 100644 --- a/packages/podspec/src/processors/validate/issues.ts +++ b/packages/podspec/src/processors/validate/issues.ts @@ -111,6 +111,17 @@ export interface ValidationStatementNegativeResultIssue entries: string[]; } +export type ValidationIssue = + | ValidationTypeMismatchIssue + | ValidationMissingEntryIssue + | ValidationMissingPodIssue + | ValidationInvalidEntryNameIssue + | ValidationInvalidStatementIssue + | ValidationInvalidPodValueIssue + | ValidationUnexpectedInputEntryIssue + | ValidationUnexpectedInputPodIssue + | ValidationStatementNegativeResultIssue; + /** * Exception class for errors that occur when parsing. */ diff --git a/packages/podspec/src/processors/validate.ts b/packages/podspec/src/processors/validate/podValidator.ts similarity index 71% rename from packages/podspec/src/processors/validate.ts rename to packages/podspec/src/processors/validate/podValidator.ts index fd4df2e..d1dfb74 100644 --- a/packages/podspec/src/processors/validate.ts +++ b/packages/podspec/src/processors/validate/podValidator.ts @@ -1,52 +1,28 @@ -import type { POD, PODContent, PODEntries, PODName, PODValue } 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 { EntrySourcePodSpec } from "./validate/EntrySource.js"; -import { checkEqualsEntry } from "./validate/checks/checkEqualsEntry.js"; -import { checkGreaterThan } from "./validate/checks/checkGreaterThan.js"; -import { checkGreaterThanEq } from "./validate/checks/checkGreaterThanEq.js"; -import { checkInRange } from "./validate/checks/checkInRange.js"; -import { checkIsMemberOf } from "./validate/checks/checkIsMemberOf.js"; -import { checkIsNotMemberOf } from "./validate/checks/checkIsNotMemberOf.js"; -import { checkLessThan } from "./validate/checks/checkLessThan.js"; -import { checkLessThanEq } from "./validate/checks/checkLessThanEq.js"; -import { checkNotEqualsEntry } from "./validate/checks/checkNotEqualsEntry.js"; -import { checkNotInRange } from "./validate/checks/checkNotInRange.js"; -import { FAILURE, SUCCESS } from "./validate/result.js"; -import type { ValidateResult } from "./validate/types.js"; +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? */ -/** - * "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; -} - -type PODEntriesFromEntryTypes = { - [K in keyof E]: Extract; -}; - export interface ValidateOptions { /** * If true, the validation will exit as soon as the first error is encountered. @@ -64,11 +40,15 @@ const DEFAULT_VALIDATE_OPTIONS: ValidateOptions = { }; interface PODValidator { - validate(pod: POD): ValidateResult>>; + validate( + pod: POD, + exitOnError?: boolean + ): ValidateResult>>; check(pod: POD): boolean; assert(pod: POD): asserts pod is StrongPOD>; strictValidate( - pod: POD + pod: POD, + exitOnError?: boolean ): ValidateResult>>; strictCheck(pod: POD): boolean; strictAssert(pod: POD): asserts pod is StrongPOD>; @@ -79,7 +59,7 @@ const SpecValidatorState = new WeakMap< boolean >(); -export function validate( +export function podValidator( spec: PODSpec ): PODValidator { const validSpec = SpecValidatorState.get(spec); @@ -97,13 +77,15 @@ export function validate( } return { - validate: (pod) => validatePOD(pod, spec, {}), + validate: (pod, exitOnError = false) => + validatePOD(pod, spec, { exitOnError }), check: (pod) => 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) => validatePOD(pod, spec, { strict: true }), + strictValidate: (pod, exitOnError = false) => + validatePOD(pod, spec, { strict: true, exitOnError }), strictCheck: (pod) => validatePOD(pod, spec, { strict: true, exitOnError: true }).isValid, strictAssert: (pod) => { diff --git a/packages/podspec/src/processors/validate/types.ts b/packages/podspec/src/processors/validate/types.ts index 64a9c8d..f269577 100644 --- a/packages/podspec/src/processors/validate/types.ts +++ b/packages/podspec/src/processors/validate/types.ts @@ -1,4 +1,4 @@ -import type { ValidationBaseIssue } from "./issues.js"; +import type { ValidationIssue } from "./issues.js"; export type ValidateSuccess = { value: T; @@ -6,7 +6,7 @@ export type ValidateSuccess = { }; export type ValidateFailure = { - issues: ValidationBaseIssue[]; + issues: ValidationIssue[]; isValid: false; }; diff --git a/packages/podspec/src/shared/types.ts b/packages/podspec/src/shared/types.ts index d22e9a2..a59bb40 100644 --- a/packages/podspec/src/shared/types.ts +++ b/packages/podspec/src/shared/types.ts @@ -1,5 +1,15 @@ -export type IsLiteral = string extends T +type IsSingleLiteralType = [P] extends [never] ? false - : T extends string - ? true - : 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/typia/podspec.ts b/packages/podspec/src/typia/podspec.ts index dcb244a..59f044d 100644 --- a/packages/podspec/src/typia/podspec.ts +++ b/packages/podspec/src/typia/podspec.ts @@ -1,7 +1,12 @@ import typia from "typia"; -import type { StatementMap } from "../builders/types/statements.js"; -import type { EntryTypes } from "../builders/types/entries.js"; +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/test/builders/PODGroupSpecBuilder.spec.ts b/packages/podspec/test/builders/PODGroupSpecBuilder.spec.ts index d318bdf..6d8a283 100644 --- a/packages/podspec/test/builders/PODGroupSpecBuilder.spec.ts +++ b/packages/podspec/test/builders/PODGroupSpecBuilder.spec.ts @@ -1,14 +1,5 @@ -import { fc, test } from "@fast-check/vitest"; import { assertType, describe, expect, it } from "vitest"; import type { AllPODEntries } from "../../src/builders/group.js"; -import type { - EntryName, - PODValueType, -} from "../../src/builders/types/entries.js"; -import type { - EntriesWithRangeChecks, - Statements, -} from "../../src/builders/types/statements.js"; import { PODGroupSpecBuilder, PODSpecBuilder } from "../../src/index.js"; describe("PODGroupSpecBuilder", () => { @@ -30,7 +21,7 @@ describe("PODGroupSpecBuilder", () => { "foo.my_string": "string", "foo.my_num": "int", "foo.$signerPublicKey": "eddsa_pubkey", - "foo.$contentID": "string", + "foo.$contentID": "cryptographic", }); expect(groupWithPod.spec()).toEqual({ @@ -76,7 +67,7 @@ describe("PODGroupSpecBuilder", () => { "foo.my_other_string": "string", "foo.my_num": "int", "foo.my_other_num": "int", - "foo.$contentID": "string", + "foo.$contentID": "cryptographic", "foo.$signerPublicKey": "eddsa_pubkey", }); @@ -87,215 +78,3 @@ describe("PODGroupSpecBuilder", () => { type _T2 = Parameters[1]; // Second parameter type }); }); - -describe("PODGroupSpecBuilder - Property tests", () => { - // Arbitraries for generating test data - const validPodName = fc - .string() - .filter((s) => /^[a-zA-Z][a-zA-Z0-9_]*$/.test(s)) - .filter((s) => s.length <= 32); - - const validEntryName = fc - .string() - .filter((s) => /^[a-zA-Z][a-zA-Z0-9_]*$/.test(s)) - .filter((s) => s.length <= 32); - - const podType = fc.constantFrom( - "string", - "int", - "boolean", - "date", - "eddsa_pubkey" - ) as fc.Arbitrary; - - const entryConfig = fc.record({ - name: validEntryName, - type: podType, - }); - - const podConfig = fc.record({ - name: validPodName, - entries: fc.uniqueArray(entryConfig, { - minLength: 1, - maxLength: 5, - comparator: (a, b) => a.name === b.name, - }), - }); - - const podConfigs = fc.uniqueArray(podConfig, { - minLength: 1, - maxLength: 3, - comparator: (a, b) => a.name === b.name, - }); - - // Helper function to create a POD spec from entries - const createPodSpec = (entries: { name: string; type: PODValueType }[]) => { - return entries.reduce( - (builder, { name, type }) => builder.entry(name, type), - PODSpecBuilder.create() - ); - }; - - // Test that adding PODs is commutative - test("should be commutative when adding PODs", () => { - fc.assert( - fc.property(podConfigs, (pods) => { - // Add PODs in original order - const builder1 = pods.reduce( - (builder, { name, entries }) => - builder.pod(name, createPodSpec(entries).spec()), - PODGroupSpecBuilder.create() - ); - - // Add PODs in reverse order - const builder2 = [...pods] - .reverse() - .reduce( - (builder, { name, entries }) => - builder.pod(name, createPodSpec(entries).spec()), - PODGroupSpecBuilder.create() - ); - - // The resulting specs should be equivalent - expect(builder1.spec()).toEqual(builder2.spec()); - }) - ); - }); - - // Test that range checks maintain valid bounds across PODs - test("should maintain valid bounds for range checks across PODs", () => { - const dateArb = fc.date({ - min: new Date(0), - max: new Date(2100, 0, 1), - }); - - fc.assert( - fc.property( - podConfigs, - validPodName, - validEntryName, - dateArb, - dateArb, - (pods, podName, entryName, date1, date2) => { - // Create a POD with a date entry - const podWithDate = { - name: podName, - entries: [{ name: entryName, type: "date" as const }], - }; - - const min = date1 < date2 ? date1 : date2; - const max = date1 < date2 ? date2 : date1; - - const builder = pods - .reduce( - (builder, { name, entries }) => - builder.pod(name, createPodSpec(entries).spec()), - PODGroupSpecBuilder.create() - ) - .pod(podWithDate.name, createPodSpec(podWithDate.entries).spec()) - .inRange(`${podWithDate.name}.${entryName}`, { min, max }); - - const spec = builder.spec(); - const statement = Object.values(spec.statements)[0] as Statements; - - // Range bounds should be ordered correctly in the spec - if (statement?.type === "inRange") { - const minTime = BigInt(statement.inRange.min); - const maxTime = BigInt(statement.inRange.max); - expect(minTime).toBeLessThanOrEqual(maxTime); - } - } - ) - ); - }); - - // Test that automatic statement names are always unique - test("should generate unique automatic statement names", () => { - fc.assert( - fc.property(podConfigs, (pods) => { - const builder = pods.reduce((b, { name, entries }) => { - return b.pod(name, createPodSpec(entries).spec()); - }, PODGroupSpecBuilder.create()); - - // Add some statements using the first entry of each POD - const builderWithStatements = pods.reduce((b, { name, entries }) => { - if (entries[0] && entries[0].type === "int") { - return b.inRange(`${name}.${entries[0].name}`, { - min: 0n, - max: 10n, - }); - } - return b; - }, builder); - - const spec = builderWithStatements.spec(); - const statementNames = Object.keys(spec.statements); - const uniqueNames = new Set(statementNames); - - expect(statementNames.length).toBe(uniqueNames.size); - }) - ); - }); - - // Test that entry equality checks work across PODs - test("should maintain type safety for equalsEntry across PODs", () => { - fc.assert( - fc.property(podConfigs, (pods) => { - // Find two PODs with matching entry types - const podsWithMatchingEntries = pods.filter( - (pod) => pod.entries[0]?.type === pods[0]?.entries[0]?.type - ); - - if (podsWithMatchingEntries.length >= 2) { - const pod1 = podsWithMatchingEntries[0]; - const pod2 = podsWithMatchingEntries[1]; - - const builder = pods.reduce( - (builder, { name, entries }) => - builder.pod(name, createPodSpec(entries).spec()), - PODGroupSpecBuilder.create() - ); - - if (pod1 && pod2 && pod1.entries[0] && pod2.entries[0]) { - const builderWithEquals = builder.equalsEntry( - `${pod1.name}.${pod1.entries[0].name}`, - `${pod2.name}.${pod2.entries[0].name}` - ); - - const spec = builderWithEquals.spec(); - const statement = Object.values(spec.statements)[0] as Statements; - - expect(statement?.type).toBe("equalsEntry"); - expect(statement?.entries).toHaveLength(2); - } - } - }) - ); - }); -}); - -// Let's start with an empty PODGroupSpecBuilder -type EmptyPODs = {}; - -// Check AllPODEntries -type Step1 = AllPODEntries; - -// Check EntriesWithRangeChecks -type Step2 = EntriesWithRangeChecks; - -// Check EntryName -type Step3 = EntryName; - -// Check the full constraint -type Step4 = EntryName>>; - -type Debug1 = AllPODEntries<{}>; -type Debug2 = EntriesWithRangeChecks; -type Debug3 = EntryName; - -// The conditional type from range: -type Step5 = N extends keyof EntriesWithRangeChecks> - ? AllPODEntries[N] extends "date" - ? Date - : bigint - : Date | bigint; diff --git a/packages/podspec/test/builders/PODSpecBuilder.spec.ts b/packages/podspec/test/builders/PODSpecBuilder.spec.ts index 51b36f5..f6230a6 100644 --- a/packages/podspec/test/builders/PODSpecBuilder.spec.ts +++ b/packages/podspec/test/builders/PODSpecBuilder.spec.ts @@ -1,6 +1,4 @@ -import { fc, test } from "@fast-check/vitest"; import { describe, expect, it } from "vitest"; -import type { PODValueType } from "../../src/builders/types/entries.js"; import type { EqualsEntry } from "../../src/builders/types/statements.js"; import { PODSpecBuilder } from "../../src/index.js"; @@ -25,6 +23,7 @@ import { PODSpecBuilder } from "../../src/index.js"; - [ ] Spec output matches expected output - [ ] Test outputs for all of the above cases - [ ] Erroneous inputs of all kinds + - [ ] Property tests */ describe("PODSpecBuilder", () => { @@ -134,150 +133,3 @@ describe("PODSpecBuilder", () => { }); }); }); - -describe("PODSpecBuilder - Property tests", () => { - // Arbitraries for generating test data - const validEntryName = fc - .string() - .filter((s) => /^[a-zA-Z][a-zA-Z0-9_]*$/.test(s)) - .filter((s) => s.length <= 32); - - const podType = fc.constantFrom( - "string", - "int", - "boolean", - "date", - "eddsa_pubkey" - ); - - const entryConfigs = fc - .uniqueArray(validEntryName, { - minLength: 1, - maxLength: 10, - }) - .map((names) => - names.map((name) => ({ - name, - type: fc.sample(podType, 1)[0] as PODValueType, - })) - ); - - // Test that adding entries is commutative - test("should be commutative when adding entries", () => { - fc.assert( - fc.property(entryConfigs, (entries) => { - // Add entries in original order - const builder1 = entries.reduce( - (b, { name, type }) => b.entry(name, type), - PODSpecBuilder.create() - ); - - // Add entries in reverse order - const builder2 = [...entries] - .reverse() - .reduce( - (b, { name, type }) => b.entry(name, type), - PODSpecBuilder.create() - ); - - // The resulting specs should be equivalent - expect(builder1.spec()).toEqual(builder2.spec()); - }) - ); - }); - - // Test that picking and then omitting entries is consistent - test("should maintain consistency when picking and omitting entries", () => { - fc.assert( - fc.property( - entryConfigs, - fc.array(fc.nat(), { minLength: 1, maxLength: 5 }), // indices to pick - (entries, pickIndices) => { - const builder = entries.reduce( - (b, { name, type }) => b.entry(name, type), - PODSpecBuilder.create() - ); - const entryNames = entries.map((e) => e.name); - const pickedNames = pickIndices - .map((i) => entryNames[i % entryNames.length]) - .filter((name): name is string => name !== undefined); - - const picked = builder.pickEntries(pickedNames); - const omitted = picked.omitEntries(pickedNames); - - // Omitting all picked entries should result in empty entries - expect(Object.keys(omitted.spec().entries)).toHaveLength(0); - } - ) - ); - }); - - // Test that range checks maintain valid bounds - test("should maintain valid bounds for range checks", () => { - const dateArb = fc.date({ - min: new Date(0), - max: new Date(2100, 0, 1), - }); - - fc.assert( - fc.property(validEntryName, dateArb, dateArb, (name, date1, date2) => { - const min = date1 < date2 ? date1 : date2; - const max = date1 < date2 ? date2 : date1; - - const builder = PODSpecBuilder.create() - .entry(name, "date") - .inRange(name, { min, max }); - - const spec = builder.spec(); - const statement = Object.values(spec.statements)[0]; - - // Range bounds should be ordered correctly in the spec - if (statement?.type === "inRange") { - const minTime = BigInt(statement.inRange.min); - const maxTime = BigInt(statement.inRange.max); - expect(minTime).toBeLessThanOrEqual(maxTime); - } - }) - ); - }); - - // Test that automatic statement names are always unique - test("should generate unique automatic statement names", () => { - fc.assert( - fc.property(entryConfigs, (entries) => { - const builder = entries.reduce( - (b, { name }) => - b.entry(name, "int").inRange(name, { min: 0n, max: 10n }), - PODSpecBuilder.create() - ); - - const spec = builder.spec(); - const statementNames = Object.keys(spec.statements); - const uniqueNames = new Set(statementNames); - - expect(statementNames.length).toBe(uniqueNames.size); - }) - ); - }); - - // Test that custom statement names are always unique - test("should generate unique custom statement names", () => { - fc.assert( - fc.property(entryConfigs, fc.string(), (entries, customName) => { - const builder = entries.reduce( - (b, { name }) => - b - .entry(name, "int") - .inRange(name, { min: 0n, max: 10n }, customName), - PODSpecBuilder.create() - ); - - const spec = builder.spec(); - const statementNames = Object.keys(spec.statements); - const uniqueNames = new Set(statementNames); - - expect(statementNames.length).toBe(uniqueNames.size); - }) - ); - }); -}); 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/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/validator.spec.ts b/packages/podspec/test/processors/validator/podValidator.spec.ts similarity index 81% rename from packages/podspec/test/processors/validator/validator.spec.ts rename to packages/podspec/test/processors/validator/podValidator.spec.ts index 66dc0ef..11157da 100644 --- a/packages/podspec/test/processors/validator/validator.spec.ts +++ b/packages/podspec/test/processors/validator/podValidator.spec.ts @@ -7,7 +7,7 @@ import { } from "@pcd/pod"; import { assert, describe, expect, it } from "vitest"; import { PODSpecBuilder } from "../../../src/index.js"; -import { validate } from "../../../src/processors/validate.js"; +import { podValidator } from "../../../src/processors/validate/podValidator.js"; import { generateKeyPair } from "../../utils.js"; describe("validator", () => { @@ -29,9 +29,9 @@ describe("validator", () => { .isMemberOf(["foo"], ["foo", "bar"]); // This should pass because the entry "foo" is in the list ["foo", "bar"] - expect(validate(myPodSpecBuilder.spec()).check(myPOD)).toBe(true); + expect(podValidator(myPodSpecBuilder.spec()).check(myPOD)).toBe(true); - const result = validate(myPodSpecBuilder.spec()).validate(myPOD); + const result = podValidator(myPodSpecBuilder.spec()).validate(myPOD); if (result.isValid) { const pod = result.value; // After validation, the entries are strongly typed @@ -49,17 +49,17 @@ describe("validator", () => { // This should fail because the entry "foo" is not in the list ["baz", "quux"] const secondBuilder = myPodSpecBuilder.isMemberOf(["foo"], ["baz", "quux"]); - expect(validate(secondBuilder.spec()).check(myPOD)).toBe(false); + expect(podValidator(secondBuilder.spec()).check(myPOD)).toBe(false); // If we omit the new statement, it should pass expect( - validate(secondBuilder.omitStatements(["foo_isMemberOf_1"]).spec()).check( - myPOD - ) + podValidator( + secondBuilder.omitStatements(["foo_isMemberOf_1"]).spec() + ).check(myPOD) ).toBe(true); { - const result = validate( + const result = podValidator( secondBuilder .omitStatements(["foo_isMemberOf_1"]) .entry("num", "int") From be4df5adc39d0b3855cef3a79cd7821932b3b032 Mon Sep 17 00:00:00 2001 From: Rob Knight Date: Wed, 5 Feb 2025 13:28:36 +0100 Subject: [PATCH 17/20] Basic narrative-form end-to-end test --- packages/podspec/src/builders/group.ts | 176 +++++++++++--- packages/podspec/src/builders/pod.ts | 97 +++++--- .../validate/checks/checkInRange.ts | 4 +- packages/podspec/test/endToEnd.spec.ts | 216 ++++++++++++++++++ packages/podspec/test/fixtures.ts | 102 +++++++++ .../processors/proofRequest/proofRequest.ts | 0 6 files changed, 526 insertions(+), 69 deletions(-) create mode 100644 packages/podspec/test/endToEnd.spec.ts create mode 100644 packages/podspec/test/fixtures.ts create mode 100644 packages/podspec/test/processors/proofRequest/proofRequest.ts diff --git a/packages/podspec/src/builders/group.ts b/packages/podspec/src/builders/group.ts index 509d30a..1e0c780 100644 --- a/packages/podspec/src/builders/group.ts +++ b/packages/podspec/src/builders/group.ts @@ -35,6 +35,7 @@ import type { NotEqualsEntry, NotInRange, StatementMap, + StatementName, SupportsRangeChecks, } from "./types/statements.js"; @@ -139,13 +140,20 @@ export class PODGroupSpecBuilder< }); } - public isMemberOf>>( + public isMemberOf>, C extends string>( names: [...N], values: N["length"] extends 1 ? PODValueTypeFromTypeName>>[] : PODValueTupleForNamedEntries, N>[], - customStatementName?: string - ): PODGroupSpecBuilder { + customStatementName?: C + ): PODGroupSpecBuilder< + P, + S & { + [K in IsSingleLiteralString extends true + ? C + : StatementName]: IsMemberOf, N>; + } + > { // Check for duplicate names const uniqueNames = new Set(names); if (uniqueNames.size !== names.length) { @@ -186,7 +194,7 @@ export class PODGroupSpecBuilder< }; const baseName = customStatementName ?? `${names.join("_")}_isMemberOf`; - let statementName = baseName; + let statementName: string = baseName; let suffix = 1; while ( @@ -204,13 +212,23 @@ export class PODGroupSpecBuilder< }); } - public isNotMemberOf>>( + public isNotMemberOf>, C extends string>( names: [...N], values: N["length"] extends 1 ? PODValueTypeFromTypeName>>[] : PODValueTupleForNamedEntries, N>[], - customStatementName?: string - ): PODGroupSpecBuilder { + customStatementName?: C + ): PODGroupSpecBuilder< + P, + S & { + [K in IsSingleLiteralString extends true + ? C + : StatementName]: IsNotMemberOf< + AllPODEntries

, + N + >; + } + > { // Check for duplicate names const uniqueNames = new Set(names); if (uniqueNames.size !== names.length) { @@ -251,7 +269,7 @@ export class PODGroupSpecBuilder< }; const baseName = customStatementName ?? `${names.join("_")}_isNotMemberOf`; - let statementName = baseName; + let statementName: string = baseName; let suffix = 1; while ( @@ -272,6 +290,7 @@ export class PODGroupSpecBuilder< public inRange< N extends keyof EntriesOfType, SupportsRangeChecks> & string, + C extends string, >( name: N, range: { @@ -286,8 +305,18 @@ export class PODGroupSpecBuilder< : bigint : Date | bigint; }, - customStatementName?: string - ): PODGroupSpecBuilder { + customStatementName?: C + ): PODGroupSpecBuilder< + P, + S & { + [K in IsSingleLiteralString extends true + ? C + : StatementName<[N & string], "inRange", S>]: InRange< + AllPODEntries

, + N + >; + } + > { // Check that the entry exists const [podName, entryName] = name.split("."); if ( @@ -349,7 +378,7 @@ export class PODGroupSpecBuilder< }; const baseName = customStatementName ?? `${name}_inRange`; - let statementName = baseName; + let statementName: string = baseName; let suffix = 1; while ( @@ -370,14 +399,25 @@ export class PODGroupSpecBuilder< 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?: string - ): PODGroupSpecBuilder { + customStatementName?: C + ): PODGroupSpecBuilder< + P, + S & { + [K in IsSingleLiteralString extends true + ? C + : StatementName<[N & string], "notInRange", S>]: NotInRange< + AllPODEntries

, + N + >; + } + > { // Check that the entry exists const [podName, entryName] = name.split("."); if ( @@ -439,7 +479,7 @@ export class PODGroupSpecBuilder< }; const baseName = customStatementName ?? `${name}_notInRange`; - let statementName = baseName; + let statementName: string = baseName; let suffix = 1; while ( @@ -460,11 +500,23 @@ export class PODGroupSpecBuilder< public greaterThan< N1 extends keyof AllPODEntries

& string, N2 extends keyof EntriesOfType, EntryType> & string, + C extends string, >( name1: N1, name2: N2, - customStatementName?: string - ): PODGroupSpecBuilder { + customStatementName?: C + ): PODGroupSpecBuilder< + P, + S & { + [K in IsSingleLiteralString extends true + ? C + : StatementName< + [N1 & string, N2 & string], + "greaterThan", + S + >]: GreaterThan, N1, N2>; + } + > { // Check that both entries exist const [pod1, entry1] = name1.split("."); const [pod2, entry2] = name2.split("."); @@ -507,7 +559,7 @@ export class PODGroupSpecBuilder< }; const baseName = customStatementName ?? `${name1}_${name2}_greaterThan`; - let statementName = baseName; + let statementName: string = baseName; let suffix = 1; while ( @@ -528,11 +580,23 @@ export class PODGroupSpecBuilder< public greaterThanEq< N1 extends keyof AllPODEntries

& string, N2 extends keyof EntriesOfType, EntryType> & string, + C extends string, >( name1: N1, name2: N2, - customStatementName?: string - ): PODGroupSpecBuilder { + customStatementName?: C + ): PODGroupSpecBuilder< + P, + S & { + [K in IsSingleLiteralString extends true + ? C + : StatementName< + [N1 & string, N2 & string], + "greaterThanEq", + S + >]: GreaterThanEq, N1, N2>; + } + > { // Check that both entries exist const [pod1, entry1] = name1.split("."); const [pod2, entry2] = name2.split("."); @@ -575,7 +639,7 @@ export class PODGroupSpecBuilder< }; const baseName = customStatementName ?? `${name1}_${name2}_greaterThanEq`; - let statementName = baseName; + let statementName: string = baseName; let suffix = 1; while ( @@ -596,11 +660,23 @@ export class PODGroupSpecBuilder< public lessThan< N1 extends keyof AllPODEntries

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

, + N1, + N2 + >; + } + > { // Check that both entries exist const [pod1, entry1] = name1.split("."); const [pod2, entry2] = name2.split("."); @@ -643,7 +719,7 @@ export class PODGroupSpecBuilder< }; const baseName = customStatementName ?? `${name1}_${name2}_lessThan`; - let statementName = baseName; + let statementName: string = baseName; let suffix = 1; while ( @@ -664,11 +740,23 @@ export class PODGroupSpecBuilder< public lessThanEq< N1 extends keyof AllPODEntries

& string, N2 extends keyof EntriesOfType, EntryType> & string, + C extends string, >( name1: N1, name2: N2, - customStatementName?: string - ): PODGroupSpecBuilder { + customStatementName?: C + ): PODGroupSpecBuilder< + P, + S & { + [K in IsSingleLiteralString extends true + ? C + : StatementName< + [N1 & string, N2 & string], + "lessThanEq", + S + >]: LessThanEq, N1, N2>; + } + > { // Check that both entries exist const [pod1, entry1] = name1.split("."); const [pod2, entry2] = name2.split("."); @@ -711,7 +799,7 @@ export class PODGroupSpecBuilder< }; const baseName = customStatementName ?? `${name1}_${name2}_lessThanEq`; - let statementName = baseName; + let statementName: string = baseName; let suffix = 1; while ( @@ -732,11 +820,23 @@ export class PODGroupSpecBuilder< public equalsEntry< N1 extends keyof AllPODEntries

& string, N2 extends keyof EntriesOfType, EntryType> & string, + C extends string, >( name1: N1, name2: N2, - customStatementName?: string - ): PODGroupSpecBuilder { + customStatementName?: C + ): PODGroupSpecBuilder< + P, + S & { + [K in IsSingleLiteralString extends true + ? C + : StatementName< + [N1 & string, N2 & string], + "equalsEntry", + S + >]: EqualsEntry, N1, N2>; + } + > { // Check that both entries exist const [pod1, entry1] = name1.split("."); const [pod2, entry2] = name2.split("."); @@ -781,7 +881,7 @@ export class PODGroupSpecBuilder< }; const baseName = customStatementName ?? `${name1}_${name2}_equalsEntry`; - let statementName = baseName; + let statementName: string = baseName; let suffix = 1; while ( @@ -802,11 +902,23 @@ export class PODGroupSpecBuilder< public notEqualsEntry< N1 extends keyof AllPODEntries

& string, N2 extends keyof EntriesOfType, EntryType> & string, + C extends string, >( name1: N1, name2: N2, - customStatementName?: string - ): PODGroupSpecBuilder { + customStatementName?: C + ): PODGroupSpecBuilder< + P, + S & { + [K in IsSingleLiteralString extends true + ? C + : StatementName< + [N1 & string, N2 & string], + "notEqualsEntry", + S + >]: NotEqualsEntry, N1, N2>; + } + > { // Check that both entries exist const [pod1, entry1] = name1.split("."); const [pod2, entry2] = name2.split("."); @@ -849,7 +961,7 @@ export class PODGroupSpecBuilder< }; const baseName = customStatementName ?? `${name1}_${name2}_notEqualsEntry`; - let statementName = baseName; + let statementName: string = baseName; let suffix = 1; while ( diff --git a/packages/podspec/src/builders/pod.ts b/packages/podspec/src/builders/pod.ts index 137d2aa..4f1e098 100644 --- a/packages/podspec/src/builders/pod.ts +++ b/packages/podspec/src/builders/pod.ts @@ -257,21 +257,20 @@ export class PODSpecBuilder< * @param values - The values to be constrained to * @returns A new PODSpecBuilder with the statement added */ - public isMemberOf>( + public isMemberOf, C extends string>( names: [...N], values: N["length"] extends 1 ? PODValueTypeFromTypeName< (E & VirtualEntries)[N[0] & keyof (E & VirtualEntries)] >[] : PODValueTupleForNamedEntries[], - customStatementName?: string + customStatementName?: C ): PODSpecBuilder< E, S & { - [K in StatementName]: IsMemberOf< - E & VirtualEntries, - N - >; + [K in IsSingleLiteralString extends true + ? C + : StatementName]: IsMemberOf; } > { // Check for duplicate names @@ -311,7 +310,7 @@ export class PODSpecBuilder< }; const baseName = customStatementName ?? `${names.join("_")}_isMemberOf`; - let statementName = baseName; + let statementName: string = baseName; let suffix = 1; while ( @@ -344,17 +343,21 @@ export class PODSpecBuilder< * @param values - The values to be constrained to * @returns A new PODSpecBuilder with the statement added */ - public isNotMemberOf>( + public isNotMemberOf, C extends string>( names: [...N], values: N["length"] extends 1 ? PODValueTypeFromTypeName< (E & VirtualEntries)[N[0] & keyof (E & VirtualEntries)] >[] : PODValueTupleForNamedEntries[], - customStatementName?: string + customStatementName?: C ): PODSpecBuilder< E, - S & { [K in StatementName]: IsNotMemberOf } + S & { + [K in IsSingleLiteralString extends true + ? C + : StatementName]: IsNotMemberOf; + } > { // Check that all names exist in entries for (const name of names) { @@ -400,7 +403,7 @@ export class PODSpecBuilder< }; const baseName = customStatementName ?? `${names.join("_")}_isNotMemberOf`; - let statementName = baseName; + let statementName: string = baseName; let suffix = 1; while ( @@ -427,17 +430,20 @@ export class PODSpecBuilder< */ 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?: string + customStatementName?: C ): PODSpecBuilder< E, S & { - [K in StatementName<[N & string], "inRange", S>]: InRange< + [K in IsSingleLiteralString extends true + ? C + : StatementName<[N & string], "inRange", S>]: InRange< E & VirtualEntries, N >; @@ -498,7 +504,7 @@ export class PODSpecBuilder< }; const baseName = customStatementName ?? `${name}_inRange`; - let statementName = baseName; + let statementName: string = baseName; let suffix = 1; while ( @@ -525,17 +531,20 @@ export class PODSpecBuilder< */ 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?: string + customStatementName?: C ): PODSpecBuilder< E, S & { - [K in StatementName<[N & string], "notInRange", S>]: NotInRange< + [K in IsSingleLiteralString extends true + ? C + : StatementName<[N & string], "notInRange", S>]: NotInRange< E & VirtualEntries, N >; @@ -597,7 +606,7 @@ export class PODSpecBuilder< }; const baseName = customStatementName ?? `${name}_notInRange`; - let statementName = baseName; + let statementName: string = baseName; let suffix = 1; while ( @@ -622,14 +631,17 @@ export class PODSpecBuilder< (E & VirtualEntries)[N1] > & string, + C extends string, >( name1: N1, name2: Exclude, - customStatementName?: string + customStatementName?: C ): PODSpecBuilder< E, S & { - [K in StatementName<[N1, N2], "equalsEntry", S>]: EqualsEntry; + [K in IsSingleLiteralString extends true + ? C + : StatementName<[N1, N2], "equalsEntry", S>]: EqualsEntry; } > { // Check that both names exist in entries @@ -658,7 +670,7 @@ export class PODSpecBuilder< } satisfies EqualsEntry; const baseName = customStatementName ?? `${name1}_${name2}_equalsEntry`; - let statementName = baseName; + let statementName: string = baseName; let suffix = 1; while ( @@ -683,14 +695,17 @@ export class PODSpecBuilder< (E & VirtualEntries)[N1] > & string, + C extends string, >( name1: N1, name2: Exclude, - customStatementName?: string + customStatementName?: C ): PODSpecBuilder< E, S & { - [K in StatementName<[N1, N2], "notEqualsEntry", S>]: NotEqualsEntry< + [K in IsSingleLiteralString extends true + ? C + : StatementName<[N1, N2], "notEqualsEntry", S>]: NotEqualsEntry< E, N1, N2 @@ -723,7 +738,7 @@ export class PODSpecBuilder< } satisfies NotEqualsEntry; const baseName = customStatementName ?? `${name1}_${name2}_notEqualsEntry`; - let statementName = baseName; + let statementName: string = baseName; let suffix = 1; while ( @@ -748,14 +763,17 @@ export class PODSpecBuilder< (E & VirtualEntries)[N1] > & string, + C extends string, >( name1: N1, name2: Exclude, - customStatementName?: string + customStatementName?: C ): PODSpecBuilder< E, S & { - [K in StatementName<[N1, N2], "greaterThan", S>]: GreaterThan; + [K in IsSingleLiteralString extends true + ? C + : StatementName<[N1, N2], "greaterThan", S>]: GreaterThan; } > { // Check that both names exist in entries @@ -784,7 +802,7 @@ export class PODSpecBuilder< } satisfies GreaterThan; const baseName = customStatementName ?? `${name1}_${name2}_greaterThan`; - let statementName = baseName; + let statementName: string = baseName; let suffix = 1; while ( @@ -809,14 +827,17 @@ export class PODSpecBuilder< (E & VirtualEntries)[N1] > & string, + C extends string, >( name1: N1, name2: Exclude, - customStatementName?: string + customStatementName?: C ): PODSpecBuilder< E, S & { - [K in StatementName<[N1, N2], "greaterThanEq", S>]: GreaterThanEq< + [K in IsSingleLiteralString extends true + ? C + : StatementName<[N1, N2], "greaterThanEq", S>]: GreaterThanEq< E, N1, N2 @@ -849,7 +870,7 @@ export class PODSpecBuilder< } satisfies GreaterThanEq; const baseName = customStatementName ?? `${name1}_${name2}_greaterThanEq`; - let statementName = baseName; + let statementName: string = baseName; let suffix = 1; while ( @@ -874,14 +895,17 @@ export class PODSpecBuilder< (E & VirtualEntries)[N1] > & string, + C extends string, >( name1: N1, name2: Exclude, - customStatementName?: string + customStatementName?: C ): PODSpecBuilder< E, S & { - [K in StatementName<[N1, N2], "lessThan", S>]: LessThan; + [K in IsSingleLiteralString extends true + ? C + : StatementName<[N1, N2], "lessThan", S>]: LessThan; } > { // Check that both names exist in entries @@ -910,7 +934,7 @@ export class PODSpecBuilder< } satisfies LessThan; const baseName = customStatementName ?? `${name1}_${name2}_lessThan`; - let statementName = baseName; + let statementName: string = baseName; let suffix = 1; while ( @@ -935,14 +959,17 @@ export class PODSpecBuilder< (E & VirtualEntries)[N1] > & string, + C extends string, >( name1: N1, name2: Exclude, - customStatementName?: string + customStatementName?: C ): PODSpecBuilder< E, S & { - [K in StatementName<[N1, N2], "lessThanEq", S>]: LessThanEq; + [K in IsSingleLiteralString extends true + ? C + : StatementName<[N1, N2], "lessThanEq", S>]: LessThanEq; } > { // Check that both names exist in entries @@ -971,7 +998,7 @@ export class PODSpecBuilder< } satisfies LessThanEq; const baseName = customStatementName ?? `${name1}_${name2}_lessThanEq`; - let statementName = baseName; + let statementName: string = baseName; let suffix = 1; while ( diff --git a/packages/podspec/src/processors/validate/checks/checkInRange.ts b/packages/podspec/src/processors/validate/checks/checkInRange.ts index 9101a34..83aff47 100644 --- a/packages/podspec/src/processors/validate/checks/checkInRange.ts +++ b/packages/podspec/src/processors/validate/checks/checkInRange.ts @@ -35,10 +35,10 @@ export function checkInRange( const isDate = entry.type === "date"; const min = isDate - ? new Date(statement.inRange.min) + ? new Date(parseInt(statement.inRange.min)) : BigInt(statement.inRange.min); const max = isDate - ? new Date(statement.inRange.max) + ? new Date(parseInt(statement.inRange.max)) : BigInt(statement.inRange.max); if (isPODArithmeticValue(entry)) { diff --git a/packages/podspec/test/endToEnd.spec.ts b/packages/podspec/test/endToEnd.spec.ts new file mode 100644 index 0000000..0e96770 --- /dev/null +++ b/packages/podspec/test/endToEnd.spec.ts @@ -0,0 +1,216 @@ +import 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 { 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() + .entry("name", "string") + .entry("date_of_birth", "date") + .entry("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: "john.doe@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); + } + }); +}); 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/processors/proofRequest/proofRequest.ts b/packages/podspec/test/processors/proofRequest/proofRequest.ts new file mode 100644 index 0000000..e69de29 From 989c386489db16af78835ec3b9c9986283334b6d Mon Sep 17 00:00:00 2001 From: Rob Knight Date: Thu, 6 Feb 2025 08:56:09 +0100 Subject: [PATCH 18/20] Separate out un-typed implementation of builders, with property-based tests --- packages/podspec/src/builders/group.ts | 687 +------------- packages/podspec/src/builders/pod.ts | 663 ++------------ .../podspec/src/builders/types/statements.ts | 30 + packages/podspec/src/builders/untypedGroup.ts | 805 +++++++++++++++++ packages/podspec/src/builders/untypedPod.ts | 836 ++++++++++++++++++ .../test/builders/fast-check/pod.spec.ts | 129 +++ 6 files changed, 1895 insertions(+), 1255 deletions(-) create mode 100644 packages/podspec/src/builders/untypedGroup.ts create mode 100644 packages/podspec/src/builders/untypedPod.ts create mode 100644 packages/podspec/test/builders/fast-check/pod.spec.ts diff --git a/packages/podspec/src/builders/group.ts b/packages/podspec/src/builders/group.ts index 1e0c780..ec6206e 100644 --- a/packages/podspec/src/builders/group.ts +++ b/packages/podspec/src/builders/group.ts @@ -1,18 +1,6 @@ -import { - type PODName, - POD_DATE_MAX, - POD_DATE_MIN, - POD_INT_MAX, - POD_INT_MIN, - checkPODName, -} from "@pcd/pod"; +import type { PODName } from "@pcd/pod"; import type { IsSingleLiteralString } from "../shared/types.js"; -import { type PODSpec, PODSpecBuilder, virtualEntries } from "./pod.js"; -import { - convertValuesToStringTuples, - supportsRangeChecks, - validateRange, -} from "./shared.js"; +import { type PODSpec, PODSpecBuilder } from "./pod.js"; import type { EntriesOfType, EntryKeys, @@ -38,6 +26,7 @@ import type { StatementName, SupportsRangeChecks, } from "./types/statements.js"; +import { UntypedPODGroupSpecBuilder } from "./untypedGroup.js"; export type NamedPODSpecs = Record>; @@ -102,21 +91,19 @@ export class PODGroupSpecBuilder< P extends NamedPODSpecs, S extends StatementMap, > { - readonly #spec: PODGroupSpec; + readonly #innerBuilder: UntypedPODGroupSpecBuilder; - private constructor(spec: PODGroupSpec) { - this.#spec = spec; + private constructor(innerBuilder: UntypedPODGroupSpecBuilder) { + this.#innerBuilder = innerBuilder; } - public static create() { - return new PODGroupSpecBuilder({ - pods: {}, - statements: {}, - }); + // eslint-disable-next-line @typescript-eslint/no-empty-object-type + public static create(): PODGroupSpecBuilder<{}, {}> { + return new PODGroupSpecBuilder(UntypedPODGroupSpecBuilder.create()); } public spec(): PODGroupSpec { - return structuredClone(this.#spec); + return this.#innerBuilder.spec() as PODGroupSpec; } public pod< @@ -127,17 +114,7 @@ export class PODGroupSpecBuilder< name: IsSingleLiteralString extends true ? N : never, spec: Spec ): PODGroupSpecBuilder { - 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 PODGroupSpecBuilder({ - ...this.#spec, - pods: { ...this.#spec.pods, [name]: spec } as unknown as NewPods, - }); + return new PODGroupSpecBuilder(this.#innerBuilder.pod(name, spec)); } public isMemberOf>, C extends string>( @@ -154,62 +131,9 @@ export class PODGroupSpecBuilder< : StatementName]: IsMemberOf, N>; } > { - // 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, - ] - ), - ]) + return new PODGroupSpecBuilder( + this.#innerBuilder.isMemberOf(names, values, customStatementName) ); - - for (const name of names) { - if (!Object.prototype.hasOwnProperty.call(allEntries, name)) { - throw new Error(`Entry "${name}" does not exist`); - } - } - - const statement: IsMemberOf, N> = { - entries: names, - type: "isMemberOf", - isMemberOf: convertValuesToStringTuples( - names, - values, - allEntries as Record - ), - }; - - 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 PODGroupSpecBuilder({ - ...this.#spec, - statements: { - ...this.#spec.statements, - [statementName]: statement, - }, - }); } public isNotMemberOf>, C extends string>( @@ -229,62 +153,9 @@ export class PODGroupSpecBuilder< >; } > { - // 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, - ] - ), - ]) + return new PODGroupSpecBuilder( + this.#innerBuilder.isNotMemberOf(names, values, customStatementName) ); - - for (const name of names) { - if (!Object.prototype.hasOwnProperty.call(allEntries, name)) { - throw new Error(`Entry "${name}" does not exist`); - } - } - - const statement: IsNotMemberOf, N> = { - entries: names, - type: "isNotMemberOf", - isNotMemberOf: convertValuesToStringTuples( - names, - values, - allEntries as Record - ), - }; - - 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 PODGroupSpecBuilder({ - ...this.#spec, - statements: { - ...this.#spec.statements, - [statementName]: statement, - }, - }); } public inRange< @@ -317,83 +188,9 @@ export class PODGroupSpecBuilder< >; } > { - // 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: InRange, N> = { - 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 PODGroupSpecBuilder({ - ...this.#spec, - statements: { - ...this.#spec.statements, - [statementName]: statement, - }, - }); + return new PODGroupSpecBuilder( + this.#innerBuilder.inRange(name, range, customStatementName) + ); } public notInRange< @@ -418,83 +215,9 @@ export class PODGroupSpecBuilder< >; } > { - // 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: NotInRange, N> = { - 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 PODGroupSpecBuilder({ - ...this.#spec, - statements: { - ...this.#spec.statements, - [statementName]: statement, - }, - }); + return new PODGroupSpecBuilder( + this.#innerBuilder.notInRange(name, range, customStatementName) + ); } public greaterThan< @@ -517,64 +240,9 @@ export class PODGroupSpecBuilder< >]: GreaterThan, N1, N2>; } > { - // 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 as string) === (name2 as string)) { - 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: GreaterThan, N1, N2> = { - 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 PODGroupSpecBuilder({ - ...this.#spec, - statements: { - ...this.#spec.statements, - [statementName]: statement, - }, - }); + return new PODGroupSpecBuilder( + this.#innerBuilder.greaterThan(name1, name2, customStatementName) + ); } public greaterThanEq< @@ -597,64 +265,9 @@ export class PODGroupSpecBuilder< >]: GreaterThanEq, N1, N2>; } > { - // 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 as string) === (name2 as string)) { - 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: GreaterThanEq, N1, N2> = { - 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 PODGroupSpecBuilder({ - ...this.#spec, - statements: { - ...this.#spec.statements, - [statementName]: statement, - }, - }); + return new PODGroupSpecBuilder( + this.#innerBuilder.greaterThanEq(name1, name2, customStatementName) + ); } public lessThan< @@ -677,64 +290,9 @@ export class PODGroupSpecBuilder< >; } > { - // 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 as string) === (name2 as string)) { - 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: LessThan, N1, N2> = { - 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 PODGroupSpecBuilder({ - ...this.#spec, - statements: { - ...this.#spec.statements, - [statementName]: statement, - }, - }); + return new PODGroupSpecBuilder( + this.#innerBuilder.lessThan(name1, name2, customStatementName) + ); } public lessThanEq< @@ -757,64 +315,9 @@ export class PODGroupSpecBuilder< >]: LessThanEq, N1, N2>; } > { - // 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 as string) === (name2 as string)) { - 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: LessThanEq, N1, N2> = { - 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 PODGroupSpecBuilder({ - ...this.#spec, - statements: { - ...this.#spec.statements, - [statementName]: statement, - }, - }); + return new PODGroupSpecBuilder( + this.#innerBuilder.lessThanEq(name1, name2, customStatementName) + ); } public equalsEntry< @@ -837,66 +340,9 @@ export class PODGroupSpecBuilder< >]: EqualsEntry, N1, N2>; } > { - // 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 as string) === (name2 as string)) { - 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: EqualsEntry, N1, N2> = { - 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 PODGroupSpecBuilder({ - ...this.#spec, - statements: { - ...this.#spec.statements, - [statementName]: statement, - }, - }); + return new PODGroupSpecBuilder( + this.#innerBuilder.equalsEntry(name1, name2, customStatementName) + ); } public notEqualsEntry< @@ -919,64 +365,9 @@ export class PODGroupSpecBuilder< >]: NotEqualsEntry, N1, N2>; } > { - // 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 as string) === (name2 as string)) { - 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: NotEqualsEntry, N1, N2> = { - 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 PODGroupSpecBuilder({ - ...this.#spec, - statements: { - ...this.#spec.statements, - [statementName]: statement, - }, - }); + return new PODGroupSpecBuilder( + this.#innerBuilder.notEqualsEntry(name1, name2, customStatementName) + ); } } diff --git a/packages/podspec/src/builders/pod.ts b/packages/podspec/src/builders/pod.ts index 4f1e098..a62bd10 100644 --- a/packages/podspec/src/builders/pod.ts +++ b/packages/podspec/src/builders/pod.ts @@ -1,18 +1,5 @@ -import { - POD_DATE_MAX, - POD_DATE_MIN, - POD_INT_MAX, - POD_INT_MIN, - checkPODName, -} from "@pcd/pod"; import type { IsJsonSafe } from "../shared/jsonSafe.js"; import type { IsSingleLiteralString } from "../shared/types.js"; -import { - convertValuesToStringTuples, - deepFreeze, - supportsRangeChecks, - validateRange, -} from "./shared.js"; import type { EntriesOfType, EntryKeys, @@ -37,6 +24,7 @@ import type { StatementMap, StatementName, } from "./types/statements.js"; +import { UntypedPODSpecBuilder } from "./untypedPod.js"; /** @todo @@ -50,14 +38,12 @@ import type { - [x] rename away from v2 suffix - [x] validate entry names - [x] validate isMemberOf/isNotMemberOf parameters - - [ ] handle multiple/incompatible range checks on the same entry + - [ ] 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 - - [ ] untyped entries? - - [ ] optional entries? - - [ ] solve the problem whereby some methods are incorrectly typed if we've - added loosely-typed entries (this seems to be a problem with omit/pick?) + - [ ] optional/nullable entries + - [ ] restrict gt/lt/etc checks to numeric types? */ export const virtualEntries: VirtualEntries = { @@ -118,31 +104,23 @@ export class PODSpecBuilder< // eslint-disable-next-line @typescript-eslint/no-empty-object-type S extends StatementMap = {}, > { - readonly #spec: PODSpec; + readonly #innerBuilder: UntypedPODSpecBuilder; - private constructor(spec: PODSpec) { - this.#spec = spec; + private constructor(innerBuilder: UntypedPODSpecBuilder) { + this.#innerBuilder = innerBuilder; } - public static create() { - return new PODSpecBuilder({ - entries: {}, - statements: {}, - }); + // eslint-disable-next-line @typescript-eslint/no-empty-object-type + public static create(): PODSpecBuilder<{}, {}> { + return new PODSpecBuilder(UntypedPODSpecBuilder.create()); } public spec(): PODSpec { - return deepFreeze(this.#spec); + return this.#innerBuilder.spec() as PODSpec; } public toJSON(): string { - return JSON.stringify( - { - ...this.#spec, - }, - null, - 2 - ); + return this.#innerBuilder.toJSON(); } public entry< @@ -153,21 +131,9 @@ export class PODSpecBuilder< key: IsSingleLiteralString extends true ? Exclude : never, type: V ): PODSpecBuilder { - 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 PODSpecBuilder({ - ...this.#spec, - entries: { - ...this.#spec.entries, - [key]: type, - } as NewEntries, - statements: this.#spec.statements, - }); + return new PODSpecBuilder( + this.#innerBuilder.entry(key, type) + ); } /** @@ -178,20 +144,7 @@ export class PODSpecBuilder< >( keys: K[] ): PODSpecBuilder, Concrete>> { - return new PODSpecBuilder({ - entries: Object.fromEntries( - Object.entries(this.#spec.entries).filter(([key]) => - keys.includes(key as K) - ) - ) as Pick, - statements: Object.fromEntries( - Object.entries(this.#spec.statements).filter(([_key, statement]) => { - return (statement.entries as EntryKeys).every((entry) => - keys.includes(entry as K) - ); - }) - ) as Concrete>, - }); + return new PODSpecBuilder(this.#innerBuilder.pickEntries(keys)); } public omitEntries< @@ -199,47 +152,19 @@ export class PODSpecBuilder< >( keys: K[] ): PODSpecBuilder, Concrete>> { - return new PODSpecBuilder({ - ...this.#spec, - entries: Object.fromEntries( - Object.entries(this.#spec.entries).filter( - ([key]) => !keys.includes(key as K) - ) - ) as Omit, - statements: Object.fromEntries( - Object.entries(this.#spec.statements).filter(([_key, statement]) => { - return (statement.entries as EntryKeys).every( - (entry) => !keys.includes(entry as K) - ); - }) - ) as Concrete>, - }); + return new PODSpecBuilder(this.#innerBuilder.omitEntries(keys)); } - public pickStatements( + public pickStatements( keys: K[] ): PODSpecBuilder>> { - return new PODSpecBuilder({ - ...this.#spec, - statements: Object.fromEntries( - Object.entries(this.#spec.statements).filter(([key]) => - keys.includes(key as K) - ) - ) as Concrete>, - }); + return new PODSpecBuilder(this.#innerBuilder.pickStatements(keys)); } - public omitStatements( + public omitStatements( keys: K[] ): PODSpecBuilder>> { - return new PODSpecBuilder({ - ...this.#spec, - statements: Object.fromEntries( - Object.entries(this.#spec.statements).filter( - ([key]) => !keys.includes(key as K) - ) - ) as Concrete>, - }); + return new PODSpecBuilder(this.#innerBuilder.omitStatements(keys)); } /** @@ -273,59 +198,9 @@ export class PODSpecBuilder< : StatementName]: IsMemberOf; } > { - // 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 PODSpecBuilder({ - ...this.#spec, - statements: { - ...this.#spec.statements, - [statementName]: statement, - }, - }); + return new PODSpecBuilder( + this.#innerBuilder.isMemberOf(names, values, customStatementName) + ); } /** @@ -359,66 +234,9 @@ export class PODSpecBuilder< : StatementName]: IsNotMemberOf; } > { - // 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 PODSpecBuilder({ - ...this.#spec, - statements: { - ...this.#spec.statements, - [statementName]: statement, - }, - }); + return new PODSpecBuilder( + this.#innerBuilder.isNotMemberOf(names, values, customStatementName) + ); } /** @@ -449,77 +267,9 @@ export class PODSpecBuilder< >; } > { - // 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 PODSpecBuilder({ - ...this.#spec, - statements: { - ...this.#spec.statements, - [statementName]: statement, - }, - }); + return new PODSpecBuilder( + this.#innerBuilder.inRange(name, range, customStatementName) + ); } /** @@ -550,78 +300,9 @@ export class PODSpecBuilder< >; } > { - // 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 PODSpecBuilder({ - ...this.#spec, - statements: { - ...this.#spec.statements, - [statementName]: statement, - }, - }); + return new PODSpecBuilder( + this.#innerBuilder.notInRange(name, range, customStatementName) + ); } public equalsEntry< @@ -644,48 +325,9 @@ export class PODSpecBuilder< : StatementName<[N1, N2], "equalsEntry", S>]: EqualsEntry; } > { - // 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 as string) === (name2 as string)) { - throw new Error("Entry names must be different"); - } - if ((this.#spec.entries[name1] as string) !== 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 PODSpecBuilder({ - ...this.#spec, - statements: { - ...this.#spec.statements, - [statementName]: statement, - }, - }); + return new PODSpecBuilder( + this.#innerBuilder.equalsEntry(name1, name2, customStatementName) + ); } public notEqualsEntry< @@ -712,48 +354,9 @@ export class PODSpecBuilder< >; } > { - // 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 as string) === (name2 as string)) { - throw new Error("Entry names must be different"); - } - if ((this.#spec.entries[name1] as string) !== 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 PODSpecBuilder({ - ...this.#spec, - statements: { - ...this.#spec.statements, - [statementName]: statement, - }, - }); + return new PODSpecBuilder( + this.#innerBuilder.notEqualsEntry(name1, name2, customStatementName) + ); } public greaterThan< @@ -776,48 +379,9 @@ export class PODSpecBuilder< : StatementName<[N1, N2], "greaterThan", S>]: GreaterThan; } > { - // 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 as string) === (name2 as string)) { - throw new Error("Entry names must be different"); - } - if ((this.#spec.entries[name1] as string) !== 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 PODSpecBuilder({ - ...this.#spec, - statements: { - ...this.#spec.statements, - [statementName]: statement, - }, - }); + return new PODSpecBuilder( + this.#innerBuilder.greaterThan(name1, name2, customStatementName) + ); } public greaterThanEq< @@ -844,48 +408,9 @@ export class PODSpecBuilder< >; } > { - // 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 as string) === (name2 as string)) { - throw new Error("Entry names must be different"); - } - if ((this.#spec.entries[name1] as string) !== 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 PODSpecBuilder({ - ...this.#spec, - statements: { - ...this.#spec.statements, - [statementName]: statement, - }, - }); + return new PODSpecBuilder( + this.#innerBuilder.greaterThanEq(name1, name2, customStatementName) + ); } public lessThan< @@ -908,48 +433,9 @@ export class PODSpecBuilder< : StatementName<[N1, N2], "lessThan", S>]: LessThan; } > { - // 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 as string) === (name2 as string)) { - throw new Error("Entry names must be different"); - } - if ((this.#spec.entries[name1] as string) !== 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 PODSpecBuilder({ - ...this.#spec, - statements: { - ...this.#spec.statements, - [statementName]: statement, - }, - }); + return new PODSpecBuilder( + this.#innerBuilder.lessThan(name1, name2, customStatementName) + ); } public lessThanEq< @@ -972,48 +458,9 @@ export class PODSpecBuilder< : StatementName<[N1, N2], "lessThanEq", S>]: LessThanEq; } > { - // 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 as string) === (name2 as string)) { - throw new Error("Entry names must be different"); - } - if ((this.#spec.entries[name1] as string) !== 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 PODSpecBuilder({ - ...this.#spec, - statements: { - ...this.#spec.statements, - [statementName]: statement, - }, - }); + return new PODSpecBuilder( + this.#innerBuilder.lessThanEq(name1, name2, customStatementName) + ); } } @@ -1078,9 +525,11 @@ if (import.meta.vitest) { "my_int_my_other_int_lessThanEq", ]); - builderWithEntries - // @ts-expect-error entry does not exist - .isMemberOf(["non_existent_entry"], ["foo", "bar"]); + expect(() => + builderWithEntries + // @ts-expect-error entry does not exist + .isMemberOf(["non_existent_entry"], ["foo", "bar"]) + ).toThrow(); }); }); } diff --git a/packages/podspec/src/builders/types/statements.ts b/packages/podspec/src/builders/types/statements.ts index 261938f..11a82ea 100644 --- a/packages/podspec/src/builders/types/statements.ts +++ b/packages/podspec/src/builders/types/statements.ts @@ -31,6 +31,9 @@ export 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[], @@ -40,6 +43,9 @@ export 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< @@ -69,6 +75,9 @@ export 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, @@ -78,6 +87,9 @@ export 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, @@ -87,6 +99,9 @@ export type EqualsEntry< 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, @@ -96,6 +111,9 @@ export type NotEqualsEntry< 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, @@ -105,6 +123,9 @@ export type GreaterThan< 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, @@ -114,6 +135,9 @@ export type GreaterThanEq< 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, @@ -123,6 +147,9 @@ export type LessThan< 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, @@ -132,6 +159,9 @@ export type LessThanEq< 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 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..c55b898 --- /dev/null +++ b/packages/podspec/src/builders/untypedPod.ts @@ -0,0 +1,836 @@ +/* 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, + }); + } + + /** + * 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/test/builders/fast-check/pod.spec.ts b/packages/podspec/test/builders/fast-check/pod.spec.ts new file mode 100644 index 0000000..9796312 --- /dev/null +++ b/packages/podspec/test/builders/fast-check/pod.spec.ts @@ -0,0 +1,129 @@ +import { fc, test } from "@fast-check/vitest"; +import { POD_NAME_REGEX } from "@pcd/pod"; +import { expect } from "vitest"; +import type { PODValueType } from "../../../src/builders/types/entries.js"; +import { UntypedPODSpecBuilder } from "../../../src/builders/untypedPod.js"; + +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" + ) +); + +test("UntypedPODSpecBuilder entries validation", () => { + // Valid entry types are fixed strings + const validEntryType = fc.constantFrom( + "string", + "int", + "boolean", + "date", + "bytes", + "cryptographic", + "null", + "eddsa_pubkey" + ); + + fc.assert( + fc.property( + fc.dictionary( + validEntryName, + validEntryType as fc.Arbitrary + ), + (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); + }); + } + ) + ); +}); From 7a70949e7e69bc3ddc88b1e79207af431d31419c Mon Sep 17 00:00:00 2001 From: Rob Knight Date: Thu, 6 Feb 2025 15:39:34 +0100 Subject: [PATCH 19/20] More complete end-to-end test --- packages/podspec/package.json | 2 +- packages/podspec/src/processors/db/podDB.ts | 16 +- .../src/processors/proof/proofRequest.ts | 13 ++ .../src/processors/validate/podValidator.ts | 9 +- .../test/builders/fast-check/definitions.ts | 67 +++++++ .../test/builders/fast-check/pod.spec.ts | 61 ++---- .../builders/fast-check/statements.spec.ts | 86 +++++++++ packages/podspec/test/endToEnd.spec.ts | 118 +++++++++++- pnpm-lock.yaml | 174 +++++++++++++++++- 9 files changed, 477 insertions(+), 69 deletions(-) create mode 100644 packages/podspec/src/processors/proof/proofRequest.ts create mode 100644 packages/podspec/test/builders/fast-check/definitions.ts create mode 100644 packages/podspec/test/builders/fast-check/statements.spec.ts diff --git a/packages/podspec/package.json b/packages/podspec/package.json index bcb3bcd..9580a63 100644 --- a/packages/podspec/package.json +++ b/packages/podspec/package.json @@ -53,7 +53,7 @@ "tsup": "^8.2.4", "typescript": "^5.5", "uuid": "^9.0.0", - "vitest": "^3.0.0" + "vitest": "^3.0.5" }, "publishConfig": { "access": "public", diff --git a/packages/podspec/src/processors/db/podDB.ts b/packages/podspec/src/processors/db/podDB.ts index 3d7f11c..e177b39 100644 --- a/packages/podspec/src/processors/db/podDB.ts +++ b/packages/podspec/src/processors/db/podDB.ts @@ -3,7 +3,11 @@ 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 } from "../../spec/types.js"; +import type { + NamedStrongPODs, + PODEntriesFromEntryTypes, + StrongPOD, +} from "../../spec/types.js"; import { groupValidator } from "../validate/groupValidator.js"; import { podValidator } from "../validate/podValidator.js"; @@ -72,7 +76,9 @@ export class PODDB { /** * Query PODs that match a PODSpec */ - public queryBySpec(spec: PODSpec): Set { + public queryBySpec( + spec: PODSpec + ): StrongPOD>[] { const initialResults = new Set(); // Find all PODs that have the required entries @@ -110,7 +116,7 @@ export class PODDB { (signature) => this.indexes.bySignature.get(signature)! ); - const finalResults = new Set(); + const finalResults = []; const validator = podValidator(spec); for (const pod of pods) { @@ -119,7 +125,7 @@ export class PODDB { // 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.add(pod); + finalResults.push(pod); } } @@ -137,7 +143,7 @@ export class PODDB { for (const [name, spec] of Object.entries(groupSpec.pods)) { const result = this.queryBySpec(spec); - candidates.set(name, result); + candidates.set(name, new Set(result)); } // Generate all possible combinations 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/validate/podValidator.ts b/packages/podspec/src/processors/validate/podValidator.ts index d1dfb74..8265045 100644 --- a/packages/podspec/src/processors/validate/podValidator.ts +++ b/packages/podspec/src/processors/validate/podValidator.ts @@ -44,13 +44,13 @@ interface PODValidator { pod: POD, exitOnError?: boolean ): ValidateResult>>; - check(pod: POD): boolean; + check(pod: POD): pod is StrongPOD>; assert(pod: POD): asserts pod is StrongPOD>; strictValidate( pod: POD, exitOnError?: boolean ): ValidateResult>>; - strictCheck(pod: POD): boolean; + strictCheck(pod: POD): pod is StrongPOD>; strictAssert(pod: POD): asserts pod is StrongPOD>; } @@ -79,14 +79,15 @@ export function podValidator( return { validate: (pod, exitOnError = false) => validatePOD(pod, spec, { exitOnError }), - check: (pod) => validatePOD(pod, spec, { exitOnError: true }).isValid, + 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) => + strictCheck: (pod): pod is StrongPOD> => validatePOD(pod, spec, { strict: true, exitOnError: true }).isValid, strictAssert: (pod) => { const result = validatePOD(pod, spec, { 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 index 9796312..92986a6 100644 --- a/packages/podspec/test/builders/fast-check/pod.spec.ts +++ b/packages/podspec/test/builders/fast-check/pod.spec.ts @@ -1,61 +1,26 @@ import { fc, test } from "@fast-check/vitest"; -import { POD_NAME_REGEX } from "@pcd/pod"; import { expect } from "vitest"; import type { PODValueType } from "../../../src/builders/types/entries.js"; import { UntypedPODSpecBuilder } from "../../../src/builders/untypedPod.js"; - -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" - ) -); +import { validEntryName, validEntryType } from "./definitions.js"; test("UntypedPODSpecBuilder entries validation", () => { - // Valid entry types are fixed strings - const validEntryType = fc.constantFrom( - "string", - "int", - "boolean", - "date", - "bytes", - "cryptographic", - "null", - "eddsa_pubkey" - ); - fc.assert( - fc.property( - fc.dictionary( - validEntryName, - validEntryType as fc.Arbitrary - ), - (entries) => { - let builder = UntypedPODSpecBuilder.create(); + 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); - }); + // Add all entries + Object.entries(entries).forEach(([key, type]) => { + builder = builder.entry(key, type); + }); - const result = builder.spec(); + 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); - }); - } - ) + // Check that all entries are present with correct types + Object.entries(entries).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 index 0e96770..3044233 100644 --- a/packages/podspec/test/endToEnd.spec.ts +++ b/packages/podspec/test/endToEnd.spec.ts @@ -1,6 +1,8 @@ import type { PODValue } from "@pcd/pod"; import { assert, describe, expect, it } from "vitest"; -import { PODSpecBuilder } from "../src/index.js"; +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"; @@ -140,7 +142,7 @@ describe("endToEnd", () => { }, email: { type: "string", - value: "john.doe@example.com", + value: "jim.bob@example.com", }, phone: { type: "string", @@ -211,6 +213,118 @@ describe("endToEnd", () => { // 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() + .entry("eventName", "string") + .entry("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/pnpm-lock.yaml b/pnpm-lock.yaml index a367be5..318cc37 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -441,7 +441,7 @@ importers: devDependencies: '@fast-check/vitest': specifier: ^0.1.5 - version: 0.1.5(vitest@3.0.4(@types/debug@4.1.12)(@types/node@22.5.5)(sass@1.80.3)(terser@5.34.1)) + 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 @@ -470,8 +470,8 @@ importers: specifier: ^9.0.0 version: 9.0.1 vitest: - 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) + 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: @@ -2033,6 +2033,9 @@ packages: '@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: @@ -2068,6 +2071,17 @@ packages: 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==} @@ -2080,6 +2094,9 @@ packages: '@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==} @@ -2092,6 +2109,9 @@ packages: '@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==} @@ -2104,6 +2124,9 @@ packages: '@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==} @@ -2116,6 +2139,9 @@ packages: '@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: @@ -2133,6 +2159,9 @@ packages: '@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: @@ -5726,6 +5755,11 @@ packages: 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: @@ -5873,6 +5907,34 @@ packages: 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==} @@ -6918,10 +6980,10 @@ snapshots: dependencies: '@expressive-code/core': 0.35.6 - '@fast-check/vitest@0.1.5(vitest@3.0.4(@types/debug@4.1.12)(@types/node@22.5.5)(sass@1.80.3)(terser@5.34.1))': + '@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.4(@types/debug@4.1.12)(@types/node@22.5.5)(sass@1.80.3)(terser@5.34.1) + 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': {} @@ -7762,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 @@ -7776,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 @@ -7855,6 +7917,13 @@ snapshots: 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 @@ -7879,6 +7948,14 @@ snapshots: 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 @@ -7895,6 +7972,10 @@ snapshots: 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 @@ -7915,6 +7996,11 @@ snapshots: '@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 @@ -7939,6 +8025,12 @@ snapshots: 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 @@ -7955,6 +8047,10 @@ snapshots: 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 @@ -7992,6 +8088,12 @@ snapshots: 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 @@ -8567,7 +8669,7 @@ snapshots: assertion-error: 2.0.1 check-error: 2.1.1 deep-eql: 5.0.2 - loupe: 3.1.1 + loupe: 3.1.2 pathval: 2.0.0 chalk@2.4.2: @@ -10708,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 @@ -12652,6 +12754,24 @@ snapshots: - 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) @@ -12832,6 +12952,42 @@ snapshots: - 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): From 926d8fb0b0e057a868b82065e3805a08a7b6aa2a Mon Sep 17 00:00:00 2001 From: Rob Knight Date: Thu, 6 Feb 2025 15:56:15 +0100 Subject: [PATCH 20/20] Allow adding multiple entries at once --- packages/podspec/src/builders/pod.ts | 6 ++++++ packages/podspec/src/builders/untypedPod.ts | 19 +++++++++++++++++++ packages/podspec/test/endToEnd.spec.ts | 16 +++++++++------- 3 files changed, 34 insertions(+), 7 deletions(-) diff --git a/packages/podspec/src/builders/pod.ts b/packages/podspec/src/builders/pod.ts index a62bd10..8dc42dc 100644 --- a/packages/podspec/src/builders/pod.ts +++ b/packages/podspec/src/builders/pod.ts @@ -136,6 +136,12 @@ export class PODSpecBuilder< ); } + public entries( + entries: NewEntries + ): PODSpecBuilder, S> { + return new PODSpecBuilder(this.#innerBuilder.entries(entries)); + } + /** * Pick entries by key */ diff --git a/packages/podspec/src/builders/untypedPod.ts b/packages/podspec/src/builders/untypedPod.ts index c55b898..fecef0f 100644 --- a/packages/podspec/src/builders/untypedPod.ts +++ b/packages/podspec/src/builders/untypedPod.ts @@ -84,6 +84,25 @@ export class UntypedPODSpecBuilder { }); } + 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 */ diff --git a/packages/podspec/test/endToEnd.spec.ts b/packages/podspec/test/endToEnd.spec.ts index 3044233..2558fdf 100644 --- a/packages/podspec/test/endToEnd.spec.ts +++ b/packages/podspec/test/endToEnd.spec.ts @@ -11,10 +11,11 @@ describe("endToEnd", () => { // First of all, we want to be able to describe a POD in the abstract. { // Here is a PODSpecBuilder: - const builder = PODSpecBuilder.create() - .entry("name", "string") - .entry("date_of_birth", "date") - .entry("email", "string"); + 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(); @@ -218,9 +219,10 @@ describe("endToEnd", () => { // 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() - .entry("eventName", "string") - .entry("attendeeEmail", "string"); + const otherBuilder = PODSpecBuilder.create().entries({ + eventName: "string", + attendeeEmail: "string", + }); // The PODs are named and grouped together in the spec. const groupBuilder = PODGroupSpecBuilder.create()