Skip to content

Commit 4fd20fd

Browse files
Add optional schema validation at function boundaries
Introduces an opt-in validation hook so Composable functions can declare input and output schemas. When declared, the ServiceManager validates the event body before handleEvent() is invoked and the resolved return value after, surfacing failures as AppException(400) and AppException(500) respectively. Functions without schemas are unaffected (single truthy check per dispatch, no behaviour change). The protocol is intentionally library-agnostic: a minimal Validator<T> interface requiring only a .parse(value) method that throws on invalid input. Zod schemas satisfy it natively, and hand-rolled validators or TypeBox adapters work with zero framework changes. No new runtime dependencies are added. - src/models/composable.ts: add Validator<T> and Infer<V>; extend Composable with optional inputSchema and outputSchema - src/system/function-registry.ts: capture schemas into metadata at save() - src/system/platform.ts: ServiceManager accepts schemas, validates input before dispatch and output after; interceptors skip output validation (their return value is already discarded); EventEnvelope returns have their body validated in place - src/index.ts: export Validator and Infer - tests/po.test.ts: add SchemaValidatedService with hand-rolled validators and three tests covering happy path, 400 on bad input, 500 on bad output Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 734d0c0 commit 4fd20fd

13 files changed

Lines changed: 277 additions & 18 deletions

dist/index.d.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ export { MultiLevelMap } from './util/multi-level-map.js';
99
export { EventEnvelope } from './models/event-envelope.js';
1010
export { AsyncHttpRequest } from './models/async-http-request.js';
1111
export { AppException } from './models/app-exception.js';
12-
export { Composable, preload } from './models/composable.js';
12+
export { Composable, Validator, Infer, preload } from './models/composable.js';
1313
export { ObjectStreamIO, ObjectStreamWriter, ObjectStreamReader } from './system/object-stream.js';
1414
export { AppConfig, ConfigReader } from './util/config-reader.js';
1515
export { TemplateLoader } from './util/template-loader.js';

dist/index.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/models/composable.d.ts

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,20 @@
11
import { EventEnvelope } from "./event-envelope.js";
2+
/**
3+
* Minimal library-agnostic validator protocol.
4+
*
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:
8+
*
9+
* const v: Validator<T> = { parse: (x) => { if (!Check(schema, x)) throw new Error(...); return x as T; } };
10+
*/
11+
export interface Validator<T = unknown> {
12+
parse(value: unknown): T;
13+
}
14+
/**
15+
* Type helper: extract the parsed type of a Validator (akin to z.infer).
16+
*/
17+
export type Infer<V> = V extends Validator<infer T> ? T : never;
218
export interface Composable {
319
/**
420
* Annotation for the initialize() method to tell the system to preload this composable function:
@@ -25,6 +41,21 @@ export interface Composable {
2541
* @param evt is the incoming event containing headers and body (payload)
2642
*/
2743
handleEvent(evt: EventEnvelope): Promise<string | boolean | number | object | EventEnvelope | null>;
44+
/**
45+
* Optional input schema. When set, the event body is validated BEFORE
46+
* handleEvent() is invoked. Validation failure throws AppException(400).
47+
* The parsed (and potentially coerced) value replaces evt.getBody().
48+
*/
49+
inputSchema?: Validator;
50+
/**
51+
* Optional output schema. When set, the handler's resolved return value
52+
* is validated AFTER handleEvent() completes. Validation failure is
53+
* reported as AppException(500). Skipped for interceptors (their return
54+
* value is not forwarded anyway).
55+
*
56+
* If the handler returns an EventEnvelope, the envelope's body is validated.
57+
*/
58+
outputSchema?: Validator;
2859
}
2960
/**
3061
* Annotation for a composable class

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: 13 additions & 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.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: 42 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/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ export { MultiLevelMap } from './util/multi-level-map.js';
99
export { EventEnvelope } from './models/event-envelope.js';
1010
export { AsyncHttpRequest } from './models/async-http-request.js';
1111
export { AppException } from './models/app-exception.js';
12-
export { Composable, preload } from './models/composable.js';
12+
export { Composable, Validator, Infer, preload } from './models/composable.js';
1313
export { ObjectStreamIO, ObjectStreamWriter, ObjectStreamReader } from './system/object-stream.js';
1414
export { AppConfig, ConfigReader } from './util/config-reader.js';
1515
export { TemplateLoader } from './util/template-loader.js';

0 commit comments

Comments
 (0)