Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 38 additions & 0 deletions renderers/lit/src/0.8/catalog/catalog.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
/*
Copyright 2025 Google LLC

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

https://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

import { CatalogItem } from './catalog_item.js';

/**
* A catalog of component definitions used by the A2uiMessageProcessor.
*/
export class Catalog {
/** An optional URI identifying this catalog. */
readonly uri?: string;
private readonly items: ReadonlyMap<string, CatalogItem>;

constructor(items: readonly CatalogItem[], uri?: string) {
this.uri = uri;
this.items = new Map(items.map(item => [item.typeName, item]));
}

/**
* Retrieves a CatalogItem by its component type name.
*/
get(componentType: string): CatalogItem | undefined {
return this.items.get(componentType);
}
}
35 changes: 35 additions & 0 deletions renderers/lit/src/0.8/catalog/catalog_item.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
/*
Copyright 2025 Google LLC

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

https://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

import { AnyComponentNode, ResolvedMap } from '../types/types.js';

export interface CatalogItem {
/** The component type name (e.g., "Text"). */
readonly typeName: string;

/**
* Validates the resolved properties and builds a typed component node.
* @param baseNode The common properties (id, dataContextPath, weight) for the node.
* @param resolvedProperties The properties of the component after resolving all data bindings and children.
* @param objCtor The object constructor to use (to support signal-based objects).
* @returns A typed `AnyComponentNode` or throws an error if validation fails.
*/
buildNode(
baseNode: { id: string; dataContextPath: string; weight: number | 'initial' },
resolvedProperties: ResolvedMap,
objCtor: ObjectConstructor
): AnyComponentNode;
}
3 changes: 3 additions & 0 deletions renderers/lit/src/0.8/core.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,9 @@ export * as Events from "./events/events.js";
export * as Types from "./types/types.js";
export * as Primitives from "./types/primitives.js";
export * as Styles from "./styles/index.js";
export { Catalog } from "./catalog/catalog.js";
export { type CatalogItem } from "./catalog/catalog_item.js";
export { StandardCatalogItems } from "./standard_catalog/index.js";
import * as Guards from "./data/guards.js";

import { create as createSignalA2uiMessageProcessor } from "./data/signal-model-processor.js";
Expand Down
250 changes: 27 additions & 223 deletions renderers/lit/src/0.8/data/model-processor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,31 +31,14 @@ import {
MessageProcessor,
ValueMap,
DataObject,
} from "../types/types";
} from "../types/types.js";
import {
isComponentArrayReference,
isObject,
isPath,
isResolvedAudioPlayer,
isResolvedButton,
isResolvedCard,
isResolvedCheckbox,
isResolvedColumn,
isResolvedDateTimeInput,
isResolvedDivider,
isResolvedIcon,
isResolvedImage,
isResolvedList,
isResolvedModal,
isResolvedMultipleChoice,
isResolvedRow,
isResolvedSlider,
isResolvedTabs,
isResolvedText,
isResolvedTextField,
isResolvedVideo,
isValueMap,
} from "./guards.js";
import { Catalog } from '../catalog/catalog.js';
import { StandardCatalogItems } from '../standard_catalog/standard_catalog_items.js';

/**
* Processes and consolidates A2UIProtocolMessage objects into a structured,
Expand All @@ -69,21 +52,23 @@ export class A2uiMessageProcessor implements MessageProcessor {
private setCtor: SetConstructor = Set;
private objCtor: ObjectConstructor = Object;
private surfaces: Map<SurfaceID, Surface>;
private readonly catalog: Catalog;

constructor(
readonly opts: {
mapCtor: MapConstructor;
arrayCtor: ArrayConstructor;
setCtor: SetConstructor;
objCtor: ObjectConstructor;
} = { mapCtor: Map, arrayCtor: Array, setCtor: Set, objCtor: Object }
mapCtor?: MapConstructor;
arrayCtor?: ArrayConstructor;
setCtor?: SetConstructor;
objCtor?: ObjectConstructor;
catalog?: Catalog;
} = {}
) {
this.arrayCtor = opts.arrayCtor;
this.mapCtor = opts.mapCtor;
this.setCtor = opts.setCtor;
this.objCtor = opts.objCtor;

this.surfaces = new opts.mapCtor();
this.arrayCtor = opts.arrayCtor ?? Array;
this.mapCtor = opts.mapCtor ?? Map;
this.setCtor = opts.setCtor ?? Set;
this.objCtor = opts.objCtor ?? Object;
this.surfaces = new (this.mapCtor)();
this.catalog = opts.catalog ?? new Catalog(StandardCatalogItems.items, 'a2ui://standard_catalog');
}
Comment on lines 57 to 72
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The constructor options are now all optional, which is a good improvement for flexibility. However, the opts parameter itself is still marked as readonly, which is not necessary since it's a function parameter and not a class property. You can simplify the signature by removing the readonly keyword.

Suggested change
constructor(
readonly opts: {
mapCtor: MapConstructor;
arrayCtor: ArrayConstructor;
setCtor: SetConstructor;
objCtor: ObjectConstructor;
} = { mapCtor: Map, arrayCtor: Array, setCtor: Set, objCtor: Object }
mapCtor?: MapConstructor;
arrayCtor?: ArrayConstructor;
setCtor?: SetConstructor;
objCtor?: ObjectConstructor;
catalog?: Catalog;
} = {}
) {
this.arrayCtor = opts.arrayCtor;
this.mapCtor = opts.mapCtor;
this.setCtor = opts.setCtor;
this.objCtor = opts.objCtor;
this.surfaces = new opts.mapCtor();
this.arrayCtor = opts.arrayCtor ?? Array;
this.mapCtor = opts.mapCtor ?? Map;
this.setCtor = opts.setCtor ?? Set;
this.objCtor = opts.objCtor ?? Object;
this.surfaces = new (this.mapCtor)();
this.catalog = opts.catalog ?? new Catalog(StandardCatalogItems.items, 'a2ui://standard_catalog');
}
constructor(
opts: {
mapCtor?: MapConstructor;
arrayCtor?: ArrayConstructor;
setCtor?: SetConstructor;
objCtor?: ObjectConstructor;
catalog?: Catalog;
} = {}
) {
this.arrayCtor = opts.arrayCtor ?? Array;
this.mapCtor = opts.mapCtor ?? Map;
this.setCtor = opts.setCtor ?? Set;
this.objCtor = opts.objCtor ?? Object;
this.surfaces = new (this.mapCtor)();
this.catalog = opts.catalog ?? new Catalog(StandardCatalogItems.items, 'a2ui://standard_catalog');
}


getSurfaces(): ReadonlyMap<string, Surface> {
Expand Down Expand Up @@ -514,205 +499,24 @@ export class A2uiMessageProcessor implements MessageProcessor {

visited.delete(fullId);

// Now that we have the resolved properties in place we can go ahead and
// ensure that they meet expectations in terms of types and so forth,
// casting them into the specific shape for usage.
const baseNode = {
const baseNode: { id: string, dataContextPath: string, weight: number | 'initial' } = {
id: fullId,
dataContextPath,
weight: componentData.weight ?? "initial",
};
switch (componentType) {
case "Text":
if (!isResolvedText(resolvedProperties)) {
throw new Error(`Invalid data; expected ${componentType}`);
}
return new this.objCtor({
...baseNode,
type: "Text",
properties: resolvedProperties,
}) as AnyComponentNode;

case "Image":
if (!isResolvedImage(resolvedProperties)) {
throw new Error(`Invalid data; expected ${componentType}`);
}
return new this.objCtor({
...baseNode,
type: "Image",
properties: resolvedProperties,
}) as AnyComponentNode;

case "Icon":
if (!isResolvedIcon(resolvedProperties)) {
throw new Error(`Invalid data; expected ${componentType}`);
}
return new this.objCtor({
...baseNode,
type: "Icon",
properties: resolvedProperties,
}) as AnyComponentNode;

case "Video":
if (!isResolvedVideo(resolvedProperties)) {
throw new Error(`Invalid data; expected ${componentType}`);
}
return new this.objCtor({
...baseNode,
type: "Video",
properties: resolvedProperties,
}) as AnyComponentNode;

case "AudioPlayer":
if (!isResolvedAudioPlayer(resolvedProperties)) {
throw new Error(`Invalid data; expected ${componentType}`);
}
return new this.objCtor({
...baseNode,
type: "AudioPlayer",
properties: resolvedProperties,
}) as AnyComponentNode;

case "Row":
if (!isResolvedRow(resolvedProperties)) {
throw new Error(`Invalid data; expected ${componentType}`);
}

return new this.objCtor({
...baseNode,
type: "Row",
properties: resolvedProperties,
}) as AnyComponentNode;

case "Column":
if (!isResolvedColumn(resolvedProperties)) {
throw new Error(`Invalid data; expected ${componentType}`);
}
const catalogItem = this.catalog.get(componentType);

return new this.objCtor({
...baseNode,
type: "Column",
properties: resolvedProperties,
}) as AnyComponentNode;

case "List":
if (!isResolvedList(resolvedProperties)) {
throw new Error(`Invalid data; expected ${componentType}`);
}
return new this.objCtor({
...baseNode,
type: "List",
properties: resolvedProperties,
}) as AnyComponentNode;

case "Card":
if (!isResolvedCard(resolvedProperties)) {
throw new Error(`Invalid data; expected ${componentType}`);
}
return new this.objCtor({
...baseNode,
type: "Card",
properties: resolvedProperties,
}) as AnyComponentNode;

case "Tabs":
if (!isResolvedTabs(resolvedProperties)) {
throw new Error(`Invalid data; expected ${componentType}`);
}
return new this.objCtor({
...baseNode,
type: "Tabs",
properties: resolvedProperties,
}) as AnyComponentNode;

case "Divider":
if (!isResolvedDivider(resolvedProperties)) {
throw new Error(`Invalid data; expected ${componentType}`);
}
return new this.objCtor({
...baseNode,
type: "Divider",
properties: resolvedProperties,
}) as AnyComponentNode;

case "Modal":
if (!isResolvedModal(resolvedProperties)) {
throw new Error(`Invalid data; expected ${componentType}`);
}
return new this.objCtor({
...baseNode,
type: "Modal",
properties: resolvedProperties,
}) as AnyComponentNode;

case "Button":
if (!isResolvedButton(resolvedProperties)) {
throw new Error(`Invalid data; expected ${componentType}`);
}
return new this.objCtor({
...baseNode,
type: "Button",
properties: resolvedProperties,
}) as AnyComponentNode;

case "CheckBox":
if (!isResolvedCheckbox(resolvedProperties)) {
throw new Error(`Invalid data; expected ${componentType}`);
}
return new this.objCtor({
...baseNode,
type: "CheckBox",
properties: resolvedProperties,
}) as AnyComponentNode;

case "TextField":
if (!isResolvedTextField(resolvedProperties)) {
throw new Error(`Invalid data; expected ${componentType}`);
}
return new this.objCtor({
...baseNode,
type: "TextField",
properties: resolvedProperties,
}) as AnyComponentNode;

case "DateTimeInput":
if (!isResolvedDateTimeInput(resolvedProperties)) {
throw new Error(`Invalid data; expected ${componentType}`);
}
return new this.objCtor({
...baseNode,
type: "DateTimeInput",
properties: resolvedProperties,
}) as AnyComponentNode;

case "MultipleChoice":
if (!isResolvedMultipleChoice(resolvedProperties)) {
throw new Error(`Invalid data; expected ${componentType}`);
}
return new this.objCtor({
...baseNode,
type: "MultipleChoice",
properties: resolvedProperties,
}) as AnyComponentNode;

case "Slider":
if (!isResolvedSlider(resolvedProperties)) {
throw new Error(`Invalid data; expected ${componentType}`);
}
return new this.objCtor({
...baseNode,
type: "Slider",
properties: resolvedProperties,
}) as AnyComponentNode;

default:
// Catch-all for other custom component types.
return new this.objCtor({
...baseNode,
type: componentType,
properties: resolvedProperties,
}) as AnyComponentNode;
if (catalogItem) {
return catalogItem.buildNode(baseNode, resolvedProperties, this.objCtor);
}

// Fallback for unknown (custom) components.
return new this.objCtor({
...baseNode,
type: componentType,
properties: resolvedProperties,
}) as AnyComponentNode;
}

/**
Expand Down
Loading
Loading