diff --git a/specification/0.9/docs/a2ui_extension_specification.md b/specification/0.9/docs/a2ui_extension_specification.md new file mode 100644 index 00000000..b0184366 --- /dev/null +++ b/specification/0.9/docs/a2ui_extension_specification.md @@ -0,0 +1,89 @@ +# A2UI (Agent-to-Agent UI) Extension Spec v0.9 + +## Overview + +This extension implements the A2UI (Agent-to-Agent UI) spec v0.9, a format for agents to send streaming, interactive user interfaces to clients. + +## Extension URI + +The URI of this extension is https://a2ui.org/a2a-extension/a2ui/v0.9 + +This is the only URI accepted for this extension. + +## Core Concepts + +The A2UI extension is built on the following main concepts: + +Surfaces: A "Surface" is a distinct, controllable region of the client's UI. The spec uses a surfaceId to direct updates to specific surfaces (e.g., a main content area, a side panel, or a new chat bubble). This allows a single agent stream to manage multiple UI areas independently. + +Catalog Definition Document: The a2ui extension is catalog-agnostic. All UI components (e.g., Text, Row, Button) and functions (e.g., required, email) are defined in a separate Catalog Definition Schema. This allows clients and servers to negotiate which catalog to use. + +Schemas: The a2ui extension is defined by several primary JSON schemas: + +- Catalog Definition Schema: A standard format for defining a library of components and functions. +- Server-to-Client Message Schema: The core wire format for messages sent from the agent to the client (e.g., updateComponents, updateDataModel). +- Client-to-Server Event Schema: The core wire format for messages sent from the client to the agent (e.g., action). +- Client Capabilities Schema: The schema for the `a2uiClientCapabilities` object. + +Client Capabilities: The client sends its capabilities to the server in an `a2uiClientCapabilities` object. This object is included in the `metadata` field of every A2A `Message` sent from the client to the server. This object allows the client to declare which catalogs it supports. + +## Agent Card details + +Agents advertise their A2UI capabilities in their AgentCard within the `AgentCapabilities.extensions` list. The `params` object defines the agent's specific UI support. + +Example AgentExtension block: + +```json +{ + "uri": "https://a2ui.org/a2a-extension/a2ui/v0.9", + "description": "Ability to render A2UI v0.9", + "required": false, + "params": { + "supportedCatalogIds": [ + "https://a2ui.dev/specification/0.9/standard_catalog.json", + "https://my-company.com/a2ui/v0.9/my_custom_catalog.json" + ], + "acceptsInlineCatalogs": true + } +} +``` + +### Parameter Definitions +- `params.supportedCatalogIds`: (OPTIONAL) An array of strings, where each string is a URI pointing to a Catalog Definition Schema that the agent can generate. +- `params.acceptsInlineCatalogs`: (OPTIONAL) A boolean indicating if the agent can accept an `inlineCatalogs` array in the client's `a2uiClientCapabilities`. If omitted, this defaults to `false`. + +## Extension Activation +Clients indicate their desire to use the A2UI extension by specifying it via the transport-defined A2A extension activation mechanism. + +For JSON-RPC and HTTP transports, this is indicated via the X-A2A-Extensions HTTP header. + +For gRPC, this is indicated via the X-A2A-Extensions metadata value. + +Activating this extension implies that the server can send A2UI-specific messages (like updateComponents) and the client is expected to send A2UI-specific events (like action). + +## Data Encoding + +A2UI messages are encoded as an A2A `DataPart`. + +To identify a `DataPart` as containing A2UI data, it must have the following metadata: + +- `mimeType`: `application/json+a2ui` + +The `data` field of the `DataPart` contains the A2UI JSON message (e.g., `updateComponents`, `action`). + +Example A2UI DataPart: + +```json +{ + "data": { + "createSurface": { + "surfaceId": "user_profile_surface", + "catalogId": "https://a2ui.dev/specification/0.9/standard_catalog.json" + } + }, + "kind": "data", + "metadata": { + "mimeType": "application/json+a2ui" + } +} +``` diff --git a/specification/0.9/docs/a2ui_protocol.md b/specification/0.9/docs/a2ui_protocol.md index 25c03b0c..f814680f 100644 --- a/specification/0.9/docs/a2ui_protocol.md +++ b/specification/0.9/docs/a2ui_protocol.md @@ -35,7 +35,7 @@ Version 0.9 of the A2UI protocol represents a philosophical shift from previous This "prompt-first" approach offers several advantages: 1. **Richer Schema:** The protocol is no longer limited by the constraints of structured output formats. This allows for more readable, complex, and expressive component catalogs. -2. **Modularity:** The schema is now refactored into separate, more manageable components (e.g., [`common_types.json`], [`standard_catalog_definition.json`], [`server_to_client.json`]), improving maintainability and modularity. +2. **Modularity:** The schema is now refactored into separate, more manageable components (e.g., [`common_types.json`], [`standard_catalog.json`], [`server_to_client.json`]), improving maintainability and modularity. The main disadvantage of this approach is that it requires more complex post-generation validation, as the LLM is not strictly constrained by the schema. This requires robust error handling and correction, so the system can identify discrepancies and attempt to fix them before rendering, or request a retry or correction from the LLM. @@ -100,7 +100,7 @@ The [`server_to_client.json`] schema is the top-level entry point. Every line st ### The Standard Catalog -The [`standard_catalog_definition.json`] schema contains the definitions for all specific UI components (e.g., `Text`, `Button`, `Row`). By separating this from the envelope, developers can easily swap in custom catalogs (e.g., `material_catalog.json` or `cupertino_catalog.json`) without rewriting the core protocol parser. +The [`standard_catalog.json`] schema contains the definitions for all specific UI components (e.g., `Text`, `Button`, `Row`) and functions (e.g., `required`, `email`). By separating this from the envelope, developers can easily swap in custom catalogs (e.g., `material_catalog.json` or `cupertino_catalog.json`) without rewriting the core protocol parser. Custom catalogs can be used to define additional UI components or modify the behavior of existing components. To use a custom catalog, simply include it in the prompt in place of the standard catalog. It should have the same form as the standard catalog, and use common elements in the [`common_types.json`] schema. @@ -115,7 +115,7 @@ This message signals the client to create a new surface and begin rendering it. **Properties:** - `surfaceId` (string, required): The unique identifier for the UI surface to be rendered. -- `catalogId` (string, required): A string that uniquely identifies the component catalog used for this surface. It is recommended to prefix this with an internet domain that you own, to avoid conflicts (e.g., `https://mycompany.com/1.0/somecatalog`). +- `catalogId` (string, required): A string that uniquely identifies the catalog (components and functions) used for this surface. It is recommended to prefix this with an internet domain that you own, to avoid conflicts (e.g., `https://mycompany.com/1.0/somecatalog`). **Example:** @@ -123,7 +123,7 @@ This message signals the client to create a new surface and begin rendering it. { "createSurface": { "surfaceId": "user_profile_card", - "catalogId": "https://a2ui.dev/specification/0.9/standard_catalog_definition.json" + "catalogId": "https://a2ui.dev/specification/0.9/standard_catalog.json" } } ``` @@ -212,9 +212,9 @@ This message instructs the client to remove a surface and all its associated com The following example demonstrates a complete interaction to render a Contact Form, expressed as a JSONL stream. ```jsonl -{"createSurface":{"surfaceId":"contact_form_1","catalogId":"https://a2ui.dev/specification/0.9/standard_catalog_definition.json"}} -{"updateComponents":{"surfaceId":"contact_form_1","components":[{"id":"root","component":"Column","children":["first_name_label","first_name_field","last_name_label","last_name_field","email_label","email_field","phone_label","phone_field","notes_label","notes_field","submit_button"]},{"id":"first_name_label","component":"Text","text":"First Name"},{"id":"first_name_field","component":"TextField","label":"First Name","value":{"path":"/contact/firstName"},"variant":"shortText"},{"id":"last_name_label","component":"Text","text":"Last Name"},{"id":"last_name_field","component":"TextField","label":"Last Name","value":{"path":"/contact/lastName"},"variant":"shortText"},{"id":"email_label","component":"Text","text":"Email"},{"id":"email_field","component":"TextField","label":"Email","value":{"path":"/contact/email"},"variant":"shortText","checks":[{"call":"email","message":"Please enter a valid email address."}]},{"id":"phone_label","component":"Text","text":"Phone"},{"id":"phone_field","component":"TextField","label":"Phone","value":{"path":"/contact/phone"},"variant":"shortText"},{"id":"notes_label","component":"Text","text":"Notes"},{"id":"notes_field","component":"TextField","label":"Notes","value":{"path":"/contact/notes"},"variant":"longText"},{"id":"submit_button_label","component":"Text","text":"Submit"},{"id":"submit_button","component":"Button","child":"submit_button_label","action":{"name":"submitContactForm"}}]}} -{"updateDataModel": {"surfaceId": "contact_form_1", "path": "/contact", "value": {"firstName": "John", "lastName": "Doe", "email": "john.doe@example.com"}}} +{"createSurface":{"surfaceId":"contact_form_1","catalogId":"https://a2ui.dev/specification/0.9/standard_catalog.json"}} +{"updateComponents":{"surfaceId":"contact_form_1","components":[{"id":"root","component":"Card","child":"form_container"},{"id":"form_container","component":"Column","children":["header_row","name_row","email_group","phone_group","pref_group","divider_1","newsletter_checkbox","submit_button"],"justify":"start","align":"stretch"},{"id":"header_row","component":"Row","children":["header_icon","header_text"],"align":"center"},{"id":"header_icon","component":"Icon","name":"mail"},{"id":"header_text","component":"Text","text":"# Contact Us","variant":"h2"},{"id":"name_row","component":"Row","children":["first_name_group","last_name_group"],"justify":"spaceBetween"},{"id":"first_name_group","component":"Column","children":["first_name_label","first_name_field"],"weight":1},{"id":"first_name_label","component":"Text","text":"First Name","variant":"caption"},{"id":"first_name_field","component":"TextField","label":"First Name","value":{"path":"/contact/firstName"},"variant":"shortText"},{"id":"last_name_group","component":"Column","children":["last_name_label","last_name_field"],"weight":1},{"id":"last_name_label","component":"Text","text":"Last Name","variant":"caption"},{"id":"last_name_field","component":"TextField","label":"Last Name","value":{"path":"/contact/lastName"},"variant":"shortText"},{"id":"email_group","component":"Column","children":["email_label","email_field"]},{"id":"email_label","component":"Text","text":"Email Address","variant":"caption"},{"id":"email_field","component":"TextField","label":"Email","value":{"path":"/contact/email"},"variant":"shortText","checks":[{"call":"required","message":"Email is required."},{"call":"email","message":"Please enter a valid email address."}]},{"id":"phone_group","component":"Column","children":["phone_label","phone_field"]},{"id":"phone_label","component":"Text","text":"Phone Number","variant":"caption"},{"id":"phone_field","component":"TextField","label":"Phone","value":{"path":"/contact/phone"},"variant":"shortText","checks":[{"call":"regex","args":{"pattern":"^\\d{10}$"},"message":"Phone number must be 10 digits."}]},{"id":"pref_group","component":"Column","children":["pref_label","pref_picker"]},{"id":"pref_label","component":"Text","text":"Preferred Contact Method","variant":"caption"},{"id":"pref_picker","component":"ChoicePicker","variant":"mutuallyExclusive","options":[{"label":"Email","value":"email"},{"label":"Phone","value":"phone"},{"label":"SMS","value":"sms"}],"value":{"path":"/contact/preference"}},{"id":"divider_1","component":"Divider","axis":"horizontal"},{"id":"newsletter_checkbox","component":"CheckBox","label":"Subscribe to our newsletter","value":{"path":"/contact/subscribe"}},{"id":"submit_button_label","component":"Text","text":"Send Message"},{"id":"submit_button","component":"Button","child":"submit_button_label","primary":true,"action":{"name":"submitContactForm","context":{"formId":"contact_form_1","clientTime":{"call":"now","returnType":"string"},"isNewsletterSubscribed":{"path":"/contact/subscribe"}}}}]}} +{"updateDataModel":{"surfaceId":"contact_form_1","path":"/contact","value":{"firstName":"John","lastName":"Doe","email":"john.doe@example.com","phone":"1234567890","preference":["email"],"subscribe":true}}} ``` ## Component Model @@ -234,7 +234,7 @@ This structure is designed to be both flexible and strictly validated. ### The Component Catalog -The set of available UI components and their properties is defined in a **Component Catalog**. The standard catalog is defined in [`standard_catalog_definition.json`]. This allows for different clients to support different sets of components, including custom ones. The server must generate `updateComponents` messages that conform to the component catalog understood by the client. +The set of available UI components and functions is defined in a **Catalog**. The standard catalog is defined in [`standard_catalog.json`]. This allows for different clients to support different sets of components and functions, including custom ones. The server must generate messages that conform to the catalog understood by the client. ### UI Composition: The Adjacency List Model @@ -495,7 +495,7 @@ Buttons can also define `checks`. If any check fails, the button is automaticall ## Standard Component Catalog -The [`standard_catalog_definition.json`] provides the baseline set of components. +The [`standard_catalog.json`] provides the baseline set of components and functions. | Component | Description | | :---------------- | :------------------------------------------------------------------------------------- | @@ -572,24 +572,25 @@ This message is sent when the user interacts with a component that has an `actio - `timestamp` (string, required): An ISO 8601 timestamp. - `context` (object, required): A JSON object containing any context provided in the component's `action` property. -### `capabilities` +### Client Capabilities -This message is sent by the client upon connection to inform the server of its capabilities, including supported component catalogs and validation catalogs. +Client capabilities are sent by the client to inform the server of its capabilities, including supported catalogs (components and functions). In A2UI v0.9, these are sent as part of the **A2A metadata** envelope in every message, rather than as a first-class A2UI message. This ensures the server always has the client's latest capabilities without needing a separate handshake. + +The `a2uiClientCapabilities` object in the metadata follows the [`a2ui_client_capabilities_schema.json`] schema. **Properties:** -- `supportedCatalogIds` (array of strings, required): URIs of supported component catalogs. -- `supportedFunctionCatalogIds`: A list of URIs for the function catalogs supported by the client. -- `inlineCatalogs`: An array of inline component catalog definitions provided directly by the client (useful for custom or ad-hoc components). -- `inlineFunctionCatalogs`: An array of function catalog definitions provided directly by the client (useful for custom or ad-hoc functions). +- `supportedCatalogIds` (array of strings, required): URIs of supported catalogs. +- `inlineCatalogs`: An array of inline catalog definitions provided directly by the client (useful for custom or ad-hoc components and functions). ### `error` This message is used to report a client-side error to the server. -[`standard_catalog_definition.json`]: ../json/standard_catalog_definition.json +[`standard_catalog.json`]: ../json/standard_catalog.json [`common_types.json`]: ../json/common_types.json [`server_to_client.json`]: ../json/server_to_client.json [`client_to_server.json`]: ../json/client_to_server.json +[`a2ui_client_capabilities_schema.json`]: ../json/a2ui_client_capabilities_schema.json [JSON Pointer]: https://datatracker.ietf.org/doc/html/rfc6901 [RFC 6901]: https://datatracker.ietf.org/doc/html/rfc6901 diff --git a/specification/0.9/docs/evolution_guide.md b/specification/0.9/docs/evolution_guide.md index 57f225e4..b8913217 100644 --- a/specification/0.9/docs/evolution_guide.md +++ b/specification/0.9/docs/evolution_guide.md @@ -20,6 +20,7 @@ Version 0.9 represents a fundamental philosophical shift from "Structured Output | **Data Model Update** | Array of Key-Value Pairs | Standard JSON Object | | **Data Binding** | `dataBinding` / `literalString` | `path` / Native JSON types | | **Button Context** | Array of Key-Value pairs | Standard JSON Object | +| **Catalog** | Separate component and function catalogs | Unified Catalog (`standard_catalog.json`) | | **Auxiliary Rules** | N/A | `standard_catalog_rules.txt` | | **Validation** | Basic Schema | Strict `ValidationFailed` feedback loop | | **Interpolation** | N/A (Object wrappers only) | Native `${expression}` syntax | @@ -36,10 +37,11 @@ Version 0.9 represents a fundamental philosophical shift from "Structured Output **v0.9:** - **Modularization**: The schema is strictly split into: - - `common_types.json`: Reusable primitives (IDs, paths, weights). + - `common_types.json`: Reusable primitives (IDs, paths, weights) and logic/expression types. - `server_to_client.json`: The "envelope" defining the message types. - - `standard_catalog_definition.json`: The specific UI components. -- **Benefit**: This allows developers to swap out the `standard_catalog_definition.json` for a `custom_catalog.json` without touching the core protocol envelope. + - `standard_catalog.json`: The unified catalog of UI components and functions. +- **Benefit**: This allows developers to swap out the `standard_catalog.json` for a `custom_catalog.json` without touching the core protocol envelope. +- **Unification**: Components and functions are now part of the same catalog object, simplifying capability negotiation and inline definitions. ### 2.2. Strict Message Typing @@ -78,7 +80,7 @@ Version 0.9 represents a fundamental philosophical shift from "Structured Output - **Purpose**: `createSurface` signals the client to create a new surface and prepare for rendering. - **Style Information Removed**: `createSurface` does **NOT** contain style information. Theming is now handled via the client styles, decoupling it from the message stream. - **Root Rule**: The rule is: "There must be exactly one component with the ID `root`." The "root" attribute that `beginRendering` had has been removed. The client is expected to render as soon as it has a valid tree with a root component. -- **New Requirement**: `createSurface` now requires a **`catalogId`** (URI) to explicitly state which component set is being used. +- **New Requirement**: `createSurface` now requires a **`catalogId`** (URI) to explicitly state which unified catalog (components and functions) is being used. **Example:** @@ -102,7 +104,7 @@ Version 0.9 represents a fundamental philosophical shift from "Structured Output { "createSurface": { "surfaceId": "user_profile_card", - "catalogId": "https://a2ui.dev/specification/0.9/standard_catalog_definition.json" + "catalogId": "https://a2ui.dev/specification/0.9/standard_catalog.json" } } ``` @@ -310,5 +312,4 @@ For developers migrating from earlier versions, here is a quick reference of pro | **TextField** | `text` | `value` | | **Many** | `usageHint` | `variant` | | **Client Message** | `userAction` | `action` | -| **Client Message** | `clientUiCapabilities` | `capabilities` | | **Common Type** | `childrenProperty` | `ChildList` | diff --git a/specification/0.9/eval/src/generation_flow.ts b/specification/0.9/eval/src/generation_flow.ts index b249aff5..cf1c598a 100644 --- a/specification/0.9/eval/src/generation_flow.ts +++ b/specification/0.9/eval/src/generation_flow.ts @@ -41,7 +41,7 @@ export const componentGeneratorFlow = ai.defineFlow( The output MUST be a series of JSON objects, each enclosed in a markdown code block (or a single block with multiple objects). Standard Instructions: -1. Generate a 'createSurface' message with surfaceId 'main' and catalogId 'https://a2ui.dev/specification/0.9/standard_catalog_definition.json'. +1. Generate a 'createSurface' message with surfaceId 'main' and catalogId 'https://a2ui.dev/specification/0.9/standard_catalog.json'. 2. Generate a 'updateComponents' message with surfaceId 'main' containing the requested UI. 3. Ensure all component children are referenced by ID (using the 'children' or 'child' property with IDs), NOT nested inline as objects. 4. If the request involves data binding, you may also generate 'updateDataModel' messages. diff --git a/specification/0.9/eval/src/index.ts b/specification/0.9/eval/src/index.ts index 1b9342c2..2715e4d6 100644 --- a/specification/0.9/eval/src/index.ts +++ b/specification/0.9/eval/src/index.ts @@ -29,8 +29,7 @@ import { analysisFlow } from "./analysis_flow"; const schemaFiles = [ "../../json/common_types.json", - "../../json/expression_types.json", - "../../json/standard_catalog_definition.json", + "../../json/standard_catalog.json", "../../json/server_to_client.json", ]; diff --git a/specification/0.9/eval/src/validator.ts b/specification/0.9/eval/src/validator.ts index 18a04409..b6b46464 100644 --- a/specification/0.9/eval/src/validator.ts +++ b/specification/0.9/eval/src/validator.ts @@ -254,9 +254,9 @@ export class Validator { if (this.ajv && c.component) { const componentType = c.component; const schemaUri = - "https://a2ui.dev/specification/0.9/standard_catalog_definition.json"; + "https://a2ui.dev/specification/0.9/standard_catalog.json"; - const defRef = `${schemaUri}#/$defs/${componentType}`; + const defRef = `${schemaUri}#/components/${componentType}`; const valid = this.ajv.validate(defRef, c); if (!valid) { diff --git a/specification/0.9/json/a2ui_client_capabilities_schema.json b/specification/0.9/json/a2ui_client_capabilities_schema.json new file mode 100644 index 00000000..2d4dab79 --- /dev/null +++ b/specification/0.9/json/a2ui_client_capabilities_schema.json @@ -0,0 +1,81 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "title": "A2UI Client Capabilities Schema", + "description": "A schema for the a2uiClientCapabilities object, which is sent from the client to the server as part of the A2A metadata to describe the client's UI rendering capabilities.", + "type": "object", + "properties": { + "supportedCatalogIds": { + "type": "array", + "description": "The URI of each of the component and function catalogs that is supported by the client.", + "items": { "type": "string" } + }, + "inlineCatalogs": { + "type": "array", + "description": "An array of inline catalog definitions, which can contain both components and functions. This should only be provided if the agent declares 'acceptsInlineCatalogs: true' in its capabilities.", + "items": { "$ref": "#/$defs/Catalog" } + } + }, + "required": ["supportedCatalogIds"], + "$defs": { + "FunctionDefinition": { + "type": "object", + "description": "Describes a function's interface.", + "properties": { + "name": { + "type": "string", + "description": "The unique name of the function." + }, + "description": { + "type": "string", + "description": "A human-readable description of what the function does and how to use it." + }, + "parameters": { + "type": "object", + "description": "A JSON Schema describing the expected arguments (args) for this function.", + "$ref": "https://json-schema.org/draft/2020-12/schema" + }, + "returnType": { + "type": "string", + "enum": ["string", "number", "boolean", "array", "object", "any"], + "description": "The type of value this function returns." + } + }, + "required": ["name", "parameters", "returnType"], + "additionalProperties": false + }, + "Catalog": { + "type": "object", + "description": "A collection of component and function definitions.", + "properties": { + "catalogId": { + "type": "string", + "description": "Unique identifier for this catalog." + }, + "components": { + "type": "object", + "description": "Definitions for UI components supported by this catalog.", + "additionalProperties": { + "$ref": "https://json-schema.org/draft/2020-12/schema" + } + }, + "functions": { + "type": "array", + "description": "Definitions for functions supported by this catalog.", + "items": { + "$ref": "#/$defs/FunctionDefinition" + } + }, + "styles": { + "title": "A2UI Styles", + "description": "A schema that defines a catalog of A2UI styles. Each key is a style name, and each value is the JSON schema for that style's properties.", + "type": "object", + "additionalProperties": { + "$ref": "https://json-schema.org/draft/2020-12/schema" + } + } + }, + "required": ["catalogId"], + "additionalProperties": false + } + } +} diff --git a/specification/0.9/json/client_to_server.json b/specification/0.9/json/client_to_server.json index 750e8c39..5c39cd0d 100644 --- a/specification/0.9/json/client_to_server.json +++ b/specification/0.9/json/client_to_server.json @@ -60,10 +60,11 @@ }, "message": { "type": "string", - "description": "A short one-sentence description of why validation failed." + "description": "A short one or two sentence description of why validation failed." } }, - "required": ["code", "path", "message", "surfaceId"] + "required": ["code", "path", "message", "surfaceId"], + "additionalProperties": false }, { "type": "object", @@ -76,7 +77,7 @@ }, "message": { "type": "string", - "description": "A short one-sentence description of why the error occurred." + "description": "A short one or two sentence description of why the error occurred." }, "surfaceId": { "type": "string", @@ -87,43 +88,7 @@ "additionalProperties": true } ] - }, - "capabilities": { - "$ref": "#/definitions/capabilities" } }, - "oneOf": [ - { "required": ["action"] }, - { "required": ["capabilities"] }, - { "required": ["error"] } - ], - "definitions": { - "capabilities": { - "type": "object", - "description": "Describes the client's UI rendering capabilities.", - "properties": { - "supportedCatalogIds": { - "type": "array", - "description": "The URI of each of the component catalogs that is supported by the client.", - "items": { "type": "string" } - }, - "inlineCatalogs": { - "type": "array", - "description": "An array of inline catalog definitions.", - "items": { "type": "object" } - }, - "supportedFunctionCatalogIds": { - "type": "array", - "description": "A list of function catalog URIs supported by the client.", - "items": { "type": "string" } - }, - "inlineFunctionCatalogs": { - "type": "array", - "description": "A list of function catalogs defined inline by the client.", - "items": { "$ref": "expression_types.json#/$defs/FunctionCatalog" } - } - }, - "required": ["supportedCatalogIds"] - } - } + "oneOf": [{ "required": ["action"] }, { "required": ["error"] }] } diff --git a/specification/0.9/json/common_types.json b/specification/0.9/json/common_types.json index 402c0309..ed30f2ad 100644 --- a/specification/0.9/json/common_types.json +++ b/specification/0.9/json/common_types.json @@ -4,48 +4,102 @@ "title": "A2UI Common Types", "description": "Common type definitions used across A2UI schemas.", "$defs": { - "FunctionCall": { + "id": { + "type": "string", + "description": "The unique identifier for this component." + }, + "ComponentCommon": { "type": "object", - "description": "Invokes a named function on the client.", "properties": { - "call": { - "type": "string", - "description": "The name of the function to call." + "id": { + "$ref": "#/$defs/id" }, - "args": { + "weight": { + "type": "number", + "description": "The relative weight of this component within a Row or Column. This is similar to the CSS 'flex-grow' property." + } + }, + "required": ["id"] + }, + "ChildList": { + "oneOf": [ + { + "type": "array", + "items": { + "type": "string" + }, + "description": "A static list of child component IDs." + }, + { "type": "object", - "description": "Arguments passed to the function.", - "additionalProperties": { - "$ref": "#/$defs/DynamicValue" - } + "description": "A template for generating a dynamic list of children from a data model list. The `componentId` is the component to use as a template.", + "properties": { + "componentId": { + "$ref": "#/$defs/id" + }, + "path": { + "type": "string", + "description": "The path to the list of component property objects in the data model." + } + }, + "required": ["componentId", "path"], + "additionalProperties": false + } + ] + }, + "DynamicValue": { + "description": "A value that can be a literal, a path, or a function call returning any type.", + "oneOf": [ + { + "type": "string" }, - "returnType": { - "type": "string", - "description": "The expected return type of the function call.", - "enum": ["string", "number", "boolean", "array", "object", "any"], - "default": "boolean" + { + "type": "number" + }, + { + "type": "boolean" + }, + { + "type": "object", + "properties": { + "path": { + "type": "string" + } + }, + "required": ["path"], + "additionalProperties": false + }, + { + "$ref": "#/$defs/FunctionCall" } - }, - "required": ["call"] + ] }, "DynamicString": { "description": "Represents a string that can contain interpolated expressions in the `${expression}` format. Supported expression types include: 1) JSON Pointer paths to the data model (e.g., `${/absolute/path}` or `${relative/path}`), and 2) client-side function calls (e.g., `${now()}`). Function arguments must be literals (quoted strings, numbers, booleans) or nested expressions (e.g., `${formatDate(${/currentDate}, 'MM-dd')}`). To include a literal `${` sequence, escape it as `\\${`.", "oneOf": [ - { "type": "string" }, + { + "type": "string" + }, { "type": "object", "properties": { - "path": { "type": "string" } + "path": { + "type": "string" + } }, "required": ["path"], "additionalProperties": false }, { "allOf": [ - { "$ref": "#/$defs/FunctionCall" }, + { + "$ref": "#/$defs/FunctionCall" + }, { "properties": { - "returnType": { "const": "string" } + "returnType": { + "const": "string" + } }, "required": ["returnType"] } @@ -56,21 +110,29 @@ "DynamicNumber": { "description": "Represents a value that can be either a literal number, a path to a number in the data model, or a function call returning a number.", "oneOf": [ - { "type": "number" }, + { + "type": "number" + }, { "type": "object", "properties": { - "path": { "type": "string" } + "path": { + "type": "string" + } }, "required": ["path"], "additionalProperties": false }, { "allOf": [ - { "$ref": "#/$defs/FunctionCall" }, + { + "$ref": "#/$defs/FunctionCall" + }, { "properties": { - "returnType": { "const": "number" } + "returnType": { + "const": "number" + } }, "required": ["returnType"] } @@ -79,26 +141,23 @@ ] }, "DynamicBoolean": { - "description": "Represents a value that can be either a literal boolean, a path to a boolean in the data model, or a function call returning a boolean.", + "description": "A boolean value that can be a literal, a path, or a logic expression (including function calls returning boolean).", "oneOf": [ - { "type": "boolean" }, + { + "type": "boolean" + }, { "type": "object", "properties": { - "path": { "type": "string" } + "path": { + "type": "string" + } }, "required": ["path"], "additionalProperties": false }, { - "allOf": [ - { "$ref": "#/$defs/FunctionCall" }, - { - "properties": { - "returnType": { "const": "boolean" } - } - } - ] + "$ref": "#/$defs/LogicExpression" } ] }, @@ -107,22 +166,30 @@ "oneOf": [ { "type": "array", - "items": { "type": "string" } + "items": { + "type": "string" + } }, { "type": "object", "properties": { - "path": { "type": "string" } + "path": { + "type": "string" + } }, "required": ["path"], "additionalProperties": false }, { "allOf": [ - { "$ref": "#/$defs/FunctionCall" }, + { + "$ref": "#/$defs/FunctionCall" + }, { "properties": { - "returnType": { "const": "array" } + "returnType": { + "const": "array" + } }, "required": ["returnType"] } @@ -130,71 +197,130 @@ } ] }, - "id": { - "type": "string", - "description": "The unique identifier for this component." - }, - "ComponentCommon": { + "FunctionCall": { "type": "object", + "description": "Invokes a named function on the client.", "properties": { - "id": { "$ref": "#/$defs/id" }, - "weight": { - "type": "number", - "description": "The relative weight of this component within a Row or Column. This is similar to the CSS 'flex-grow' property." + "call": { + "type": "string", + "description": "The name of the function to call." + }, + "args": { + "type": "object", + "description": "Arguments passed to the function.", + "additionalProperties": { + "$ref": "#/$defs/DynamicValue" + } + }, + "returnType": { + "type": "string", + "description": "The expected return type of the function call.", + "enum": ["string", "number", "boolean", "array", "object", "any"], + "default": "boolean" } }, - "required": ["id"] + "required": ["call"] }, - "Checkable": { - "description": "Properties for components that support client-side checks.", + "LogicExpression": { "type": "object", - "properties": { - "checks": { - "type": "array", - "description": "A list of checks to perform. These are function calls that must return a boolean indicating validity.", - "items": { "$ref": "expression_types.json#/$defs/CheckRule" } - } - } - }, - "DynamicValue": { - "description": "A value that can be a literal, a path, or a function call returning any type.", + "description": "A boolean expression used for conditional state (e.g. 'enabled').", "oneOf": [ - { "type": "string" }, - { "type": "number" }, - { "type": "boolean" }, - { - "type": "object", - "properties": { "path": { "type": "string" } }, - "required": ["path"], - "additionalProperties": false + "properties": { + "and": { + "type": "array", + "items": { + "$ref": "#/$defs/LogicExpression" + }, + "minItems": 1 + } + }, + "required": ["and"] + }, + { + "properties": { + "or": { + "type": "array", + "items": { + "$ref": "#/$defs/LogicExpression" + }, + "minItems": 1 + } + }, + "required": ["or"] }, - { "$ref": "#/$defs/FunctionCall" } + { + "properties": { + "not": { + "$ref": "#/$defs/LogicExpression" + } + }, + "required": ["not"] + }, + { + "allOf": [ + { + "$ref": "#/$defs/FunctionCall" + }, + { + "properties": { + "returnType": { + "const": "boolean" + } + } + } + ] + }, + { + "properties": { + "true": { + "const": true + } + }, + "required": ["true"] + }, + { + "properties": { + "false": { + "const": false + } + }, + "required": ["false"] + } ] }, - "ChildList": { - "oneOf": [ + "CheckRule": { + "type": "object", + "description": "A single validaton rule applied to an input component.", + "unevaluatedProperties": false, + "allOf": [ { - "type": "array", - "items": { "type": "string" }, - "description": "A static list of child component IDs." + "$ref": "#/$defs/LogicExpression" }, { "type": "object", - "description": "A template for generating a dynamic list of children from a data model list. The `componentId` is the component to use as a template.", "properties": { - "componentId": { - "$ref": "#/$defs/id" - }, - "path": { + "message": { "type": "string", - "description": "The path to the list of component property objects in the data model." + "description": "The error message to display if the check fails." } }, - "required": ["componentId", "path"], - "additionalProperties": false + "required": ["message"] } ] + }, + "Checkable": { + "description": "Properties for components that support client-side checks.", + "type": "object", + "properties": { + "checks": { + "type": "array", + "description": "A list of checks to perform. These are function calls that must return a boolean indicating validity.", + "items": { + "$ref": "#/$defs/CheckRule" + } + } + } } } } diff --git a/specification/0.9/json/contact_form_example.jsonl b/specification/0.9/json/contact_form_example.jsonl deleted file mode 100644 index 6d9d04b1..00000000 --- a/specification/0.9/json/contact_form_example.jsonl +++ /dev/null @@ -1,3 +0,0 @@ -{"createSurface":{"surfaceId":"contact_form_1","catalogId":"https://a2ui.dev/specification/0.9/standard_catalog_definition.json"}} -{"updateComponents":{"surfaceId":"contact_form_1","components":[{"id":"root","component":"Column","children":["first_name_label","first_name_field","last_name_label","last_name_field","email_label","email_field","phone_label","phone_field","notes_label","notes_field","submit_button"]},{"id":"first_name_label","component":"Text","text":"First Name"},{"id":"first_name_field","component":"TextField","label":"First Name","value":{"path":"/contact/firstName"},"variant":"shortText"},{"id":"last_name_label","component":"Text","text":"Last Name"},{"id":"last_name_field","component":"TextField","label":"Last Name","value":{"path":"/contact/lastName"},"variant":"shortText"},{"id":"email_label","component":"Text","text":"Email"},{"id":"email_field","component":"TextField","label":"Email","value":{"path":"/contact/email"},"variant":"shortText","checks":[{"call":"email","message":"Please enter a valid email address."}]},{"id":"phone_label","component":"Text","text":"Phone"},{"id":"phone_field","component":"TextField","label":"Phone","value":{"path":"/contact/phone"},"variant":"shortText"},{"id":"notes_label","component":"Text","text":"Notes"},{"id":"notes_field","component":"TextField","label":"Notes","value":{"path":"/contact/notes"},"variant":"longText"},{"id":"submit_button_label","component":"Text","text":"Submit"},{"id":"submit_button","component":"Button","child":"submit_button_label","action":{"name":"submitContactForm"}}]}} -{"updateDataModel": {"surfaceId": "contact_form_1", "path": "/contact", "value": {"firstName": "John", "lastName": "Doe", "email": "john.doe@example.com"}}} diff --git a/specification/0.9/json/expression_types.json b/specification/0.9/json/expression_types.json deleted file mode 100644 index 4fca6396..00000000 --- a/specification/0.9/json/expression_types.json +++ /dev/null @@ -1,120 +0,0 @@ -{ - "$schema": "https://json-schema.org/draft/2020-12/schema", - "$id": "https://a2ui.dev/specification/0.9/expression_types.json", - "title": "A2UI Expression Types", - "description": "Type definitions for A2UI client-side function calls and logic expressions.", - "$defs": { - "CheckRule": { - "type": "object", - "description": "A single validaton rule applied to an input component.", - "unevaluatedProperties": false, - "allOf": [ - { "$ref": "#/$defs/LogicExpression" }, - { - "type": "object", - "properties": { - "message": { - "type": "string", - "description": "The error message to display if the check fails." - } - }, - "required": ["message"] - } - ] - }, - "LogicExpression": { - "type": "object", - "description": "A boolean expression used for conditional state (e.g. 'enabled').", - "oneOf": [ - { - "properties": { - "and": { - "type": "array", - "items": { "$ref": "#/$defs/LogicExpression" }, - "minItems": 1 - } - }, - "required": ["and"] - }, - { - "properties": { - "or": { - "type": "array", - "items": { "$ref": "#/$defs/LogicExpression" }, - "minItems": 1 - } - }, - "required": ["or"] - }, - { - "properties": { - "not": { "$ref": "#/$defs/LogicExpression" } - }, - "required": ["not"] - }, - { - "allOf": [ - { "$ref": "common_types.json#/$defs/FunctionCall" }, - { - "properties": { - "returnType": { "const": "boolean" } - } - } - ] - }, - { - "properties": { - "true": { "const": true } - }, - "required": ["true"] - }, - { - "properties": { - "false": { "const": true } - }, - "required": ["false"] - } - ] - }, - "FunctionDefinition": { - "type": "object", - "description": "Describes a function's interface.", - "properties": { - "name": { - "type": "string", - "description": "The unique name of the function." - }, - "description": { - "type": "string", - "description": "A human-readable description of what the function does." - }, - "parameters": { - "type": "object", - "description": "A JSON Schema describing the expected arguments (args) for this function.", - "$ref": "https://json-schema.org/draft/2020-12/schema" - }, - "returnType": { - "type": "string", - "enum": ["string", "number", "boolean", "array", "object", "any"], - "description": "The type of value this function returns." - } - }, - "required": ["name", "parameters", "returnType"] - }, - "FunctionCatalog": { - "type": "object", - "description": "A collection of function definitions.", - "properties": { - "catalogId": { - "type": "string", - "description": "Unique identifier for this catalog." - }, - "functions": { - "type": "array", - "items": { "$ref": "#/$defs/FunctionDefinition" } - } - }, - "required": ["catalogId", "functions"] - } - } -} diff --git a/specification/0.9/json/server_to_client.json b/specification/0.9/json/server_to_client.json index 90640f78..2814e23d 100644 --- a/specification/0.9/json/server_to_client.json +++ b/specification/0.9/json/server_to_client.json @@ -52,7 +52,7 @@ "description": "A list containing all UI components for the surface.", "minItems": 1, "items": { - "$ref": "standard_catalog_definition.json#/$defs/anyComponent" + "$ref": "standard_catalog.json#/$defs/anyComponent" } } }, @@ -80,6 +80,14 @@ }, "value": { "description": "The data to be updated in the data model. If present, the value at 'path' is replaced (or created). If omitted, the key at 'path' is removed.", + "type": [ + "object", + "array", + "string", + "number", + "boolean", + "null" + ], "additionalProperties": true } }, diff --git a/specification/0.9/json/standard_catalog_definition.json b/specification/0.9/json/standard_catalog.json similarity index 83% rename from specification/0.9/json/standard_catalog_definition.json rename to specification/0.9/json/standard_catalog.json index fec2e90e..7911b898 100644 --- a/specification/0.9/json/standard_catalog_definition.json +++ b/specification/0.9/json/standard_catalog.json @@ -1,50 +1,10 @@ { "$schema": "https://json-schema.org/draft/2020-12/schema", - "$id": "https://a2ui.dev/specification/0.9/standard_catalog_definition.json", - "title": "A2UI Component Catalog", - "description": "Definitions for the standard catalog of A2UI components.", - "$defs": { - "Theme": { - "type": "object", - "description": "Theming information for the UI.", - "properties": { - "font": { - "type": "string", - "description": "The primary font for the UI." - }, - "primaryColor": { - "type": "string", - "description": "The primary UI color as a hexadecimal code (e.g., '#00BFFF').", - "pattern": "^#[0-9a-fA-F]{6}$" - } - }, - "additionalProperties": false - }, - "anyComponent": { - "oneOf": [ - { "$ref": "#/$defs/Text" }, - { "$ref": "#/$defs/Image" }, - { "$ref": "#/$defs/Icon" }, - { "$ref": "#/$defs/Video" }, - { "$ref": "#/$defs/AudioPlayer" }, - { "$ref": "#/$defs/Row" }, - { "$ref": "#/$defs/Column" }, - { "$ref": "#/$defs/List" }, - { "$ref": "#/$defs/Card" }, - { "$ref": "#/$defs/Tabs" }, - { "$ref": "#/$defs/Divider" }, - { "$ref": "#/$defs/Modal" }, - { "$ref": "#/$defs/Button" }, - { "$ref": "#/$defs/CheckBox" }, - { "$ref": "#/$defs/TextField" }, - { "$ref": "#/$defs/DateTimeInput" }, - { "$ref": "#/$defs/ChoicePicker" }, - { "$ref": "#/$defs/Slider" } - ], - "discriminator": { - "propertyName": "component" - } - }, + "$id": "https://a2ui.dev/specification/0.9/standard_catalog.json", + "title": "A2UI Standard Catalog", + "description": "Unified catalog of standard A2UI components and functions.", + "catalogId": "https://a2ui.dev/specification/0.9/standard_catalog.json", + "components": { "Text": { "type": "object", "allOf": [ @@ -387,43 +347,44 @@ ], "unevaluatedProperties": false }, - "Divider": { + "Modal": { "type": "object", "allOf": [ { "$ref": "common_types.json#/$defs/ComponentCommon" }, { "type": "object", "properties": { - "component": { "const": "Divider" }, - "axis": { + "component": { "const": "Modal" }, + "trigger": { "type": "string", - "description": "The orientation of the divider.", - "enum": ["horizontal", "vertical"] + "description": "The ID of the component that opens the modal when interacted with (e.g., a button). Do NOT define the component inline." + }, + "content": { + "type": "string", + "description": "The ID of the component to be displayed inside the modal. Do NOT define the component inline." } }, - "required": ["component"] + "required": ["component", "trigger", "content"] } ], "unevaluatedProperties": false }, - "Modal": { + "Divider": { "type": "object", "allOf": [ { "$ref": "common_types.json#/$defs/ComponentCommon" }, { "type": "object", "properties": { - "component": { "const": "Modal" }, - "trigger": { - "type": "string", - "description": "The ID of the component that opens the modal when interacted with (e.g., a button). Do NOT define the component inline." - }, - "content": { + "component": { "const": "Divider" }, + "axis": { "type": "string", - "description": "The ID of the component to be displayed inside the modal. Do NOT define the component inline." + "description": "The orientation of the divider.", + "enum": ["horizontal", "vertical"], + "default": "horizontal" } }, - "required": ["component", "trigger", "content"] + "required": ["component"] } ], "unevaluatedProperties": false @@ -467,29 +428,6 @@ ], "unevaluatedProperties": false }, - "CheckBox": { - "type": "object", - "allOf": [ - { "$ref": "common_types.json#/$defs/ComponentCommon" }, - { "$ref": "common_types.json#/$defs/Checkable" }, - { - "type": "object", - "properties": { - "component": { "const": "CheckBox" }, - "label": { - "$ref": "common_types.json#/$defs/DynamicString", - "description": "The text to display next to the checkbox." - }, - "value": { - "$ref": "common_types.json#/$defs/DynamicBoolean", - "description": "The current state of the checkbox (true for checked, false for unchecked)." - } - }, - "required": ["component", "label", "value"] - } - ], - "unevaluatedProperties": false - }, "TextField": { "type": "object", "allOf": [ @@ -518,7 +456,7 @@ ], "unevaluatedProperties": false }, - "DateTimeInput": { + "CheckBox": { "type": "object", "allOf": [ { "$ref": "common_types.json#/$defs/ComponentCommon" }, @@ -526,29 +464,17 @@ { "type": "object", "properties": { - "component": { "const": "DateTimeInput" }, - "value": { - "$ref": "common_types.json#/$defs/DynamicString", - "description": "The selected date and/or time value in ISO 8601 format. If not yet set, initialize with an empty string." - }, - "enableDate": { - "type": "boolean", - "description": "If true, allows the user to select a date." - }, - "enableTime": { - "type": "boolean", - "description": "If true, allows the user to select a time." - }, - "outputFormat": { - "type": "string", - "description": "The desired format for the output string after a date or time is selected." - }, + "component": { "const": "CheckBox" }, "label": { "$ref": "common_types.json#/$defs/DynamicString", - "description": "The text label for the input field." + "description": "The text to display next to the checkbox." + }, + "value": { + "$ref": "common_types.json#/$defs/DynamicBoolean", + "description": "The current state of the checkbox (true for checked, false for unchecked)." } }, - "required": ["component", "value"] + "required": ["component", "label", "value"] } ], "unevaluatedProperties": false @@ -609,9 +535,7 @@ { "type": "object", "properties": { - "component": { - "const": "Slider" - }, + "component": { "const": "Slider" }, "label": { "$ref": "common_types.json#/$defs/DynamicString", "description": "The label for the slider." @@ -629,10 +553,185 @@ "description": "The current value of the slider." } }, + "required": ["component", "value", "min", "max"] + } + ], + "unevaluatedProperties": false + }, + "DateTimeInput": { + "type": "object", + "allOf": [ + { "$ref": "common_types.json#/$defs/ComponentCommon" }, + { "$ref": "common_types.json#/$defs/Checkable" }, + { + "type": "object", + "properties": { + "component": { "const": "DateTimeInput" }, + "value": { + "$ref": "common_types.json#/$defs/DynamicString", + "description": "The selected date and/or time value in ISO 8601 format. If not yet set, initialize with an empty string." + }, + "enableDate": { + "type": "boolean", + "description": "If true, allows the user to select a date." + }, + "enableTime": { + "type": "boolean", + "description": "If true, allows the user to select a time." + }, + "outputFormat": { + "type": "string", + "description": "The desired format for the output string after a date or time is selected." + }, + "label": { + "$ref": "common_types.json#/$defs/DynamicString", + "description": "The text label for the input field." + } + }, "required": ["component", "value"] } ], "unevaluatedProperties": false } + }, + "functions": [ + { + "name": "required", + "description": "Checks that the value is not null, undefined, or empty.", + "returnType": "boolean", + "parameters": { + "allOf": [{ "$ref": "#/$defs/valueParam" }], + "unevaluatedProperties": false + } + }, + { + "name": "regex", + "description": "Checks that the value matches a regular expression string.", + "returnType": "boolean", + "parameters": { + "allOf": [ + { "$ref": "#/$defs/valueParam" }, + { + "type": "object", + "properties": { + "value": { "type": "string" }, + "pattern": { + "type": "string", + "description": "The regex pattern to match against." + } + }, + "required": ["pattern"] + } + ], + "unevaluatedProperties": false + } + }, + { + "name": "length", + "description": "Checks string length constraints.", + "returnType": "boolean", + "parameters": { + "allOf": [ + { "$ref": "#/$defs/valueParam" }, + { + "type": "object", + "properties": { + "value": { "type": "string" }, + "min": { + "type": "integer", + "minimum": 0, + "description": "The minimum allowed length." + }, + "max": { + "type": "integer", + "minimum": 0, + "description": "The maximum allowed length." + } + }, + "anyOf": [{ "required": ["min"] }, { "required": ["max"] }] + } + ], + "unevaluatedProperties": false + } + }, + { + "name": "numeric", + "description": "Checks numeric range constraints.", + "returnType": "boolean", + "parameters": { + "allOf": [ + { "$ref": "#/$defs/valueParam" }, + { + "type": "object", + "properties": { + "value": { "type": "number" }, + "min": { + "type": "number", + "description": "The minimum allowed value." + }, + "max": { + "type": "number", + "description": "The maximum allowed value." + } + }, + "anyOf": [{ "required": ["min"] }, { "required": ["max"] }] + } + ], + "unevaluatedProperties": false + } + }, + { + "name": "email", + "description": "Checks that the value is a valid email address.", + "returnType": "boolean", + "parameters": { + "allOf": [ + { "$ref": "#/$defs/valueParam" }, + { + "type": "object", + "properties": { + "value": { "type": "string" } + } + } + ], + "unevaluatedProperties": false + } + } + ], + "$defs": { + "anyComponent": { + "oneOf": [ + { "$ref": "#/components/Text" }, + { "$ref": "#/components/Image" }, + { "$ref": "#/components/Icon" }, + { "$ref": "#/components/Video" }, + { "$ref": "#/components/AudioPlayer" }, + { "$ref": "#/components/Row" }, + { "$ref": "#/components/Column" }, + { "$ref": "#/components/List" }, + { "$ref": "#/components/Card" }, + { "$ref": "#/components/Tabs" }, + { "$ref": "#/components/Modal" }, + { "$ref": "#/components/Divider" }, + { "$ref": "#/components/Button" }, + { "$ref": "#/components/TextField" }, + { "$ref": "#/components/CheckBox" }, + { "$ref": "#/components/ChoicePicker" }, + { "$ref": "#/components/Slider" }, + { "$ref": "#/components/DateTimeInput" } + ], + "discriminator": { + "propertyName": "component" + } + }, + "valueParam": { + "type": "object", + "properties": { + "value": { + "description": "The value to check." + } + }, + "required": ["value"] + } } } diff --git a/specification/0.9/json/standard_function_catalog.json b/specification/0.9/json/standard_function_catalog.json deleted file mode 100644 index 3977cda8..00000000 --- a/specification/0.9/json/standard_function_catalog.json +++ /dev/null @@ -1,122 +0,0 @@ -{ - "$schema": "https://json-schema.org/draft/2020-12/schema", - "$id": "https://a2ui.dev/specification/0.9/standard_function_catalog.json", - "title": "A2UI Standard Function Catalog", - "description": "Definitions for the standard set of A2UI functions.", - "catalogId": "https://a2ui.dev/specification/0.9/standard_function_catalog.json", - "functions": [ - { - "name": "required", - "description": "Checks that the value is not null, undefined, or empty.", - "returnType": "boolean", - "parameters": { - "allOf": [{ "$ref": "#/$defs/valueParam" }], - "unevaluatedProperties": false - } - }, - { - "name": "regex", - "description": "Checks that the value matches a regular expression string.", - "returnType": "boolean", - "parameters": { - "allOf": [ - { "$ref": "#/$defs/valueParam" }, - { - "type": "object", - "properties": { - "value": { "type": "string" }, - "pattern": { - "type": "string", - "description": "The regex pattern to match against." - } - }, - "required": ["pattern"] - } - ], - "unevaluatedProperties": false - } - }, - { - "name": "length", - "description": "Checks string length constraints.", - "returnType": "boolean", - "parameters": { - "allOf": [ - { "$ref": "#/$defs/valueParam" }, - { - "type": "object", - "properties": { - "value": { "type": "string" }, - "min": { - "type": "integer", - "minimum": 0, - "description": "The minimum allowed length." - }, - "max": { - "type": "integer", - "minimum": 0, - "description": "The maximum allowed length." - } - }, - "anyOf": [{ "required": ["min"] }, { "required": ["max"] }] - } - ], - "unevaluatedProperties": false - } - }, - { - "name": "numeric", - "description": "Checks numeric range constraints.", - "returnType": "boolean", - "parameters": { - "allOf": [ - { "$ref": "#/$defs/valueParam" }, - { - "type": "object", - "properties": { - "value": { "type": "number" }, - "min": { - "type": "number", - "description": "The minimum allowed value." - }, - "max": { - "type": "number", - "description": "The maximum allowed value." - } - }, - "anyOf": [{ "required": ["min"] }, { "required": ["max"] }] - } - ], - "unevaluatedProperties": false - } - }, - { - "name": "email", - "description": "Checks that the value is a valid email address.", - "returnType": "boolean", - "parameters": { - "allOf": [ - { "$ref": "#/$defs/valueParam" }, - { - "type": "object", - "properties": { - "value": { "type": "string" } - } - } - ], - "unevaluatedProperties": false - } - } - ], - "$defs": { - "valueParam": { - "type": "object", - "properties": { - "value": { - "description": "The value to check." - } - }, - "required": ["value"] - } - } -} diff --git a/specification/0.9/json/validate_example.sh b/specification/0.9/json/validate_example.sh deleted file mode 100755 index a8027f8b..00000000 --- a/specification/0.9/json/validate_example.sh +++ /dev/null @@ -1,38 +0,0 @@ -#!/bin/bash - -# 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. - -SERVER_SCHEMA="server_to_client.json" -COMMON_TYPES="common_types.json" -COMPONENT_CATALOG="standard_catalog_definition.json" -EXAMPLE_FILE="contact_form_example.jsonl" -i=0 -while read -r line; do - ((i++)) - TEMP_FILE="example_line_${i}.json" - echo "$line" | jq '.' > "${TEMP_FILE}" - if [ $? -ne 0 ]; then - echo "jq failed to parse line: $line" - rm "${TEMP_FILE}" - continue - fi - pnpx ajv-cli validate --verbose --strict=false -s "${SERVER_SCHEMA}" -r "${COMMON_TYPES}" -r "${COMPONENT_CATALOG}" -r "expression_types.json" --spec=draft2020 -d "${TEMP_FILE}" - if [ $? -ne 0 ]; then - echo "Validation failed for line: $line" - fi - rm "${TEMP_FILE}" -done < "${EXAMPLE_FILE}" - -echo "Validation complete." diff --git a/specification/0.9/test/cases/button_checks.json b/specification/0.9/test/cases/button_checks.json index c8bbb97f..081a609e 100644 --- a/specification/0.9/test/cases/button_checks.json +++ b/specification/0.9/test/cases/button_checks.json @@ -65,7 +65,7 @@ } }, { - "description": "Button with invalid check structure (missing returnType)", + "description": "Button with invalid check structure (invalid returnType)", "valid": false, "data": { "updateComponents": { @@ -79,7 +79,8 @@ "checks": [ { "call": "required", - "args": { "value": "foo" } + "args": { "value": "foo" }, + "returnType": "not_a_valid_type" } ] }, @@ -88,7 +89,7 @@ } } }, - { + { "description": "Button with invalid nested structure (extra property)", "valid": false, "data": { diff --git a/specification/0.9/test/cases/checkable_components.json b/specification/0.9/test/cases/checkable_components.json index df1f70fb..dd03fdd0 100644 --- a/specification/0.9/test/cases/checkable_components.json +++ b/specification/0.9/test/cases/checkable_components.json @@ -12,7 +12,7 @@ "id": "tf1", "component": "TextField", "label": "Email", - "text": { "path": "/formData/email" }, + "value": { "path": "/formData/email" }, "checks": [ { "call": "required", @@ -43,7 +43,7 @@ "id": "cp1", "component": "ChoicePicker", "label": "Interests", - "usageHint": "multipleSelection", + "variant": "multipleSelection", "options": [ { "label": "Code", "value": "code" }, { "label": "Design", "value": "design" } @@ -73,24 +73,24 @@ "surfaceId": "test_surface", "components": [ { - "id": "sl1", - "component": "Slider", - "label": "Rating", - "min": 1, - "max": 5, - "value": { "path": "/formData/rating" }, - "checks": [ - { - "call": "numeric", - "args": { - "value": { "path": "/formData/rating" }, - "min": 3 - }, - "returnType": "boolean", - "message": "Rating must be > 3" - } - ] - } + "id": "sl1", + "component": "Slider", + "label": "Rating", + "min": 1, + "max": 5, + "value": { "path": "/formData/rating" }, + "checks": [ + { + "call": "numeric", + "args": { + "value": { "path": "/formData/rating" }, + "min": 3 + }, + "returnType": "boolean", + "message": "Rating must be > 3" + } + ] + } ] } } diff --git a/specification/0.9/test/cases/contact_form_example.jsonl b/specification/0.9/test/cases/contact_form_example.jsonl new file mode 100644 index 00000000..0c0a0006 --- /dev/null +++ b/specification/0.9/test/cases/contact_form_example.jsonl @@ -0,0 +1,4 @@ +{"createSurface":{"surfaceId":"contact_form_1","catalogId":"https://a2ui.dev/specification/0.9/standard_catalog.json"}} +{"updateComponents":{"surfaceId":"contact_form_1","components":[{"id":"root","component":"Card","child":"form_container"},{"id":"form_container","component":"Column","children":["header_row","name_row","email_group","phone_group","pref_group","divider_1","newsletter_checkbox","submit_button"],"justify":"start","align":"stretch"},{"id":"header_row","component":"Row","children":["header_icon","header_text"],"align":"center"},{"id":"header_icon","component":"Icon","name":"mail"},{"id":"header_text","component":"Text","text":"# Contact Us","variant":"h2"},{"id":"name_row","component":"Row","children":["first_name_group","last_name_group"],"justify":"spaceBetween"},{"id":"first_name_group","component":"Column","children":["first_name_label","first_name_field"],"weight":1},{"id":"first_name_label","component":"Text","text":"First Name","variant":"caption"},{"id":"first_name_field","component":"TextField","label":"First Name","value":{"path":"/contact/firstName"},"variant":"shortText"},{"id":"last_name_group","component":"Column","children":["last_name_label","last_name_field"],"weight":1},{"id":"last_name_label","component":"Text","text":"Last Name","variant":"caption"},{"id":"last_name_field","component":"TextField","label":"Last Name","value":{"path":"/contact/lastName"},"variant":"shortText"},{"id":"email_group","component":"Column","children":["email_label","email_field"]},{"id":"email_label","component":"Text","text":"Email Address","variant":"caption"},{"id":"email_field","component":"TextField","label":"Email","value":{"path":"/contact/email"},"variant":"shortText","checks":[{"call":"required","message":"Email is required."},{"call":"email","message":"Please enter a valid email address."}]},{"id":"phone_group","component":"Column","children":["phone_label","phone_field"]},{"id":"phone_label","component":"Text","text":"Phone Number","variant":"caption"},{"id":"phone_field","component":"TextField","label":"Phone","value":{"path":"/contact/phone"},"variant":"shortText","checks":[{"call":"regex","args":{"pattern":"^\\d{10}$"},"message":"Phone number must be 10 digits."}]},{"id":"pref_group","component":"Column","children":["pref_label","pref_picker"]},{"id":"pref_label","component":"Text","text":"Preferred Contact Method","variant":"caption"},{"id":"pref_picker","component":"ChoicePicker","variant":"mutuallyExclusive","options":[{"label":"Email","value":"email"},{"label":"Phone","value":"phone"},{"label":"SMS","value":"sms"}],"value":{"path":"/contact/preference"}},{"id":"divider_1","component":"Divider","axis":"horizontal"},{"id":"newsletter_checkbox","component":"CheckBox","label":"Subscribe to our newsletter","value":{"path":"/contact/subscribe"}},{"id":"submit_button_label","component":"Text","text":"Send Message"},{"id":"submit_button","component":"Button","child":"submit_button_label","primary":true,"action":{"name":"submitContactForm","context":{"formId":"contact_form_1","clientTime":{"call":"now","returnType":"string"},"isNewsletterSubscribed":{"path":"/contact/subscribe"}}}}]}} +{"updateDataModel":{"surfaceId":"contact_form_1","path":"/contact","value":{"firstName":"John","lastName":"Doe","email":"john.doe@example.com","phone":"1234567890","preference":["email"],"subscribe":true}}} +{"deleteSurface":{"surfaceId":"contact_form_1"}} diff --git a/specification/0.9/test/cases/contact_form_example_test.json b/specification/0.9/test/cases/contact_form_example_test.json index d43b3931..ef59a602 100644 --- a/specification/0.9/test/cases/contact_form_example_test.json +++ b/specification/0.9/test/cases/contact_form_example_test.json @@ -4,12 +4,105 @@ { "description": "Contact Form Example: Create Surface", "valid": true, - "data": {"createSurface":{"surfaceId":"contact_form_1","catalogId":"https://a2ui.dev/specification/0.9/standard_catalog_definition.json"}} + "data": { + "createSurface": { + "surfaceId": "contact_form_1", + "catalogId": "https://a2ui.dev/specification/0.9/standard_catalog.json" + } + } }, { "description": "Contact Form Example: Update Components", "valid": true, - "data": {"updateComponents":{"surfaceId":"contact_form_1","components":[{"id":"root","component":"Column","children":["first_name_label","first_name_field","last_name_label","last_name_field","email_label","email_field","phone_label","phone_field","notes_label","notes_field","submit_button"]},{"id":"first_name_label","component":"Text","text":"First Name"},{"id":"first_name_field","component":"TextField","label":"First Name","text":{"path":"/contact/firstName"},"usageHint":"shortText"},{"id":"last_name_label","component":"Text","text":"Last Name"},{"id":"last_name_field","component":"TextField","label":"Last Name","text":{"path":"/contact/lastName"},"usageHint":"shortText"},{"id":"email_label","component":"Text","text":"Email"},{"id":"email_field","component":"TextField","label":"Email","text":{"path":"/contact/email"},"usageHint":"shortText","checks":[{"call":"email","message":"Please enter a valid email address."}]},{"id":"phone_label","component":"Text","text":"Phone"},{"id":"phone_field","component":"TextField","label":"Phone","text":{"path":"/contact/phone"},"usageHint":"shortText"},{"id":"notes_label","component":"Text","text":"Notes"},{"id":"notes_field","component":"TextField","label":"Notes","text":{"path":"/contact/notes"},"usageHint":"longText"},{"id":"submit_button_label","component":"Text","text":"Submit"},{"id":"submit_button","component":"Button","child":"submit_button_label","action":{"name":"submitContactForm"}}]}} + "data": { + "updateComponents": { + "surfaceId": "contact_form_1", + "components": [ + { + "id": "root", + "component": "Column", + "children": [ + "first_name_label", + "first_name_field", + "last_name_label", + "last_name_field", + "email_label", + "email_field", + "phone_label", + "phone_field", + "notes_label", + "notes_field", + "submit_button" + ] + }, + { + "id": "first_name_label", + "component": "Text", + "text": "First Name" + }, + { + "id": "first_name_field", + "component": "TextField", + "label": "First Name", + "value": { "path": "/contact/firstName" }, + "variant": "shortText" + }, + { + "id": "last_name_label", + "component": "Text", + "text": "Last Name" + }, + { + "id": "last_name_field", + "component": "TextField", + "label": "Last Name", + "value": { "path": "/contact/lastName" }, + "variant": "shortText" + }, + { "id": "email_label", "component": "Text", "text": "Email" }, + { + "id": "email_field", + "component": "TextField", + "label": "Email", + "value": { "path": "/contact/email" }, + "variant": "shortText", + "checks": [ + { + "call": "email", + "message": "Please enter a valid email address." + } + ] + }, + { "id": "phone_label", "component": "Text", "text": "Phone" }, + { + "id": "phone_field", + "component": "TextField", + "label": "Phone", + "value": { "path": "/contact/phone" }, + "variant": "shortText" + }, + { "id": "notes_label", "component": "Text", "text": "Notes" }, + { + "id": "notes_field", + "component": "TextField", + "label": "Notes", + "value": { "path": "/contact/notes" }, + "variant": "longText" + }, + { + "id": "submit_button_label", + "component": "Text", + "text": "Submit" + }, + { + "id": "submit_button", + "component": "Button", + "child": "submit_button_label", + "action": { "name": "submitContactForm" } + } + ] + } + } }, { "description": "Contact Form Example: Update Data Model", diff --git a/specification/0.9/test/run_tests.py b/specification/0.9/test/run_tests.py old mode 100644 new mode 100755 index 799d0387..46a09739 --- a/specification/0.9/test/run_tests.py +++ b/specification/0.9/test/run_tests.py @@ -1,3 +1,5 @@ +#!/usr/bin/env python3 + import json import subprocess import os @@ -14,10 +16,8 @@ SCHEMAS = { "server_to_client.json": os.path.join(SCHEMA_DIR, "server_to_client.json"), "common_types.json": os.path.join(SCHEMA_DIR, "common_types.json"), - "standard_catalog_definition.json": os.path.join(SCHEMA_DIR, "standard_catalog_definition.json"), - "expression_types.json": os.path.join(SCHEMA_DIR, "expression_types.json"), + "standard_catalog.json": os.path.join(SCHEMA_DIR, "standard_catalog.json"), "client_to_server.json": os.path.join(SCHEMA_DIR, "client_to_server.json"), - "standard_function_catalog.json": os.path.join(SCHEMA_DIR, "standard_function_catalog.json"), } def validate_ajv(schema_path, data_path, all_schemas): @@ -45,7 +45,7 @@ def run_suite(suite_path): try: suite = json.load(f) except json.JSONDecodeError as e: - print(f"Error parse JSON in {suite_path}: {e}") + print(f"Error parsing JSON in {suite_path}: {e}") return 0, 0 schema_name = suite.get("schema", "server_to_client.json") @@ -85,24 +85,61 @@ def run_suite(suite_path): return passed, failed +def validate_jsonl_example(jsonl_path): + if not os.path.exists(jsonl_path): + print(f"Error: Example file not found: {jsonl_path}") + return 0, 1 + + print(f"\nValidating JSONL example: {os.path.basename(jsonl_path)}") + print(f"Target Schema: server_to_client.json") + + passed = 0 + failed = 0 + schema_path = SCHEMAS["server_to_client.json"] + + with open(jsonl_path, 'r') as f: + for i, line in enumerate(f): + line = line.strip() + if not line: + continue + + # Use temp file for each line + with open(TEMP_FILE, 'w') as tf: + tf.write(line) + + is_valid, output = validate_ajv(schema_path, TEMP_FILE, SCHEMAS) + if is_valid: + passed += 1 + # print(f" [PASS] Line {i+1}") + else: + failed += 1 + print(f" [FAIL] Line {i+1}") + print(f" Output: {output.strip()}") + + return passed, failed + def main(): if not os.path.exists(CASES_DIR): print(f"No cases directory found at {CASES_DIR}") return test_files = glob.glob(os.path.join(CASES_DIR, "*.json")) - if not test_files: - print("No test files found in cases directory.") - return total_passed = 0 total_failed = 0 + # 1. Run standard test suites for test_file in sorted(test_files): p, f = run_suite(test_file) total_passed += p total_failed += f + # 2. Run .jsonl example validation + example_path = os.path.join(CASES_DIR, "contact_form_example.jsonl") + p, f = validate_jsonl_example(example_path) + total_passed += p + total_failed += f + print("\n" + "="*30) print(f"Total Passed: {total_passed}") print(f"Total Failed: {total_failed}")