diff --git a/README.md b/README.md index c3cd5ea..332c613 100644 --- a/README.md +++ b/README.md @@ -14,6 +14,8 @@ This is not an ORM in a fake mustache. It is for cases where you want: ## Current Shape - document types are registered explicitly +- a document schema only needs a `parse(...)` function, so generated JSON-schema + contracts can plug in directly without growing a second hand-written Zod body - each persisted envelope stores: - `type` - `key` @@ -166,6 +168,10 @@ const combatLogDocument = defineDocumentType({ If you do not supply one, `CultCacheTS` uses MessagePack plus Zod validation by default. +Zod is no longer mandatory. Any schema object with a `parse(...)` function will +do, which means generated Ajv-backed JSON-schema contracts can feed the cache +directly as long as they expose the same parse surface. + The important boundary is this: - payload bytes are persisted as bytes diff --git a/src/cult-cache.ts b/src/cult-cache.ts index 02e3681..fd7c768 100644 --- a/src/cult-cache.ts +++ b/src/cult-cache.ts @@ -30,7 +30,7 @@ type HydratedEntry = CultCacheEnvelope & { }; type BackingStoreTypeReference = string | AnyCultCacheDocumentDefinition; -type AccessorInput = string | ((value: unknown) => unknown); +type AccessorInput = string | ((value: any) => unknown); export class CultCacheBuilder { readonly #cache = new CultCache(); diff --git a/src/document.ts b/src/document.ts index e92575d..ce4d103 100644 --- a/src/document.ts +++ b/src/document.ts @@ -1,8 +1,11 @@ -import type { ZodType } from "zod"; +import type { + AnyCultCacheDocumentDefinition, + CultCacheDocumentDefinition, + CultCacheDocumentRegistry, + CultCacheSchema, +} from "./types"; -import type { AnyCultCacheDocumentDefinition, CultCacheDocumentDefinition, CultCacheDocumentRegistry } from "./types"; - -export function defineDocumentType( +export function defineDocumentType( definition: CultCacheDocumentDefinition, ): CultCacheDocumentDefinition { if (!definition.type || definition.type.trim().length === 0) { diff --git a/src/index.ts b/src/index.ts index bf05774..fac6574 100644 --- a/src/index.ts +++ b/src/index.ts @@ -10,9 +10,11 @@ export type { CultCacheDocumentFormatter, CultCacheDocumentIndexDefinition, CultCacheDocumentRegistry, + CultCacheSchema, CultCacheDocumentValue, CultCacheEnvelope, CultCacheIndexScalar, + InferCultCacheSchemaValue, CultCacheStoreRegistration, PushAllOptions, } from "./types"; diff --git a/src/types.ts b/src/types.ts index 0d81aab..a3eb958 100644 --- a/src/types.ts +++ b/src/types.ts @@ -1,7 +1,12 @@ -import { z, type ZodType } from "zod"; - export type CultCacheIndexScalar = string | number | boolean | bigint; +export interface CultCacheSchema { + parse(input: unknown): TValue; +} + +export type InferCultCacheSchemaValue = + TSchema extends CultCacheSchema ? TValue : never; + export type CultCacheDocumentFieldName = TValue extends object ? Extract : never; export type CultCacheDocumentAccessor = @@ -18,19 +23,19 @@ export interface CultCacheDocumentIndexDefinition { accessor: CultCacheDocumentAccessor; } -export interface CultCacheDocumentDefinition { +export interface CultCacheDocumentDefinition { type: string; schema: TSchema; schemaName?: string; global?: boolean; - name?: CultCacheDocumentAccessor>; + name?: CultCacheDocumentAccessor>; indexes?: - | Readonly>>> - | readonly CultCacheDocumentIndexDefinition>[]; - formatter?: CultCacheDocumentFormatter>; + | Readonly>>> + | readonly CultCacheDocumentIndexDefinition>[]; + formatter?: CultCacheDocumentFormatter>; } -export type CultCacheDocumentValue = z.infer< +export type CultCacheDocumentValue = InferCultCacheSchemaValue< TDefinition["schema"] >; diff --git a/test/cult-cache.test.ts b/test/cult-cache.test.ts index e1013dd..4108e70 100644 --- a/test/cult-cache.test.ts +++ b/test/cult-cache.test.ts @@ -10,7 +10,7 @@ import { z } from "zod"; import { CultCache } from "../src/cult-cache"; import { defineDocumentRegistry, defineDocumentType } from "../src/document"; import { SingleFileMessagePackBackingStore } from "../src/single-file-messagepack-backing-store"; -import type { CacheBackingStore, CultCacheEnvelope } from "../src/types"; +import type { CacheBackingStore, CultCacheEnvelope, CultCacheSchema } from "../src/types"; test("CultCache supports registry bootstrap, global documents, and lookup by name and index", async () => { const itemDocument = defineDocumentType({ @@ -179,3 +179,59 @@ test("CultCache fails closed on unknown or illegal persisted polymorphic state", /has multiple persisted entries/, ); }); + +test("CultCache accepts generated parse-style schemas without a Zod mirror", async () => { + type GeneratedSettings = { + schema_version: "generated.settings.v0"; + theme: string; + retries: number; + }; + + const generatedSchema: CultCacheSchema = { + parse(input: unknown): GeneratedSettings { + if (!input || typeof input !== "object") { + throw new Error("generated settings must be an object"); + } + + const value = input as Record; + if (value.schema_version !== "generated.settings.v0") { + throw new Error("generated settings schema_version mismatch"); + } + if (typeof value.theme !== "string") { + throw new Error("generated settings theme must be a string"); + } + if (typeof value.retries !== "number") { + throw new Error("generated settings retries must be a number"); + } + + return { + schema_version: "generated.settings.v0", + theme: value.theme, + retries: value.retries, + }; + }, + }; + + const generatedDocument = defineDocumentType({ + type: "generated-settings", + schema: generatedSchema, + global: true, + }); + + const storePath = join(await mkdtemp(join(tmpdir(), "cultcache-ts-")), "generated.msgpack"); + const cache = CultCache.builder() + .withDocumentType(generatedDocument) + .withGenericStore(new SingleFileMessagePackBackingStore(storePath)) + .build(); + + await cache.putGlobal(generatedDocument, { + schema_version: "generated.settings.v0", + theme: "ash", + retries: 3, + }); + + const settings = cache.getRequiredGlobal(generatedDocument); + assert.equal(settings.schema_version, "generated.settings.v0"); + assert.equal(settings.theme, "ash"); + assert.equal(settings.retries, 3); +});