Skip to content

Commit bd2df6c

Browse files
Refactor Validator from interface to plain function type
Change Validator<T> from an object with a .parse() method to a plain function type (value: unknown) => T. This removes the bias toward Zod's API convention and lets any validation library plug in directly without an adapter object. - src/models/composable.ts: Validator is now a type alias for a function - src/system/platform.ts: call inputSchema()/outputSchema() directly - src/system/function-registry.ts: duck-type check is now typeof === 'function' - tests/po.test.ts: test validators rewritten as arrow functions Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 4fd20fd commit bd2df6c

10 files changed

Lines changed: 54 additions & 50 deletions

File tree

dist/models/composable.d.ts

Lines changed: 12 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,20 @@
11
import { EventEnvelope } from "./event-envelope.js";
22
/**
3-
* Minimal library-agnostic validator protocol.
3+
* Library-agnostic validator — a plain function that receives an unknown value,
4+
* throws on invalid input, and returns the parsed/coerced result.
45
*
5-
* Any object exposing a `parse(value) => T` method that throws on invalid input
6-
* satisfies this interface. Zod schemas satisfy it natively. TypeBox users can
7-
* wrap a schema with a small adapter:
6+
* Any validation library can be plugged in directly:
87
*
9-
* const v: Validator<T> = { parse: (x) => { if (!Check(schema, x)) throw new Error(...); return x as T; } };
8+
* // Zod
9+
* inputSchema = (v: unknown) => MyZodSchema.parse(v);
10+
*
11+
* // TypeBox + Ajv
12+
* inputSchema = (v: unknown) => { if (!ajv.validate(schema, v)) throw new Error(ajv.errorsText()); return v as T; };
13+
*
14+
* // Hand-rolled
15+
* inputSchema = (v: unknown) => { if (typeof v !== 'string') throw new Error('expected string'); return v; };
1016
*/
11-
export interface Validator<T = unknown> {
12-
parse(value: unknown): T;
13-
}
17+
export type Validator<T = unknown> = (value: unknown) => T;
1418
/**
1519
* Type helper: extract the parsed type of a Validator (akin to z.infer).
1620
*/

dist/models/composable.js.map

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

dist/system/function-registry.js

Lines changed: 3 additions & 3 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

dist/system/function-registry.js.map

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

dist/system/platform.js

Lines changed: 3 additions & 3 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

dist/system/platform.js.map

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/models/composable.ts

Lines changed: 12 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -4,17 +4,21 @@ import { Logger } from "../util/logger.js";
44
const log = Logger.getInstance();
55

66
/**
7-
* Minimal library-agnostic validator protocol.
7+
* Library-agnostic validator — a plain function that receives an unknown value,
8+
* throws on invalid input, and returns the parsed/coerced result.
89
*
9-
* Any object exposing a `parse(value) => T` method that throws on invalid input
10-
* satisfies this interface. Zod schemas satisfy it natively. TypeBox users can
11-
* wrap a schema with a small adapter:
10+
* Any validation library can be plugged in directly:
1211
*
13-
* const v: Validator<T> = { parse: (x) => { if (!Check(schema, x)) throw new Error(...); return x as T; } };
12+
* // Zod
13+
* inputSchema = (v: unknown) => MyZodSchema.parse(v);
14+
*
15+
* // TypeBox + Ajv
16+
* inputSchema = (v: unknown) => { if (!ajv.validate(schema, v)) throw new Error(ajv.errorsText()); return v as T; };
17+
*
18+
* // Hand-rolled
19+
* inputSchema = (v: unknown) => { if (typeof v !== 'string') throw new Error('expected string'); return v; };
1420
*/
15-
export interface Validator<T = unknown> {
16-
parse(value: unknown): T;
17-
}
21+
export type Validator<T = unknown> = (value: unknown) => T;
1822

1923
/**
2024
* Type helper: extract the parsed type of a Validator (akin to z.infer).

src/system/function-registry.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -145,12 +145,12 @@ class SimpleRegistry {
145145
'instances': instances, 'private': isPrivate, 'interceptor': interceptor
146146
};
147147
// Capture optional input/output schemas from the Composable instance.
148-
// Duck-typed: anything exposing a .parse(value) function qualifies.
148+
// Validators are plain functions: (value: unknown) => T
149149
const anyThat = that as Record<string, unknown>;
150-
if (anyThat['inputSchema'] && typeof (anyThat['inputSchema'] as {parse?: unknown}).parse === 'function') {
150+
if (typeof anyThat['inputSchema'] === 'function') {
151151
meta['inputSchema'] = anyThat['inputSchema'];
152152
}
153-
if (anyThat['outputSchema'] && typeof (anyThat['outputSchema'] as {parse?: unknown}).parse === 'function') {
153+
if (typeof anyThat['outputSchema'] === 'function') {
154154
meta['outputSchema'] = anyThat['outputSchema'];
155155
}
156156
this.metadata.set(route, meta);

src/system/platform.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -353,7 +353,7 @@ class ServiceManager {
353353
// Input schema validation (strict: AppException 400 on failure)
354354
if (this.inputSchema) {
355355
try {
356-
const parsed = this.inputSchema.parse(evt.getBody());
356+
const parsed = this.inputSchema(evt.getBody());
357357
evt.setBody(parsed as string | number | object | boolean | Buffer | Uint8Array);
358358
} catch (err) {
359359
const msg = err instanceof Error ? err.message : String(err);
@@ -372,10 +372,10 @@ class ServiceManager {
372372
if (this.outputSchema && !this.interceptor) {
373373
try {
374374
if (v instanceof EventEnvelope) {
375-
const parsed = this.outputSchema.parse(v.getBody());
375+
const parsed = this.outputSchema(v.getBody());
376376
v.setBody(parsed as string | number | object | boolean | Buffer | Uint8Array);
377377
} else {
378-
v = this.outputSchema.parse(v);
378+
v = this.outputSchema(v);
379379
}
380380
} catch (err) {
381381
const msg = err instanceof Error ? err.message : String(err);

tests/po.test.ts

Lines changed: 15 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -108,32 +108,28 @@ class DemoHealth implements Composable {
108108
}
109109

110110
// Minimal hand-rolled validators (avoids adding zod as a test dependency).
111-
// Any object with a .parse(value) that throws on invalid input satisfies Validator.
111+
// Validators are plain functions: (value: unknown) => T, throwing on invalid input.
112112
interface SchemaInput { id: string; amount: number; }
113113
interface SchemaOutput { ok: boolean; doubled: number; }
114114

115-
const schemaInputValidator: Validator<SchemaInput> = {
116-
parse(value: unknown): SchemaInput {
117-
if (!value || typeof value !== 'object') {
118-
throw new Error('expected object');
119-
}
120-
const v = value as Record<string, unknown>;
121-
if (typeof v.id !== 'string') throw new Error('id must be string');
122-
if (typeof v.amount !== 'number') throw new Error('amount must be number');
123-
return { id: v.id, amount: v.amount };
115+
const schemaInputValidator: Validator<SchemaInput> = (value: unknown): SchemaInput => {
116+
if (!value || typeof value !== 'object') {
117+
throw new Error('expected object');
124118
}
119+
const v = value as Record<string, unknown>;
120+
if (typeof v.id !== 'string') throw new Error('id must be string');
121+
if (typeof v.amount !== 'number') throw new Error('amount must be number');
122+
return { id: v.id, amount: v.amount };
125123
};
126124

127-
const schemaOutputValidator: Validator<SchemaOutput> = {
128-
parse(value: unknown): SchemaOutput {
129-
if (!value || typeof value !== 'object') {
130-
throw new Error('expected object');
131-
}
132-
const v = value as Record<string, unknown>;
133-
if (typeof v.ok !== 'boolean') throw new Error('ok must be boolean');
134-
if (typeof v.doubled !== 'number') throw new Error('doubled must be number');
135-
return { ok: v.ok, doubled: v.doubled };
125+
const schemaOutputValidator: Validator<SchemaOutput> = (value: unknown): SchemaOutput => {
126+
if (!value || typeof value !== 'object') {
127+
throw new Error('expected object');
136128
}
129+
const v = value as Record<string, unknown>;
130+
if (typeof v.ok !== 'boolean') throw new Error('ok must be boolean');
131+
if (typeof v.doubled !== 'number') throw new Error('doubled must be number');
132+
return { ok: v.ok, doubled: v.doubled };
137133
};
138134

139135
class SchemaValidatedService implements Composable {

0 commit comments

Comments
 (0)