-
Notifications
You must be signed in to change notification settings - Fork 834
Proposal with simple interpolation #504
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
46bfb13
5e0a1bd
1172d2b
4fb3059
16ab84a
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -270,9 +270,9 @@ flowchart TD | |
|
|
||
| ``` | ||
|
|
||
| ## Data Model Representation: Binding, Scope, and Interpolation | ||
| ## Data Model Representation: Binding and Scope | ||
|
|
||
| This section describes how UI components **represent** and reference data from the Data Model. A2UI relies on a strictly defined relationship between the UI structure (Components) and the state (Data Model), defining the mechanics of path resolution, variable scope during iteration, and interpolation. | ||
| This section describes how UI components **represent** and reference data from the Data Model. A2UI relies on a strictly defined relationship between the UI structure (Components) and the state (Data Model), defining the mechanics of path resolution, variable scope during iteration. | ||
|
|
||
| ### Path Resolution & Scope | ||
|
|
||
|
|
@@ -342,22 +342,6 @@ When a container component (such as `Column`, `Row`, or `List`) utilizes the **T | |
| } | ||
| ``` | ||
|
|
||
| #### Client-Side Functions | ||
|
|
||
| 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. | ||
|
|
||
| Arguments can be **Literals** (quoted strings, numbers, or booleans), or **Nested Expressions**. | ||
|
|
||
| #### Nested Interpolation | ||
|
|
||
| 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')}` | ||
| - **Nested Functions**: `${upper(${now()})}` | ||
|
|
||
| #### Type Conversion | ||
|
|
||
| When a non-string value is interpolated, the client converts it to a string: | ||
|
|
@@ -568,61 +552,47 @@ The [`standard_catalog.json`] provides the baseline set of components and functi | |
|
|
||
| ### Functions | ||
|
|
||
| #### Validation 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. | | ||
| | **string_format** | Does string interpolation of data model values and registered functions. | | ||
|
|
||
| ### The `string_format` function | ||
|
|
||
| The `string_format` function supports embedding dynamic expressions directly within string properties. This allows for mixing static text with data model values and function results. | ||
|
|
||
| #### _Syntax_ | ||
|
|
||
| Interpolated expressions are enclosed in `${...}`. To include a literal `${` in a string, it must be escaped as `\${`. | ||
| #### String Formatting Functions | ||
|
|
||
| #### _Data Model Binding_ | ||
|
|
||
| Values from the data model can be interpolated using their JSON Pointer path. | ||
|
|
||
| - `${/user/profile/name}`: Absolute path. | ||
| - `${firstName}`: Relative path (resolved against the current collection scope). | ||
| | Function | Description | | ||
| | :---------------- | :---------------------------------------------------------------------------------- | | ||
| | **formatString** | Performs string interpolation on a template string using named arguments. The template contains placeholders in the `${key}` format, which are replaced by the corresponding values from the `args` object. To include a literal `${` in a string, it must be escaped as `\${`. | | ||
| | **formatNumber** | Formats a number with the specified grouping and decimal precision. | | ||
| | **formatCurrency**| Formats a number as a currency string. | | ||
| | **formatDate** | Formats a timestamp into a string using a pattern. | | ||
| | **pluralize** | Returns the singular string if the count is 1, otherwise returns the plural string. | | ||
|
|
||
| **Example:** | ||
|
|
||
| ```json | ||
| { | ||
| "id": "user_welcome", | ||
| "id": "receipt", | ||
| "component": "Text", | ||
| "text": { | ||
| "call": "string_format", | ||
| "call": "formatString", | ||
| "args": { | ||
| "value": "Hello, ${/user/firstName}! Welcome back to ${/appName}." | ||
| "template": "Bought ${quantity} ${item} on ${date} for ${price}.", | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I see what you're going for here, but it seems like it's a lot more verbose than the previous design, and still requires parsing/validating of the template to interpolate the values, and escaping of the template markers. I realize that it's a simpler parser, but it's not that much simpler (and that's not really where we want to optimize complexity). An equivalent To me, this seems easier/smaller to generate, since it doesn't require the extra argument names, "call", and "args" objects and eliminates much of the nesting. It has only marginally harder parsing complexity (which we'll put into a reusable SDK function anyhow). |
||
| "args": { | ||
| "quantity": { "call": "formatNumber", "args": { "value": { "path": "/transaction/count" }, "decimals": 0 } }, | ||
| "item": { "call": "pluralize", "args": { "value": { "path": "/transaction/count" }, "singular": "apple", "plural": "apples" } }, | ||
| "date": { "call": "formatDate", "args": { "value": { "path": "/transaction/timestamp" }, "pattern": "MMM d, yyyy" } }, | ||
| "price": { "call": "formatCurrency", "args": { "value": { "path": "/transaction/amount" }, "currencyCode": "USD" } } | ||
| } | ||
| } | ||
| } | ||
| } | ||
| ``` | ||
|
|
||
| #### _Client-Side Functions_ | ||
|
|
||
| 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. | ||
|
|
||
| Arguments can be **Literals** (quoted strings, numbers, or booleans), or **Nested Expressions**. | ||
|
|
||
| #### _Nested Interpolation_ | ||
|
|
||
| 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')}` | ||
| - **Nested Functions**: `${upper(${now()})}` | ||
|
|
||
| #### _Type Conversion_ | ||
|
|
||
| When a non-string value is interpolated, the client converts it to a string: | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -716,20 +716,107 @@ | |
| } | ||
| }, | ||
| { | ||
| "name": "string_format", | ||
| "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 `\\${`.", | ||
| "name": "formatString", | ||
| "description": "Performs string interpolation on a template string using named arguments. The template can contain placeholders in the `${key}` format, which are replaced by the corresponding values from the `args` object.", | ||
| "returnType": "string", | ||
| "parameters": { | ||
| "allOf": [ | ||
| { "$ref": "#/$defs/valueParam" }, | ||
| { | ||
| "type": "object", | ||
| "required": [ "template", "args" ], | ||
| "properties": { | ||
| "template": { | ||
| "$ref": "common_types.json#/$defs/DynamicString", | ||
| "description": "The template string containing `${key}` placeholders." | ||
| }, | ||
| "args": { | ||
| "type": "object", | ||
| "properties": { | ||
| "value": { "type": "string" } | ||
| } | ||
| "description": "A map of named values to be interpolated into the template. Keys must match the placeholders in the template.", | ||
| "additionalProperties": true | ||
| } | ||
| ], | ||
| "unevaluatedProperties": false | ||
| } | ||
| } | ||
| }, | ||
| { | ||
| "name": "formatNumber", | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I prefer this lower-magic version of string interpolation function calling overall. But I still wonder if there is some middle ground where you need to explicitly pass arguments to stringFormat, but these specific formatting options for certain types are still built into the formatString function rather than separated out, which makes the "call sites" super complex. E.g. trying to take inspiration from Python: At least this way, the core spec and data flow is still pretty simple and synchronous, but the standard catalog just happens to have a slightly magical function that can understand some fancy formatting placeholders.
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. That was my original proposal, but I thought we wanted to do the dart / calling functions approach for string formatting. I'd actually prefer just a single string_format function if that what you prefer. @gspencergoog thoughts?
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I talked this over with Jacob, and I still think that I'd rather use the more "modern" version of string interpolation that just takes a single string:
So, how about this compromise: We keep the args in the string, but include some basic formatters that don't need function calls? Like so: {
"id": "receipt",
"component": "Text",
"text": {
"call": "formatString",
"args": {
"template": "Bought ${/transaction/quantity} ${/transaction/name} on ${/transaction/timestamp:%m %d, %Y} for ${/transaction/amount:.2f}.",
}
}
}In the end, I think the best solution will be whatever generates the best results from the various LLMs, and we kind of need to run an implementation through evals to know that definitively.
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Just wanted to add: if we remove mention of the DSL above from the core A2UI specification, and just include it in the definition for the stringFormat function in the standard catalog, then developers using custom catalogs would be completely free to implement your exact proposal, @wrenj . So people using the standard catalog get a convenient string format syntax that is (hopefully!) simple enough for humans and LLMs to write directly. But Catalog creators have the hooks they need to innovate with other approaches, and they are not tethered to the DSL we are using in the standard catalog. Re the actual DSL (sorry, not sure if it technically counts as a language), maybe we should try to stick more closely to the Python fstring format, e.g. by using
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
It was intended to be close to TypeScript, JavaScript, Dart, Bash, Kotlin, PHP and Groovy. I figured that was a larger learning base than Python and Rust.
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Oh that makes perfect sense. My language knowledge is not as broad as yours :-D. I was referring to the colon syntax in Overall, I don't feel strongly other than borrowing familiar syntax ideas, which we already have! |
||
| "description": "Formats a number with the specified grouping and decimal precision.", | ||
| "returnType": "string", | ||
| "parameters": { | ||
| "type": "object", | ||
| "required": [ "value" ], | ||
| "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." | ||
| }, | ||
| "useGrouping": { | ||
| "$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." | ||
| } | ||
| } | ||
| } | ||
| }, | ||
| { | ||
| "name": "formatCurrency", | ||
| "description": "Formats a number as a currency string.", | ||
| "returnType": "string", | ||
| "parameters": { | ||
| "type": "object", | ||
| "required": [ "value" ], | ||
| "properties": { | ||
| "value": { | ||
| "$ref": "common_types.json#/$defs/DynamicNumber", | ||
| "description": "The monetary amount." | ||
| }, | ||
| "currencyCode": { | ||
| "$ref": "common_types.json#/$defs/DynamicString", | ||
| "description": "Optional. The ISO 4217 currency code (e.g., 'USD', 'EUR'). If omitted, uses the device's default currency." | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. How does the LLM know the device's default currency to be able to put in the relevant value? |
||
| } | ||
| } | ||
| } | ||
| }, | ||
| { | ||
| "name": "formatDate", | ||
| "description": "Formats a timestamp into a string using a pattern.", | ||
| "returnType": "string", | ||
| "parameters": { | ||
| "type": "object", | ||
| "required": [ "value", "pattern" ], | ||
| "properties": { | ||
| "value": { | ||
| "$ref": "common_types.json#/$defs/DynamicDate", | ||
| "description": "The date to format." | ||
| }, | ||
| "pattern": { | ||
| "$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'" | ||
| } | ||
| } | ||
| } | ||
| }, | ||
| { | ||
| "name": "pluralize", | ||
| "description": "Returns the singular string if the count is 1, otherwise returns the plural string.", | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Note that this definition is only sufficient for English text. Other languages will need more complex rules, and more arguments. |
||
| "returnType": "string", | ||
| "parameters": { | ||
| "type": "object", | ||
| "required": [ "value", "singular", "plural" ], | ||
| "properties": { | ||
| "value": { | ||
| "$ref": "common_types.json#/$defs/DynamicNumber", | ||
| "description": "The numeric value to check." | ||
| }, | ||
| "singular": { | ||
| "$ref": "common_types.json#/$defs/DynamicString", | ||
| "description": "The string to return if count is exactly 1." | ||
| }, | ||
| "plural": { | ||
| "$ref": "common_types.json#/$defs/DynamicString", | ||
| "description": "The string to return if count is not 1." | ||
| } | ||
| } | ||
| } | ||
| } | ||
| ], | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@gspencergoog this defines a DSL to call functions from within string templates. I think if we do decide to support a syntax like this, we should move this specification to the "standard catalog" section and make it specific to the
string_formatfunction etc. So maybestring_formathappens to use some magical DSL and be able to refer to the data model and other client-side functions directly (we will expose the necessary rendering framework APIs to make this feasible to implement). A different catalog can invent some other magical DSL. But the core A2UI specification is more purely JSON-based.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah, this section should be removed. I agree that it should only be part of the string_format function's definition, and not general for all strings. (I thought I had already removed that when I added
string_format).