Skip to content

Commit 3b6e2a1

Browse files
committed
Update schema resolution to use decoupled metadata with Zod chained prototype operators
1 parent 9cada31 commit 3b6e2a1

5 files changed

Lines changed: 119 additions & 118 deletions

File tree

renderers/angular/src/v0_9/core/types.ts

Lines changed: 15 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -73,24 +73,28 @@ type DataBindingType = z.infer<typeof DataBindingSchema>;
7373
type FunctionCallType = z.infer<typeof FunctionCallSchema>;
7474
type DynamicTypes = DataBindingType | FunctionCallType;
7575

76-
type ResolveAngularProp<T> = T extends any
77-
? T extends null | undefined
78-
? T
79-
: [T] extends [ChildList]
80-
? Child[]
81-
: [T] extends [ComponentId]
82-
? Child
83-
: Exclude<T, DynamicTypes> extends never
84-
? any
85-
: ResolveAngularPropNested<Exclude<T, DynamicTypes>>
86-
: never;
76+
type UnwrappedDynamic<T> = Exclude<T, DynamicTypes>;
8777

8878
type ResolveAngularPropNested<T> = T extends (infer U)[]
8979
? ResolveAngularProp<U>[]
9080
: T extends object
9181
? {[K in keyof T]: ResolveAngularProp<T[K]>}
9282
: T;
9383

84+
type ResolveNonComponentProp<T> = UnwrappedDynamic<T> extends never
85+
? any
86+
: ResolveAngularPropNested<UnwrappedDynamic<T>>;
87+
88+
type ResolveNonNullAngularProp<T> = [T] extends [ChildList]
89+
? Child[]
90+
: [T] extends [ComponentId]
91+
? Child
92+
: ResolveNonComponentProp<T>;
93+
94+
export type ResolveAngularProp<T> = T extends null | undefined
95+
? T
96+
: ResolveNonNullAngularProp<T>;
97+
9498
type InferredInterfaceToProps<InferredSchema> = {
9599
[K in keyof InferredSchema]: BoundProperty<ResolveAngularProp<InferredSchema[K]>>;
96100
};

renderers/web_core/src/v0_9/processing/message-processor.test.ts

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -67,11 +67,11 @@ describe('MessageProcessor', () => {
6767
assert.deepStrictEqual(buttonSchema.allOf[1].required, ['component', 'label']);
6868
});
6969

70-
it('transforms REF: descriptions into valid $ref nodes', () => {
70+
it('transforms schemas with refPath into valid $ref nodes', () => {
7171
const customApi: ComponentApi = {
7272
name: 'Custom',
7373
schema: z.object({
74-
title: z.string().describe('REF:common_types.json#/$defs/DynamicString|The title'),
74+
title: z.string().describe('The title').setRefPath('common_types.json#/$defs/DynamicString'),
7575
}),
7676
};
7777
const cat = new Catalog('cat-ref', [customApi]);
@@ -105,7 +105,7 @@ describe('MessageProcessor', () => {
105105
};
106106

107107
const themeSchema = z.object({
108-
primaryColor: z.string().describe('REF:common_types.json#/$defs/Color|The main color'),
108+
primaryColor: z.string().describe('The main color').setRefPath('common_types.json#/$defs/Color'),
109109
});
110110

111111
const cat = new Catalog('cat-full', [buttonApi], [addFn], themeSchema);
@@ -151,7 +151,8 @@ describe('MessageProcessor', () => {
151151
z.object({
152152
action: z
153153
.string()
154-
.describe('REF:common_types.json#/$defs/Action|The action to perform'),
154+
.describe('The action to perform')
155+
.setRefPath('common_types.json#/$defs/Action'),
155156
}),
156157
),
157158
}),
@@ -172,8 +173,8 @@ describe('MessageProcessor', () => {
172173
const edgeApi: ComponentApi = {
173174
name: 'EdgeComp',
174175
schema: z.object({
175-
noPipe: z.string().describe('REF:common_types.json#/$defs/NoPipe'),
176-
multiPipe: z.string().describe('REF:common_types.json#/$defs/MultiPipe|First|Second'),
176+
noPipe: z.string().setRefPath('common_types.json#/$defs/NoPipe'),
177+
multiPipe: z.string().describe('First|Second').setRefPath('common_types.json#/$defs/MultiPipe'),
177178
}),
178179
};
179180
const cat = new Catalog('cat-edge', [edgeApi]);
@@ -186,7 +187,7 @@ describe('MessageProcessor', () => {
186187
assert.strictEqual(properties.noPipe.description, undefined);
187188

188189
assert.strictEqual(properties.multiPipe.$ref, 'common_types.json#/$defs/MultiPipe');
189-
assert.strictEqual(properties.multiPipe.description, 'First');
190+
assert.strictEqual(properties.multiPipe.description, 'First|Second');
190191
});
191192

192193
it('handles multiple catalogs correctly', () => {

renderers/web_core/src/v0_9/processing/message-processor.ts

Lines changed: 20 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,9 @@ import {Catalog, ComponentApi} from '../catalog/types.js';
1919
import {SurfaceGroupModel} from '../state/surface-group-model.js';
2020
import {ComponentModel} from '../state/component-model.js';
2121
import {Subscription} from '../common/events.js';
22-
import {zodToJsonSchema} from 'zod-to-json-schema';
22+
import {zodToJsonSchema, ignoreOverride} from 'zod-to-json-schema';
23+
import {A2uiTypeDef} from '../schema/common-types.js';
24+
import '../schema/common-types.js';
2325

2426
import {
2527
A2uiMessage,
@@ -41,6 +43,20 @@ export interface CapabilitiesOptions {
4143
includeInlineCatalogs?: boolean;
4244
}
4345

46+
const zodToJsonSchemaOptions = {
47+
target: 'jsonSchema2019-09' as const,
48+
override: (def: any) => {
49+
const a2uiDef = def as A2uiTypeDef;
50+
if (a2uiDef.refPath) {
51+
return {
52+
$ref: a2uiDef.refPath,
53+
description: (def as any).description,
54+
};
55+
}
56+
return ignoreOverride;
57+
},
58+
};
59+
4460
/**
4561
* The central processor for A2UI messages.
4662
* @template T The concrete type of the ComponentApi.
@@ -88,12 +104,7 @@ export class MessageProcessor<T extends ComponentApi> {
88104
const components: Record<string, any> = {};
89105

90106
for (const [name, api] of catalog.components.entries()) {
91-
const zodSchema = zodToJsonSchema(api.schema, {
92-
target: 'jsonSchema2019-09',
93-
}) as any;
94-
95-
// Clean up Zod-specific artifacts and process REF: tags
96-
this.processRefs(zodSchema);
107+
const zodSchema = zodToJsonSchema(api.schema as any, zodToJsonSchemaOptions) as any;
97108

98109
// Wrap in standard A2UI component envelope (ComponentCommon)
99110
components[name] = {
@@ -112,11 +123,7 @@ export class MessageProcessor<T extends ComponentApi> {
112123

113124
const functions: any[] = [];
114125
for (const api of catalog.functions.values()) {
115-
const zodSchema = zodToJsonSchema(api.schema, {
116-
target: 'jsonSchema2019-09',
117-
}) as any;
118-
119-
this.processRefs(zodSchema);
126+
const zodSchema = zodToJsonSchema(api.schema as any, zodToJsonSchemaOptions) as any;
120127

121128
functions.push({
122129
name: api.name,
@@ -128,11 +135,7 @@ export class MessageProcessor<T extends ComponentApi> {
128135

129136
let theme: Record<string, any> | undefined;
130137
if (catalog.themeSchema) {
131-
const zodSchema = zodToJsonSchema(catalog.themeSchema, {
132-
target: 'jsonSchema2019-09',
133-
}) as any;
134-
135-
this.processRefs(zodSchema);
138+
const zodSchema = zodToJsonSchema(catalog.themeSchema as any, zodToJsonSchemaOptions) as any;
136139
theme = zodSchema.properties;
137140
}
138141

@@ -144,39 +147,7 @@ export class MessageProcessor<T extends ComponentApi> {
144147
};
145148
}
146149

147-
private processRefs(node: any): void {
148-
if (typeof node !== 'object' || node === null) return;
149-
150-
// If the node itself is a REF target, transform it and stop recursion.
151-
if (typeof node.description === 'string' && node.description.startsWith('REF:')) {
152-
const parts = node.description.substring(4).split('|');
153-
const ref = parts[0];
154-
const desc = parts[1] || '';
155-
156-
// Clear the node of all other properties.
157-
for (const k of Object.keys(node)) {
158-
delete node[k];
159-
}
160150

161-
// Re-add only the $ref and an optional description.
162-
node['$ref'] = ref;
163-
if (desc) {
164-
node['description'] = desc;
165-
}
166-
return;
167-
}
168-
169-
// If not a REF target, recurse into its children.
170-
if (Array.isArray(node)) {
171-
for (const item of node) {
172-
this.processRefs(item);
173-
}
174-
} else {
175-
for (const key of Object.keys(node)) {
176-
this.processRefs(node[key]);
177-
}
178-
}
179-
}
180151

181152
/**
182153
* Returns the aggregated data model for all surfaces that have 'sendDataModel' enabled.

renderers/web_core/src/v0_9/rendering/generic-binder.ts

Lines changed: 17 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -146,26 +146,30 @@ type IsDynamic<T> = DataBinding extends NonNullable<T> ? true : false;
146146
* Maps raw Zod inferred types to their resolved runtime equivalents.
147147
* For example, an `Action` object becomes a callable `() => void` function.
148148
*/
149-
export type ResolveA2uiProp<T> = T extends any
150-
? T extends null | undefined
151-
? T
152-
: [T] extends [Action]
153-
? () => void
154-
: [T] extends [ChildList]
155-
? any
156-
: [T] extends [ComponentId]
157-
? {id: ComponentId; basePath: string}
158-
: Exclude<T, DynamicTypes> extends never
159-
? any
160-
: ResolveA2uiPropNested<Exclude<T, DynamicTypes>>
161-
: never;
149+
type UnwrappedDynamic<T> = Exclude<T, DynamicTypes>;
162150

163151
type ResolveA2uiPropNested<T> = T extends (infer U)[]
164152
? ResolveA2uiProp<U>[]
165153
: T extends object
166154
? {[K in keyof T]: ResolveA2uiProp<T[K]>}
167155
: T;
168156

157+
type ResolveNonComponentA2uiProp<T> = UnwrappedDynamic<T> extends never
158+
? any
159+
: ResolveA2uiPropNested<UnwrappedDynamic<T>>;
160+
161+
type ResolveNonNullA2uiProp<T> = [T] extends [Action]
162+
? () => void
163+
: [T] extends [ChildList]
164+
? any
165+
: [T] extends [ComponentId]
166+
? {id: ComponentId; basePath: string}
167+
: ResolveNonComponentA2uiProp<T>;
168+
169+
export type ResolveA2uiProp<T> = T extends null | undefined
170+
? T
171+
: ResolveNonNullA2uiProp<T>;
172+
169173
/**
170174
* Automatically generates two-way binding setters for dynamic properties.
171175
* If a schema has a `value: DynamicString`, this type generates a `setValue(val: string)` method.

0 commit comments

Comments
 (0)