diff --git a/specification/v0_9/docs/a2ui_protocol.md b/specification/v0_9/docs/a2ui_protocol.md index 1466e1441..7f55ca2d6 100644 --- a/specification/v0_9/docs/a2ui_protocol.md +++ b/specification/v0_9/docs/a2ui_protocol.md @@ -88,8 +88,8 @@ To support A2UI, a transport layer must fulfill the following contract: 1. **Reliable delivery**: Messages must be delivered in the order they were generated. A2UI relies on stateful updates (e.g., creating a surface before updating it), so out-of-order delivery can corrupt the UI state. 2. **Message framing**: The transport must clearly delimit individual JSON envelope messages (e.g., using newlines in JSONL, WebSocket frames, or SSE events). 3. **Metadata support**: The transport must provide a mechanism to associate metadata with messages. This is critical for: - * **Data model synchronization**: The `sendDataModel` feature requires the client to send the current data model state as metadata alongside user actions. - * **Capabilities exchange**: Client capabilities (supported catalogs, custom components) are exchanged via metadata. + - **Data model synchronization**: The `sendDataModel` feature requires the client to send the current data model state as metadata alongside user actions. + - **Capabilities exchange**: Client capabilities (supported catalogs, custom components) are exchanged via metadata. 4. **Bidirectional capability (optional)**: While the rendering stream is unidirectional (Server -> Client), interactive applications require a return channel for `action` messages (Client -> Server). ### Transport bindings @@ -101,16 +101,16 @@ While A2UI is agnostic, it is most commonly used with the following transports. [A2A (Agent-to-Agent)](https://a2a-protocol.org/latest/) is an excellent transport option for A2UI in agentic systems, extending A2A with additional payloads. A2A is uniquely capable of handling remote agent communication, and can also provide a secure and effecient transport between an agentic backend and front end application. -* **Message mapping**: Each A2UI envelope (e.g., `updateComponents`) corresponds to the payload of a single A2A message Part. -* **Metadata**: - * **Data model**: When `sendDataModel` is active, the client's `a2uiClientDataModel` object is placed in the `metadata` field of the A2A message. - * **Capabilities**: The `a2uiClientCapabilities` object is placed in the `metadata` field of every A2A message sent from the client to the server. -* **Context**: A2UI sessions typically map to A2A `contextId`. All messages for a set of related surfaces should share the same `contextId`. +- **Message mapping**: Each A2UI envelope (e.g., `updateComponents`) corresponds to the payload of a single A2A message Part. +- **Metadata**: + - **Data model**: When `sendDataModel` is active, the client's `a2uiClientDataModel` object is placed in the `metadata` field of the A2A message. + - **Capabilities**: The `a2uiClientCapabilities` object is placed in the `metadata` field of every A2A message sent from the client to the server. +- **Context**: A2UI sessions typically map to A2A `contextId`. All messages for a set of related surfaces should share the same `contextId`. #### AG UI (Agent to User Interface) binding **[AG-UI](https://docs.ag-ui.com/introduction)** is also an excellent transport option for A2UI Agent–User Interaction protocol. -AG UI provides convenient integrations into many agent frameworks and frontends. AG UI provides low latency and shared state message passing between front ends and agentic backends. +AG UI provides convenient integrations into many agent frameworks and frontends. AG UI provides low latency and shared state message passing between front ends and agentic backends. #### Other transports @@ -131,7 +131,6 @@ The [`common_types.json`] schema defines reusable primitives used throughout the - **`DynamicString` / `DynamicNumber` / `DynamicBoolean` / `DynamicStringList`**: The core of the data binding system. Any property that can be bound to data is defined as a `Dynamic*` type. It accepts either a literal value, a `path` string ([JSON Pointer]), or a `FunctionCall` (function call). - **`ChildList`**: Defines how containers hold children. It supports: - - `array`: A static array of `ComponentId` component references. - `object`: A template for generating children from a data binding list (requires a template `componentId` and a data binding `path`). @@ -163,11 +162,11 @@ Custom catalogs can be used to define additional UI components or modify the beh To ensure that automated validators can verify the integrity of your UI tree (checking that parents reference existing children), custom catalogs MUST adhere to the following strict typing rules: 1. **Single child references:** Any property that holds the ID of another component MUST use the `ComponentId` type defined in `common_types.json`. - * Use: `"$ref": "common_types.json#/$defs/ComponentId"` - * Do NOT use: `"type": "string"` + - Use: `"$ref": "common_types.json#/$defs/ComponentId"` + - Do NOT use: `"type": "string"` 2. **List references:** Any property that holds a list of children or a template MUST use the `ChildList` type. - * Use: `"$ref": "common_types.json#/$defs/ChildList"` + - Use: `"$ref": "common_types.json#/$defs/ChildList"` Validators determine which fields represent structural links by looking for these specific schema references. If you use a raw string type for an ID, the validator will treat it as static text (like a URL or label) and will not check if the target component exists. @@ -283,7 +282,7 @@ The following example demonstrates a complete interaction to render a Contact Fo ```jsonl {"createSurface":{"surfaceId":"contact_form_1","catalogId":"https://a2ui.dev/specification/v0_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","args":[{"path":"/contact/email"}],"message":"Email is required."},{"call":"email","args":[{"path":"/contact/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":[{"path":"/contact/phone"}, "^\\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","variant":"primary","action":{"event":{"name":"submitContactForm","context":{"formId":"contact_form_1","clientTime":{"call":"now","returnType":"string"},"isNewsletterSubscribed":{"path":"/contact/subscribe"}}}}}]}} +{"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","args":{"value":{"path":"/contact/email"}},"message":"Email is required."},{"call":"email","args":{"value":{"path":"/contact/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":{"value":{"path":"/contact/phone"},"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","variant":"primary","action":{"event":{"name":"submitContactForm","context":{"formId":"contact_form_1","clientTime":{"call":"now","args":{},"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"}} ``` @@ -404,7 +403,6 @@ When a container component (such as `Column`, `Row`, or `List`) utilizes the **T - **Template definition:** When a container binds its children to a path (e.g., `path: "/users"`), the client iterates over the array found at that location. - **Scope instantiation:** For every item in the array, the client instantiates the template component. - **Relative resolution:** Inside these instantiated components, any path that **does not** start with a forward slash `/` is treated as a **Relative Path**. - - A relative path `firstName` inside a template iterating over `/users` resolves to `/users/0/firstName` for the first item, `/users/1/firstName` for the second, etc. - **Mixing scopes:** Components inside a Child Scope can still access the Root Scope by using an Absolute Path. @@ -571,9 +569,9 @@ _Replace the entire data model:_ When `sendDataModel` is set to `true` for a surface, the client automatically appends the **entire data model** of that surface to the metadata of every message (such as `action` or user query) sent to the server that created the surface. The data model is included using the transport's metadata facility (e.g., the `metadata` field in A2A or a header in HTTP). The payload follows the schema in [`a2ui_client_data_model.json`](../json/a2ui_client_data_model.json). - **Targeted Delivery**: The data model is sent exclusively to the server that created the surface. Data cannot leak to other agents or servers. -- **Trigger**: Data is sent only when a client-to-server message is triggered (e.g., by a user action like a button click). Passive data changes (like typing in a text field) do not trigger a network request on their own; they simply update the local state, which will be sent with the next action. -- **Payload**: The data model is included in the transport metadata, tagged by its `surfaceId`. -- **Convergence**: The server treats the received data model as the current state of the client at the time of the action. +- **Trigger:** Data is sent only when a client-to-server message is triggered (e.g., by a user action like a button click). Passive data changes (like typing in a text field) do not trigger a network request on their own; they simply update the local state, which will be sent with the next action. +- **Payload:** The data model is included in the transport metadata, tagged by its `surfaceId`. +- **Convergence:** The server treats the received data model as the current state of the client at the time of the action. ## Client-side logic & validation @@ -589,15 +587,15 @@ Input components (like `TextField`, `CheckBox`) can define a list of checks. Eac "checks": [ { "call": "required", - "args": [{ "path": "/formData/zip" }], + "args": { "value": { "path": "/formData/zip" } }, "message": "Zip code is required" }, { "call": "regex", - "args": [ - { "path": "/formData/zip" }, - "^[0-9]{5}$" - ], + "args": { + "value": { "path": "/formData/zip" }, + "pattern": "^[0-9]{5}$" + }, "message": "Must be a 5-digit zip code" } ] @@ -616,17 +614,17 @@ Buttons can also define `checks`. If any check fails, the button is automaticall "and": [ { "call": "required", - "args": [{ "path": "/formData/terms" }] + "args": { "value": { "path": "/formData/terms" } } }, { "or": [ { "call": "required", - "args": [{ "path": "/formData/email" }] + "args": { "value": { "path": "/formData/email" } } }, { "call": "required", - "args": [{ "path": "/formData/phone" }] + "args": { "value": { "path": "/formData/phone" } } } ] } @@ -666,15 +664,19 @@ The [`standard_catalog.json`] provides the baseline set of components and functi ### Functions -| Function | Description | -| :---------------- | :----------------------------------------------------------------------- | -| **required** | Checks that the value is not null, undefined, or empty. | -| **regex** | Checks that the value matches a regular expression string. | -| **length** | Checks string length constraints. | -| **numeric** | Checks numeric range constraints. | -| **email** | Checks that the value is a valid email address. | -| **formatString** | Does string interpolation of data model values and registered functions. | -| **openUrl** | Opens a URL in a browser. | +| Function | Description | +| :----------------- | :----------------------------------------------------------------------- | +| **required** | Checks that the value is not null, undefined, or empty. | +| **regex** | Checks that the value matches a regular expression string. | +| **length** | Checks string length constraints. | +| **numeric** | Checks numeric range constraints. | +| **email** | Checks that the value is a valid email address. | +| **formatString** | Does string interpolation of data model values and registered functions. | +| **formatNumber** | Formats a number with grouping and precision. | +| **formatCurrency** | Formats a number as a currency string. | +| **formatDate** | Formats a date/time using a pattern. | +| **pluralize** | Selects a localized string based on a numeric count. | +| **openUrl** | Opens a URL in a browser. | ### Theme @@ -715,9 +717,9 @@ Values from the data model can be interpolated using their JSON Pointer path. "component": "Text", "text": { "call": "formatString", - "args": [ - "Hello, ${/user/firstName}! Welcome back to ${/appName}." - ] + "args": { + "value": "Hello, ${/user/firstName}! Welcome back to ${/appName}." + } } } ``` @@ -727,7 +729,7 @@ Values from the data model can be interpolated using their JSON Pointer path. Results of client-side functions can be interpolated. Function calls are identified by the presence of parentheses `()`. - `${now()}`: A function with no arguments. -- `${formatDate(${/currentDate}, 'yyyy-MM-dd')}`: A function with positional arguments. +- `${formatDate(value:${/currentDate}, format:'yyyy-MM-dd')}`: A function with named arguments. Arguments can be **Literals** (quoted strings, numbers, or booleans), or **Nested Expressions**. @@ -735,7 +737,7 @@ Arguments can be **Literals** (quoted strings, numbers, or booleans), or **Neste Expressions can be nested using additional `${...}` wrappers inside an outer expression to make bindings explicit or to chain function calls. -- **Explicit Binding**: `${formatDate(${/currentDate}, 'yyyy-MM-dd')}` +- **Explicit Binding**: `${formatDate(value:${/currentDate}, format:'yyyy-MM-dd')}` - **Nested Functions**: `${upper(${now()})}` #### `formatString` type conversion @@ -751,7 +753,6 @@ When a non-string value is interpolated, the client converts it to a string: The A2UI protocol is designed to be used in a three-step loop with a Large Language Model: 1. **Prompt**: Construct a prompt for the LLM that includes: - - The desired UI to be generated. - The A2UI JSON schema, including the component catalog. - Examples of valid A2UI JSON. diff --git a/specification/v0_9/docs/evolution_guide.md b/specification/v0_9/docs/evolution_guide.md index 8a4c7dcf8..ab566f52b 100644 --- a/specification/v0_9/docs/evolution_guide.md +++ b/specification/v0_9/docs/evolution_guide.md @@ -228,7 +228,7 @@ Specifying an unknown surfaceId will cause an error. It is recommended that clie - **String Formatting**: Introduced the `formatString` function, which supports `${expression}` syntax for interpolation. - **Unified Expression Language**: Allows embedding JSON Pointer paths (absolute and relative) and client-side function calls directly within the format string. -- **Nesting**: Supports recursive nesting of expressions (e.g., `${formatDate(${/timestamp}, 'yyyy-MM-dd')}`). +- **Nesting**: Supports recursive nesting of expressions (e.g., `${formatDate(value: ${/timestamp}, format: 'yyyy-MM-dd')}`). - **Restriction**: String interpolation `${...}` is **ONLY** supported within the `formatString` function. It is not supported in general for string properties, in order to strictly separate data binding definitions from static content. - **Reason**: Improves readability for complex strings. Instead of generating complex nested JSON objects (like chained concatenations) to combine strings and data, the model can write natural-looking template literals within the `formatString` function. diff --git a/specification/v0_9/eval/src/index.ts b/specification/v0_9/eval/src/index.ts index 2715e4d61..7c10556ed 100644 --- a/specification/v0_9/eval/src/index.ts +++ b/specification/v0_9/eval/src/index.ts @@ -40,6 +40,22 @@ function loadSchemas(): Record { const schema = JSON.parse(schemaString); schemas[path.basename(file)] = schema; } + + // Alias standard_catalog.json to catalog.json to match server_to_client.json references + // This mirrors the logic in run_tests.py + if (schemas["standard_catalog.json"]) { + const catalogSchema = JSON.parse( + JSON.stringify(schemas["standard_catalog.json"]), + ); + if (catalogSchema["$id"]) { + catalogSchema["$id"] = catalogSchema["$id"].replace( + "standard_catalog.json", + "catalog.json", + ); + } + schemas["catalog.json"] = catalogSchema; + } + return schemas; } diff --git a/specification/v0_9/json/common_types.json b/specification/v0_9/json/common_types.json index 2a80dbdf0..ffb569ae5 100644 --- a/specification/v0_9/json/common_types.json +++ b/specification/v0_9/json/common_types.json @@ -74,23 +74,43 @@ "DynamicValue": { "description": "A value that can be a literal, a path, or a function call returning any type.", "oneOf": [ - { "type": "string" }, - { "type": "number" }, - { "type": "boolean" }, - { "$ref": "#/$defs/DataBinding" }, - { "$ref": "#/$defs/FunctionCall" } + { + "type": "string" + }, + { + "type": "number" + }, + { + "type": "boolean" + }, + { + "$ref": "#/$defs/DataBinding" + }, + { + "$ref": "#/$defs/FunctionCall" + } ] }, "DynamicString": { "description": "Represents a string", "oneOf": [ - { "type": "string" }, - { "$ref": "#/$defs/DataBinding" }, + { + "type": "string" + }, + { + "$ref": "#/$defs/DataBinding" + }, { "allOf": [ - { "$ref": "#/$defs/FunctionCall" }, { - "properties": { "returnType": { "const": "string" } }, + "$ref": "#/$defs/FunctionCall" + }, + { + "properties": { + "returnType": { + "const": "string" + } + }, "required": ["returnType"] } ] @@ -100,13 +120,23 @@ "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" }, - { "$ref": "#/$defs/DataBinding" }, + { + "type": "number" + }, + { + "$ref": "#/$defs/DataBinding" + }, { "allOf": [ - { "$ref": "#/$defs/FunctionCall" }, { - "properties": { "returnType": { "const": "number" } }, + "$ref": "#/$defs/FunctionCall" + }, + { + "properties": { + "returnType": { + "const": "number" + } + }, "required": ["returnType"] } ] @@ -116,9 +146,15 @@ "DynamicBoolean": { "description": "A boolean value that can be a literal, a path, or a logic expression (including function calls returning boolean).", "oneOf": [ - { "type": "boolean" }, - { "$ref": "#/$defs/DataBinding" }, - { "$ref": "#/$defs/LogicExpression" } + { + "type": "boolean" + }, + { + "$ref": "#/$defs/DataBinding" + }, + { + "$ref": "#/$defs/LogicExpression" + } ] }, "DynamicStringList": { @@ -126,14 +162,24 @@ "oneOf": [ { "type": "array", - "items": { "type": "string" } + "items": { + "type": "string" + } + }, + { + "$ref": "#/$defs/DataBinding" }, - { "$ref": "#/$defs/DataBinding" }, { "allOf": [ - { "$ref": "#/$defs/FunctionCall" }, { - "properties": { "returnType": { "const": "array" } }, + "$ref": "#/$defs/FunctionCall" + }, + { + "properties": { + "returnType": { + "const": "array" + } + }, "required": ["returnType"] } ] @@ -149,11 +195,13 @@ "description": "The name of the function to call." }, "args": { - "type": "array", + "type": "object", "description": "Arguments passed to the function.", - "items": { + "additionalProperties": { "anyOf": [ - { "$ref": "#/$defs/DynamicValue" }, + { + "$ref": "#/$defs/DynamicValue" + }, { "type": "object", "description": "A literal object argument (e.g. configuration)." @@ -164,7 +212,15 @@ "returnType": { "type": "string", "description": "The expected return type of the function call.", - "enum": ["string", "number", "boolean", "array", "object", "any", "void"], + "enum": [ + "string", + "number", + "boolean", + "array", + "object", + "any", + "void" + ], "default": "boolean" } }, diff --git a/specification/v0_9/json/standard_catalog.json b/specification/v0_9/json/standard_catalog.json index 77722f081..13c877f72 100644 --- a/specification/v0_9/json/standard_catalog.json +++ b/specification/v0_9/json/standard_catalog.json @@ -654,9 +654,14 @@ "description": "Checks that the value is not null, undefined, or empty.", "returnType": "boolean", "parameters": { - "type": "array", - "items": [{ "$ref": "common_types.json#/$defs/DynamicValue" }], - "minItems": 1 + "type": "object", + "properties": { + "value": { + "description": "The value to check." + } + }, + "required": ["value"], + "unevaluatedProperties": false } }, { @@ -664,15 +669,16 @@ "description": "Checks that the value matches a regular expression string.", "returnType": "boolean", "parameters": { - "type": "array", - "items": [ - { "$ref": "common_types.json#/$defs/DynamicValue" }, - { + "type": "object", + "properties": { + "value": { "$ref": "common_types.json#/$defs/DynamicString" }, + "pattern": { "type": "string", "description": "The regex pattern to match against." } - ], - "minItems": 2 + }, + "required": ["value", "pattern"], + "unevaluatedProperties": false } }, { @@ -680,27 +686,23 @@ "description": "Checks string length constraints.", "returnType": "boolean", "parameters": { - "type": "array", - "items": [ - { "$ref": "common_types.json#/$defs/DynamicValue" }, - { - "type": "object", - "properties": { - "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"] }] + "type": "object", + "properties": { + "value": { "$ref": "common_types.json#/$defs/DynamicString" }, + "min": { + "type": "integer", + "minimum": 0, + "description": "The minimum allowed length." + }, + "max": { + "type": "integer", + "minimum": 0, + "description": "The maximum allowed length." } - ], - "minItems": 2 + }, + "required": ["value"], + "anyOf": [{ "required": ["min"] }, { "required": ["max"] }], + "unevaluatedProperties": false } }, { @@ -708,25 +710,21 @@ "description": "Checks numeric range constraints.", "returnType": "boolean", "parameters": { - "type": "array", - "items": [ - { "$ref": "common_types.json#/$defs/DynamicValue" }, - { - "type": "object", - "properties": { - "min": { - "type": "number", - "description": "The minimum allowed value." - }, - "max": { - "type": "number", - "description": "The maximum allowed value." - } - }, - "anyOf": [{ "required": ["min"] }, { "required": ["max"] }] + "type": "object", + "properties": { + "value": { "$ref": "common_types.json#/$defs/DynamicNumber" }, + "min": { + "type": "number", + "description": "The minimum allowed value." + }, + "max": { + "type": "number", + "description": "The maximum allowed value." } - ], - "minItems": 2 + }, + "required": ["value"], + "anyOf": [{ "required": ["min"] }, { "required": ["max"] }], + "unevaluatedProperties": false } }, { @@ -734,20 +732,25 @@ "description": "Checks that the value is a valid email address.", "returnType": "boolean", "parameters": { - "type": "array", - "items": [{ "$ref": "common_types.json#/$defs/DynamicValue" }], - "minItems": 1 + "type": "object", + "properties": { + "value": { "$ref": "common_types.json#/$defs/DynamicString" } + }, + "required": ["value"], + "unevaluatedProperties": false } }, { "name": "formatString", - "description": "Performs string interpolation of data model values and other functions in the catalog functions list and returns the resulting string. The value string can contain interpolated expressions in the `${expression}` format. Supported expression types include: JSON Pointer paths to the data model (e.g., `${/absolute/path}` or `${relative/path}`), and 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 `\\${`.", + "description": "Performs string interpolation of data model values and other functions in the catalog functions list and returns the resulting string. The value string can contain interpolated expressions in the `${expression}` format. Supported expression types include: JSON Pointer paths to the data model (e.g., `${/absolute/path}` or `${relative/path}`), and client-side function calls (e.g., `${now()}`). Function arguments must be named (e.g., `${formatDate(value:${/currentDate}, format:'MM-dd')}`). To include a literal `${` sequence, escape it as `\\${`.", "returnType": "string", "parameters": { - "type": "array", - "items": [{ "description": "The format string.", "type": "string" }], - "additionalItems": { "$ref": "common_types.json#/$defs/DynamicValue" }, - "minItems": 1 + "type": "object", + "properties": { + "value": { "$ref": "common_types.json#/$defs/DynamicString" } + }, + "required": ["value"], + "unevaluatedProperties": false } }, { @@ -755,22 +758,23 @@ "description": "Formats a number with the specified grouping and decimal precision.", "returnType": "string", "parameters": { - "type": "array", - "items": [ - { + "type": "object", + "properties": { + "value": { "$ref": "common_types.json#/$defs/DynamicNumber", "description": "The number to format." }, - { + "decimals": { "$ref": "common_types.json#/$defs/DynamicNumber", "description": "Optional. The number of decimal places to show. Defaults to 0 or 2 depending on locale." }, - { + "grouping": { "$ref": "common_types.json#/$defs/DynamicBoolean", "description": "Optional. If true, uses locale-specific grouping separators (e.g. '1,000'). If false, returns raw digits (e.g. '1000'). Defaults to true." } - ], - "minItems": 1 + }, + "required": ["value"], + "unevaluatedProperties": false } }, { @@ -778,18 +782,27 @@ "description": "Formats a number as a currency string.", "returnType": "string", "parameters": { - "type": "array", - "items": [ - { + "type": "object", + "properties": { + "value": { "$ref": "common_types.json#/$defs/DynamicNumber", "description": "The monetary amount." }, - { + "currency": { "$ref": "common_types.json#/$defs/DynamicString", "description": "The ISO 4217 currency code (e.g., 'USD', 'EUR')." + }, + "decimals": { + "$ref": "common_types.json#/$defs/DynamicNumber", + "description": "Optional. The number of decimal places to show. Defaults to 0 or 2 depending on locale." + }, + "grouping": { + "$ref": "common_types.json#/$defs/DynamicBoolean", + "description": "Optional. If true, uses locale-specific grouping separators (e.g. '1,000'). If false, returns raw digits (e.g. '1000'). Defaults to true." } - ], - "minItems": 2 + }, + "required": ["currency", "value"], + "unevaluatedProperties": false } }, { @@ -797,18 +810,19 @@ "description": "Formats a timestamp into a string using a pattern.", "returnType": "string", "parameters": { - "type": "array", - "items": [ - { + "type": "object", + "properties": { + "value": { "$ref": "common_types.json#/$defs/DynamicValue", "description": "The date to format." }, - { + "format": { "$ref": "common_types.json#/$defs/DynamicString", "description": "A Unicode TR35 date pattern string.\n\nToken Reference:\n- Year: 'yy' (26), 'yyyy' (2026)\n- Month: 'M' (1), 'MM' (01), 'MMM' (Jan), 'MMMM' (January)\n- Day: 'd' (1), 'dd' (01), 'E' (Tue), 'EEEE' (Tuesday)\n- Hour (12h): 'h' (1-12), 'hh' (01-12) - requires 'a' for AM/PM\n- Hour (24h): 'H' (0-23), 'HH' (00-23) - Military Time\n- Minute: 'mm' (00-59)\n- Second: 'ss' (00-59)\n- Period: 'a' (AM/PM)\n\nExamples:\n- 'MMM dd, yyyy' -> 'Jan 16, 2026'\n- 'HH:mm' -> '14:30' (Military)\n- 'h:mm a' -> '2:30 PM'\n- 'EEEE, d MMMM' -> 'Friday, 16 January'" } - ], - "minItems": 2 + }, + "required": ["format", "value"], + "unevaluatedProperties": false } }, { @@ -816,49 +830,39 @@ "description": "Returns a localized string based on the Common Locale Data Repository (CLDR) plural category of the count (zero, one, two, few, many, other). Requires an 'other' fallback. For English, just use 'one' and 'other'.", "returnType": "string", "parameters": { - "type": "array", - "items": [ - { + "type": "object", + "properties": { + "value": { "$ref": "common_types.json#/$defs/DynamicNumber", "description": "The numeric value used to determine the plural category." }, - { - "type": "object", - "description": "A map of CLDR plural categories to their corresponding strings.", - "properties": { - "zero": { - "$ref": "common_types.json#/$defs/DynamicString", - "description": "String for the 'zero' category (e.g., 0 items)." - }, - "one": { - "$ref": "common_types.json#/$defs/DynamicString", - "description": "String for the 'one' category (e.g., 1 item)." - }, - "two": { - "$ref": "common_types.json#/$defs/DynamicString", - "description": "String for the 'two' category (used in Arabic, Welsh, etc.)." - }, - "few": { - "$ref": "common_types.json#/$defs/DynamicString", - "description": "String for the 'few' category (e.g., small groups in Slavic languages)." - }, - "many": { - "$ref": "common_types.json#/$defs/DynamicString", - "description": "String for the 'many' category (e.g., large groups in various languages)." - }, - "other": { - "$ref": "common_types.json#/$defs/DynamicString", - "description": "The default/fallback string (used for general plural cases)." - } - }, - "required": [ - "other" - ], - "additionalProperties": false + "zero": { + "$ref": "common_types.json#/$defs/DynamicString", + "description": "String for the 'zero' category (e.g., 0 items)." + }, + "one": { + "$ref": "common_types.json#/$defs/DynamicString", + "description": "String for the 'one' category (e.g., 1 item)." + }, + "two": { + "$ref": "common_types.json#/$defs/DynamicString", + "description": "String for the 'two' category (used in Arabic, Welsh, etc.)." + }, + "few": { + "$ref": "common_types.json#/$defs/DynamicString", + "description": "String for the 'few' category (e.g., small groups in Slavic languages)." + }, + "many": { + "$ref": "common_types.json#/$defs/DynamicString", + "description": "String for the 'many' category (e.g., large groups in various languages)." + }, + "other": { + "$ref": "common_types.json#/$defs/DynamicString", + "description": "The default/fallback string (used for general plural cases)." } - ], - "minItems": 2, - "maxItems": 2 + }, + "required": ["value", "other"], + "unevaluatedProperties": false } }, { diff --git a/specification/v0_9/test/cases/button_checks.json b/specification/v0_9/test/cases/button_checks.json index 2e957f815..91a5e93af 100644 --- a/specification/v0_9/test/cases/button_checks.json +++ b/specification/v0_9/test/cases/button_checks.json @@ -18,19 +18,19 @@ "and": [ { "call": "required", - "args": [{ "path": "/formData/terms" }], + "args": { "value": { "path": "/formData/terms" } }, "returnType": "boolean" }, { "or": [ { "call": "required", - "args": [{ "path": "/formData/email" }], + "args": { "value": { "path": "/formData/email" } }, "returnType": "boolean" }, { "call": "required", - "args": [{ "path": "/formData/phone" }], + "args": { "value": { "path": "/formData/phone" } }, "returnType": "boolean" } ] @@ -79,7 +79,7 @@ "checks": [ { "call": "required", - "args": ["foo"], + "args": { "value": "foo" }, "returnType": "not_a_valid_type" } ] diff --git a/specification/v0_9/test/cases/checkable_components.json b/specification/v0_9/test/cases/checkable_components.json index 7b9b1a810..ade580f29 100644 --- a/specification/v0_9/test/cases/checkable_components.json +++ b/specification/v0_9/test/cases/checkable_components.json @@ -16,13 +16,13 @@ "checks": [ { "call": "required", - "args": [{ "path": "/formData/email" }], + "args": { "value": { "path": "/formData/email" } }, "returnType": "boolean", "message": "Email is required" }, { "call": "email", - "args": [{ "path": "/formData/email" }], + "args": { "value": { "path": "/formData/email" } }, "returnType": "boolean", "message": "Must be valid email" } @@ -52,7 +52,10 @@ "checks": [ { "call": "length", - "args": [{ "path": "/formData/interests" }, { "min": 1 }], + "args": { + "value": { "path": "/formData/interests" }, + "min": 1 + }, "returnType": "boolean", "message": "Select at least one" } @@ -79,7 +82,10 @@ "checks": [ { "call": "numeric", - "args": [{ "path": "/formData/rating" }, { "min": 3 }], + "args": { + "value": { "path": "/formData/rating" }, + "min": 3 + }, "returnType": "boolean", "message": "Rating must be > 3" } @@ -104,7 +110,7 @@ "checks": [ { "call": "required", - "args": [{ "path": "/formData/agreed" }], + "args": { "value": { "path": "/formData/agreed" } }, "returnType": "boolean", "message": "Must agree" } @@ -129,7 +135,7 @@ "checks": [ { "call": "required", - "args": [{ "path": "/formData/date" }], + "args": { "value": { "path": "/formData/date" } }, "returnType": "boolean", "message": "Date required" } @@ -154,7 +160,10 @@ "checks": [ { "call": "regex", - "args": [{ "path": "/formData/phone" }, "^\\d{10}$"], + "args": { + "value": { "path": "/formData/phone" }, + "pattern": "^\\d{10}$" + }, "message": "Must be 10 digits" } ] @@ -178,7 +187,11 @@ "checks": [ { "call": "length", - "args": [{ "path": "/formData/pw" }, { "min": 8, "max": 64 }], + "args": { + "value": { "path": "/formData/pw" }, + "min": 8, + "max": 64 + }, "message": "Password must be 8-64 characters" } ] @@ -204,7 +217,11 @@ "checks": [ { "call": "numeric", - "args": [{ "path": "/formData/score" }, { "min": 0, "max": 100 }], + "args": { + "value": { "path": "/formData/score" }, + "min": 0, + "max": 100 + }, "message": "Score must be between 0 and 100" } ] @@ -228,11 +245,28 @@ "checks": [ { "and": [ - { "call": "required", "args": [{ "path": "/formData/code" }] }, + { + "call": "required", + "args": { "value": { "path": "/formData/code" } } + }, { "or": [ - { "call": "regex", "args": [{ "path": "/formData/code" }, "^[A-Z]"] }, - { "not": { "call": "regex", "args": [{ "path": "/formData/code" }, "^[0-9]"] } } + { + "call": "regex", + "args": { + "value": { "path": "/formData/code" }, + "pattern": "^[A-Z]" + } + }, + { + "not": { + "call": "regex", + "args": { + "value": { "path": "/formData/code" }, + "pattern": "^[0-9]" + } + } + } ] } ], @@ -259,7 +293,7 @@ "checks": [ { "call": "email", - "args": [{ "path": "/formData/email" }] + "args": { "value": { "path": "/formData/email" } } } ] } @@ -282,7 +316,7 @@ "checks": [ { "call": "formatString", - "args": ["Hello ${/formData/name}"], + "args": { "value": "Hello ${/formData/name}" }, "returnType": "string", "message": "This should fail because returnType is string, not boolean" } @@ -313,7 +347,11 @@ "checks": [ { "call": "length", - "args": [{ "path": "/formData/interests" }, { "min": 2, "max": 2 }], + "args": { + "value": { "path": "/formData/interests" }, + "min": 2, + "max": 2 + }, "message": "Select exactly 2 interests" } ] diff --git a/specification/v0_9/test/cases/contact_form_example.jsonl b/specification/v0_9/test/cases/contact_form_example.jsonl index 146a12908..52549e983 100644 --- a/specification/v0_9/test/cases/contact_form_example.jsonl +++ b/specification/v0_9/test/cases/contact_form_example.jsonl @@ -1,4 +1,4 @@ {"createSurface":{"surfaceId":"contact_form_1","catalogId":"https://a2ui.dev/specification/v0_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","args":[{"path":"/contact/email"}],"message":"Email is required."},{"call":"email","args":[{"path":"/contact/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":[{"path":"/contact/phone"},"^\\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","variant":"primary","action":{"event":{"name":"submitContactForm","context":{"formId":"contact_form_1","clientTime":{"call":"now","returnType":"string"},"isNewsletterSubscribed":{"path":"/contact/subscribe"}}}}}]}} +{"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","args":{"value":{"path":"/contact/email"}},"message":"Email is required."},{"call":"email","args":{"value":{"path":"/contact/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":{"value":{"path":"/contact/phone"},"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","variant":"primary","action":{"event":{"name":"submitContactForm","context":{"formId":"contact_form_1","clientTime":{"call":"now","args":{},"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"}} \ No newline at end of file diff --git a/specification/v0_9/test/cases/contact_form_example_test.json b/specification/v0_9/test/cases/contact_form_example_test.json index 69a86b37c..35c10a30f 100644 --- a/specification/v0_9/test/cases/contact_form_example_test.json +++ b/specification/v0_9/test/cases/contact_form_example_test.json @@ -69,8 +69,8 @@ "checks": [ { "call": "email", - "args": [{ "path": "/contact/email" }], - "message": "Please enter a valid email address." + "message": "Please enter a valid email address.", + "args": { "value": { "path": "/contact/email" } } } ] }, @@ -122,4 +122,4 @@ } } ] -} \ No newline at end of file +}