diff --git a/specification/v0_9/docs/a2ui_custom_functions.md b/specification/v0_9/docs/a2ui_custom_functions.md new file mode 100644 index 000000000..fe395fdb6 --- /dev/null +++ b/specification/v0_9/docs/a2ui_custom_functions.md @@ -0,0 +1,121 @@ +# Extending A2UI with Custom Functions + +A2UI functions are designed to be extensible. Third-party developers can define +their own +function catalogs while preserving strict validation for the standard set. + +This guide demonstrates how to create a `custom_catalog.json` that adds a string +`trim` function and a hardware query function (`getScreenResolution`). + +## 1. Define the Custom Catalog + +Create a JSON Schema file (e.g., `custom_catalog.json`) that defines your +function parameters. + +Use the `functions` property to define a map of function schemas. + +```json +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://example.com/schemas/custom_catalog.json", + "title": "Custom Function Catalog", + "description": "Extension catalog adding string trimming and screen resolution functions.", + "functions": { + "trim": { + "type": "object", + "properties": { + "call": { "const": "trim" }, + "returnType": { "const": "string" }, + "args": { + "type": "array", + "minItems": 1, + "maxItems": 2, + "prefixItems": [ + { + "$ref": "https://a2ui.dev/specification/v0_9/common_types.json#/$defs/DynamicString", + "description": "The string to trim." + }, + { + "$ref": "https://a2ui.dev/specification/v0_9/common_types.json#/$defs/DynamicString", + "description": "Optional. A set of characters to remove. Defaults to whitespace." + } + ] + } + }, + "required": ["call", "args"] + }, + "getScreenResolution": { + "type": "object", + "properties": { + "call": { "const": "getScreenResolution" }, + "returnType": { "const": "array" }, + "args": { + "type": "array", + "minItems": 0, + "maxItems": 1, + "prefixItems": [ + { + "$ref": "https://a2ui.dev/specification/v0_9/common_types.json#/$defs/DynamicNumber", + "description": "Optional. The index of the screen to query. Defaults to 0 (primary screen)." + } + ] + } + }, + "required": ["call"] + } + } +} +``` + +## 2. Make the functions available + +The `FunctionCall` definition refers to a [catalog-agnostic reference](a2ui_protocol.md#the-standard-catalog). +In your catalog, you simply need to define the `anyFunctions` reference: +```json +{ + "$defs": { + "anyFunction": { + "oneOf": [ + {"$ref": "#/functions/trim"}, + {"$ref": "#/functions/getScreenResolution"} + ] + } + } +} +``` + +If you want to incorporate functions defined in the [`standard_catalog.json`], +those can be added too: +```json +{ + "$defs": { + "anyFunction": { + "oneOf": [ + {"$ref": "#/functions/trim"}, + {"$ref": "#/functions/getScreenResolution"}, + {"$ref": "catalog.json#/$defs/anyFunction" } + ] + } + } +} +``` + +## How Validation Works + +When a `FunctionCall` is validated: + +1. **Discriminator Lookup:** The validator looks at the `call` property of the + object. +2. **Schema Matching:** + * If `call` is "length", it matches `Functions` -> `length` + and validates the arguments against the length rules. + * If `call` is "trim", it matches `CustomFunctions` -> `trim` and + validates against your custom rules. + * If `call` is "unknownFunc": + * If `GenericFunction` is NOT included, validation FAILS immediately ( + strict mode). + * If `GenericFunction` IS included, it matches the generic fallback and + PASSES (loose mode). + +This strict-by-default approach ensures typos are caught early, while the +modular structure makes it easy to add new capabilities. diff --git a/specification/v0_9/json/common_types.json b/specification/v0_9/json/common_types.json index 2a80dbdf0..c2cd37713 100644 --- a/specification/v0_9/json/common_types.json +++ b/specification/v0_9/json/common_types.json @@ -168,7 +168,10 @@ "default": "boolean" } }, - "required": ["call"] + "required": ["call"], + "oneOf": [ + { "$ref": "catalog.json#/$defs/anyFunction" } + ] }, "LogicExpression": { "type": "object", diff --git a/specification/v0_9/json/standard_catalog.json b/specification/v0_9/json/standard_catalog.json index 77722f081..345627884 100644 --- a/specification/v0_9/json/standard_catalog.json +++ b/specification/v0_9/json/standard_catalog.json @@ -648,241 +648,336 @@ "unevaluatedProperties": false } }, - "functions": [ - { - "name": "required", + "functions": { + "required": { + "type": "object", "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 - } + "properties": { + "call": { "const": "required" }, + "returnType": { "const": "boolean" }, + "args": { + "type": "array", + "minItems": 1, + "prefixItems": [ + { "$ref": "common_types.json#/$defs/DynamicValue" } + ], + "items": false + } + }, + "required": ["call", "args"] }, - { - "name": "regex", + "regex": { + "type": "object", "description": "Checks that the value matches a regular expression string.", - "returnType": "boolean", - "parameters": { - "type": "array", - "items": [ - { "$ref": "common_types.json#/$defs/DynamicValue" }, - { - "type": "string", - "description": "The regex pattern to match against." - } - ], - "minItems": 2 - } + "properties": { + "call": { "const": "regex" }, + "returnType": { "const": "boolean" }, + "args": { + "type": "array", + "minItems": 2, + "prefixItems": [ + { "$ref": "common_types.json#/$defs/DynamicValue" }, + { + "type": "string", + "description": "The regex pattern to match against." + } + ], + "items": false + } + }, + "required": ["call", "args"] }, - { - "name": "length", + "length": { + "type": "object", "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." + "properties": { + "call": { "const": "length" }, + "returnType": { "const": "boolean" }, + "args": { + "type": "array", + "minItems": 2, + "prefixItems": [ + { "$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." + } }, - "max": { - "type": "integer", - "minimum": 0, - "description": "The maximum allowed length." - } - }, - "anyOf": [{ "required": ["min"] }, { "required": ["max"] }] - } - ], - "minItems": 2 - } + "anyOf": [ { "required": [ "min" ] }, { "required": [ "max" ] } ] + } + ], + "items": false + } + }, + "required": ["call", "args"] }, - { - "name": "numeric", + "numeric": { + "type": "object", "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." + "properties": { + "call": { "const": "numeric" }, + "returnType": { "const": "boolean" }, + "args": { + "type": "array", + "minItems": 2, + "prefixItems": [ + { "$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." + } }, - "max": { - "type": "number", - "description": "The maximum allowed value." - } - }, - "anyOf": [{ "required": ["min"] }, { "required": ["max"] }] - } - ], - "minItems": 2 - } + "anyOf": [ { "required": [ "min" ] }, { "required": [ "max" ] } ] + } + ], + "items": false + } + }, + "required": ["call", "args"] }, - { - "name": "email", + "email": { + "type": "object", "description": "Checks that the value is a valid email address.", - "returnType": "boolean", - "parameters": { - "type": "array", - "items": [{ "$ref": "common_types.json#/$defs/DynamicValue" }], - "minItems": 1 - } + "properties": { + "call": { "const": "email" }, + "returnType": { "const": "boolean" }, + "args": { + "type": "array", + "minItems": 1, + "prefixItems": [ + { "$ref": "common_types.json#/$defs/DynamicValue" } + ], + "items": false + } + }, + "required": ["call", "args"] }, - { - "name": "formatString", + "formatString": { + "type": "object", "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 `\\${`.", - "returnType": "string", - "parameters": { - "type": "array", - "items": [{ "description": "The format string.", "type": "string" }], - "additionalItems": { "$ref": "common_types.json#/$defs/DynamicValue" }, - "minItems": 1 - } + "properties": { + "call": { "const": "formatString" }, + "returnType": { "const": "string" }, + "args": { + "type": "array", + "minItems": 1, + "prefixItems": [ + { + "description": "The format string.", + "type": "string" + } + ], + "items": { "$ref": "common_types.json#/$defs/DynamicValue" } + } + }, + "required": ["call", "args"] }, - { - "name": "formatNumber", + "formatNumber": { + "type": "object", "description": "Formats a number with the specified grouping and decimal precision.", - "returnType": "string", - "parameters": { - "type": "array", - "items": [ - { - "$ref": "common_types.json#/$defs/DynamicNumber", - "description": "The number to format." - }, - { - "$ref": "common_types.json#/$defs/DynamicNumber", - "description": "Optional. The number of decimal places to show. Defaults to 0 or 2 depending on locale." - }, - { - "$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 - } + "properties": { + "call": { "const": "formatNumber" }, + "returnType": { "const": "string" }, + "args": { + "type": "array", + "minItems": 1, + "prefixItems": [ + { + "$ref": "common_types.json#/$defs/DynamicNumber", + "description": "The number to format." + }, + { + "$ref": "common_types.json#/$defs/DynamicNumber", + "description": "Optional. The number of decimal places to show. Defaults to 0 or 2 depending on locale." + }, + { + "$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." + } + ], + "items": false + } + }, + "required": ["call", "args"] }, - { - "name": "formatCurrency", + "formatCurrency": { + "type": "object", "description": "Formats a number as a currency string.", - "returnType": "string", - "parameters": { - "type": "array", - "items": [ - { - "$ref": "common_types.json#/$defs/DynamicNumber", - "description": "The monetary amount." - }, - { - "$ref": "common_types.json#/$defs/DynamicString", - "description": "The ISO 4217 currency code (e.g., 'USD', 'EUR')." - } - ], - "minItems": 2 - } + "properties": { + "call": { "const": "formatCurrency" }, + "returnType": { "const": "string" }, + "args": { + "type": "array", + "minItems": 2, + "prefixItems": [ + { + "$ref": "common_types.json#/$defs/DynamicNumber", + "description": "The monetary amount." + }, + { + "$ref": "common_types.json#/$defs/DynamicString", + "description": "The ISO 4217 currency code (e.g., 'USD', 'EUR')." + } + ], + "items": false + } + }, + "required": ["call", "args"] }, - { - "name": "formatDate", + "formatDate": { + "type": "object", "description": "Formats a timestamp into a string using a pattern.", - "returnType": "string", - "parameters": { - "type": "array", - "items": [ - { - "$ref": "common_types.json#/$defs/DynamicValue", - "description": "The date to 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 - } + "properties": { + "call": { "const": "formatDate" }, + "returnType": { "const": "string" }, + "args": { + "type": "array", + "minItems": 2, + "prefixItems": [ + { + "$ref": "common_types.json#/$defs/DynamicValue", + "description": "The date to 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'" + } + ], + "items": false + } + }, + "required": ["call", "args"] }, - { - "name": "pluralize", + "pluralize": { + "type": "object", "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": [ - { - "$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)." - } + "properties": { + "call": { "const": "pluralize" }, + "returnType": { "const": "string" }, + "args": { + "type": "array", + "minItems": 2, + "maxItems": 2, + "prefixItems": [ + { + "$ref": "common_types.json#/$defs/DynamicNumber", + "description": "The numeric value used to determine the plural category." }, - "required": [ - "other" - ], - "additionalProperties": false - } - ], - "minItems": 2, - "maxItems": 2 - } + { + "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 + } + ], + "items": false + } + }, + "required": ["call", "args"] }, - { - "name": "openUrl", + "openUrl": { + "type": "object", "description": "Opens the specified URL in a browser or handler. This function has no return value.", - "returnType": "void", - "parameters": { - "allOf": [ - { - "type": "object", - "properties": { - "url": { - "type": "string", - "format": "uri", - "description": "The URL to open." - } - }, - "required": ["url"] + "properties": { + "call": { "const": "openUrl" }, + "returnType": { "const": "void" }, + "args": { + "type": "array", + "items": false, + "prefixItems": [ + { + "allOf": [ + { + "type": "object", + "properties": { + "url": { + "type": "string", + "format": "uri", + "description": "The URL to open." + } + }, + "required": ["url"] + } + ], + "unevaluatedProperties": false + } + ], + "minItems": 1, + "maxItems": 1 + } + }, + "required": ["call", "args"] + }, + "generic": { + "type": "object", + "description": "Matches any function call not explicitly defined in the standard catalog, allowing for extensibility.", + "properties": { + "call": { + "type": "string", + "not": { + "enum": [ + "required", + "regex", + "length", + "numeric", + "email", + "formatString", + "formatNumber", + "formatCurrency", + "formatDate", + "pluralize", + "openUrl" + ] } - ], - "unevaluatedProperties": false - } + } + }, + "required": ["call"] } - ], + }, "theme": { "primaryColor": { "type": "string", @@ -933,6 +1028,22 @@ "discriminator": { "propertyName": "component" } + }, + "anyFunction": { + "oneOf": [ + { "$ref": "#/functions/required" }, + { "$ref": "#/functions/regex" }, + { "$ref": "#/functions/length" }, + { "$ref": "#/functions/numeric" }, + { "$ref": "#/functions/email" }, + { "$ref": "#/functions/formatString" }, + { "$ref": "#/functions/formatNumber" }, + { "$ref": "#/functions/formatCurrency" }, + { "$ref": "#/functions/formatDate" }, + { "$ref": "#/functions/pluralize" }, + { "$ref": "#/functions/openUrl" }, + { "$ref": "#/functions/generic" } + ] } } } diff --git a/specification/v0_9/test/cases/function_catalog_validation.json b/specification/v0_9/test/cases/function_catalog_validation.json new file mode 100644 index 000000000..ad319115d --- /dev/null +++ b/specification/v0_9/test/cases/function_catalog_validation.json @@ -0,0 +1,682 @@ +{ + "schema": "server_to_client.json", + "tests": [ + { + "description": "required: Valid call", + "valid": true, + "data": { + "updateComponents": { + "surfaceId": "test", + "components": [ + { + "id": "tf1", + "component": "TextField", + "label": "L", + "value": "", + "checks": [ + { + "call": "required", + "args": [{ "path": "/val" }], + "returnType": "boolean", + "message": "Required" + } + ] + } + ] + } + } + }, + { + "description": "required: Invalid args (empty)", + "valid": false, + "data": { + "updateComponents": { + "surfaceId": "test", + "components": [ + { + "id": "tf1", + "component": "TextField", + "label": "L", + "value": "", + "checks": [{ "call": "required", "args": [], "message": "Msg" }] + } + ] + } + } + }, + { + "description": "required: Invalid returnType", + "valid": false, + "data": { + "updateComponents": { + "surfaceId": "test", + "components": [ + { + "id": "tf1", + "component": "TextField", + "label": "L", + "value": "", + "checks": [{ "call": "required", "args": [{"path": "/val"}], "returnType": "string", "message": "Msg" }] + } + ] + } + } + }, + { + "description": "regex: Valid call", + "valid": true, + "data": { + "updateComponents": { + "surfaceId": "test", + "components": [ + { + "id": "tf1", + "component": "TextField", + "label": "L", + "value": "", + "checks": [ + { + "call": "regex", + "args": [{ "path": "/val" }, "^abc$"], + "message": "Match" + } + ] + } + ] + } + } + }, + { + "description": "regex: Invalid args (missing pattern)", + "valid": false, + "data": { + "updateComponents": { + "surfaceId": "test", + "components": [ + { + "id": "tf1", + "component": "TextField", + "label": "L", + "value": "", + "checks": [{ "call": "regex", "args": [{ "path": "/val" }], "message": "Msg" }] + } + ] + } + } + }, + { + "description": "length: Valid min", + "valid": true, + "data": { + "updateComponents": { + "surfaceId": "test", + "components": [ + { + "id": "tf1", + "component": "TextField", + "label": "L", + "value": "", + "checks": [{ "call": "length", "args": [{ "path": "/val" }, { "min": 5 }], "message": "Msg" }] + } + ] + } + } + }, + { + "description": "length: Valid max", + "valid": true, + "data": { + "updateComponents": { + "surfaceId": "test", + "components": [ + { + "id": "tf1", + "component": "TextField", + "label": "L", + "value": "", + "checks": [{ "call": "length", "args": [{ "path": "/val" }, { "max": 10 }], "message": "Msg" }] + } + ] + } + } + }, + { + "description": "length: Invalid constraint (empty object)", + "valid": false, + "data": { + "updateComponents": { + "surfaceId": "test", + "components": [ + { + "id": "tf1", + "component": "TextField", + "label": "L", + "value": "", + "checks": [{ "call": "length", "args": [{ "path": "/val" }, {}], "message": "Msg" }] + } + ] + } + } + }, + { + "description": "numeric: Valid range", + "valid": true, + "data": { + "updateComponents": { + "surfaceId": "test", + "components": [ + { + "id": "tf1", + "component": "TextField", + "label": "L", + "value": "", + "checks": [{ "call": "numeric", "args": [{ "path": "/val" }, { "min": 0, "max": 100 }], "message": "Msg" }] + } + ] + } + } + }, + { + "description": "email: Valid call", + "valid": true, + "data": { + "updateComponents": { + "surfaceId": "test", + "components": [ + { + "id": "tf1", + "component": "TextField", + "label": "L", + "value": "", + "checks": [{ "call": "email", "args": [{ "path": "/val" }], "message": "Msg" }] + } + ] + } + } + }, + { + "description": "formatString: Valid call", + "valid": true, + "data": { + "updateComponents": { + "surfaceId": "test", + "components": [ + { + "id": "txt1", + "component": "Text", + "text": { + "call": "formatString", + "args": ["Hello ${name}", { "path": "/name" }], + "returnType": "string" + } + } + ] + } + } + }, + { + "description": "formatString: Invalid returnType", + "valid": false, + "data": { + "updateComponents": { + "surfaceId": "test", + "components": [ + { + "id": "txt1", + "component": "Text", + "text": { + "call": "formatString", + "args": ["Hi"], + "returnType": "boolean" + } + } + ] + } + } + }, + { + "description": "formatNumber: Valid args", + "valid": true, + "data": { + "updateComponents": { + "surfaceId": "test", + "components": [ + { + "id": "txt1", + "component": "Text", + "text": { + "call": "formatNumber", + "args": [1234.567, 2, true], + "returnType": "string" + } + } + ] + } + } + }, + { + "description": "formatNumber: Invalid args (wrong type for precision)", + "valid": false, + "data": { + "updateComponents": { + "surfaceId": "test", + "components": [ + { + "id": "txt1", + "component": "Text", + "text": { + "call": "formatNumber", + "args": [1234.567, "2"], + "returnType": "string" + } + } + ] + } + } + }, + { + "description": "formatCurrency: Valid args", + "valid": true, + "data": { + "updateComponents": { + "surfaceId": "test", + "components": [ + { + "id": "txt1", + "component": "Text", + "text": { + "call": "formatCurrency", + "args": [99.99, "USD"], + "returnType": "string" + } + } + ] + } + } + }, + { + "description": "formatCurrency: Missing currency code", + "valid": false, + "data": { + "updateComponents": { + "surfaceId": "test", + "components": [ + { + "id": "txt1", + "component": "Text", + "text": { + "call": "formatCurrency", + "args": [99.99], + "returnType": "string" + } + } + ] + } + } + }, + { + "description": "formatDate: Valid args", + "valid": true, + "data": { + "updateComponents": { + "surfaceId": "test", + "components": [ + { + "id": "txt1", + "component": "Text", + "text": { + "call": "formatDate", + "args": ["2023-01-01", "MMM d"], + "returnType": "string" + } + } + ] + } + } + }, + { + "description": "pluralize: Valid args", + "valid": true, + "data": { + "updateComponents": { + "surfaceId": "test", + "components": [ + { + "id": "txt1", + "component": "Text", + "text": { + "call": "pluralize", + "args": [5, { "one": "item", "other": "items" }], + "returnType": "string" + } + } + ] + } + } + }, + { + "description": "pluralize: Invalid (missing 'other')", + "valid": false, + "data": { + "updateComponents": { + "surfaceId": "test", + "components": [ + { + "id": "txt1", + "component": "Text", + "text": { + "call": "pluralize", + "args": [5, { "one": "item" }], + "returnType": "string" + } + } + ] + } + } + }, + { + "description": "openUrl: Valid call in Action", + "valid": true, + "data": { + "updateComponents": { + "surfaceId": "test", + "components": [ + { + "id": "btn1", + "component": "Button", + "child": "txt1", + "action": { + "functionCall": { + "call": "openUrl", + "args": [{ "url": "https://example.com" }], + "returnType": "void" + } + } + }, + { "id": "txt1", "component": "Text", "text": "Go" } + ] + } + } + }, + { + "description": "openUrl: Invalid args (string instead of object)", + "valid": false, + "data": { + "updateComponents": { + "surfaceId": "test", + "components": [ + { + "id": "btn1", + "component": "Button", + "child": "txt1", + "action": { + "functionCall": { + "call": "openUrl", + "args": ["https://example.com"], + "returnType": "void" + } + } + }, + { "id": "txt1", "component": "Text", "text": "Go" } + ] + } + } + }, + { + "description": "openUrl: Invalid returnType", + "valid": false, + "data": { + "updateComponents": { + "surfaceId": "test", + "components": [ + { + "id": "btn1", + "component": "Button", + "child": "txt1", + "action": { + "functionCall": { + "call": "openUrl", + "args": [{ "url": "https://example.com" }], + "returnType": "boolean" + } + } + }, + { "id": "txt1", "component": "Text", "text": "Go" } + ] + } + } + }, + { + "description": "Unknown function: Should be valid (extensibility)", + "valid": true, + "data": { + "updateComponents": { + "surfaceId": "test", + "components": [ + { + "id": "txt1", + "component": "Text", + "text": { + "call": "myCustomFunc", + "args": [1, 2, 3], + "returnType": "string" + } + } + ] + } + } + }, + { + "description": "length: Invalid min type (string)", + "valid": false, + "data": { + "updateComponents": { + "surfaceId": "test", + "components": [ + { + "id": "tf1", + "component": "TextField", + "label": "L", + "value": "", + "checks": [{ "call": "length", "args": [{ "path": "/val" }, { "min": "5" }], "message": "Msg" }] + } + ] + } + } + }, + { + "description": "length: Invalid max value (negative)", + "valid": false, + "data": { + "updateComponents": { + "surfaceId": "test", + "components": [ + { + "id": "tf1", + "component": "TextField", + "label": "L", + "value": "", + "checks": [{ "call": "length", "args": [{ "path": "/val" }, { "max": -2 }], "message": "Msg" }] + } + ] + } + } + }, + { + "description": "numeric: Invalid min type (string)", + "valid": false, + "data": { + "updateComponents": { + "surfaceId": "test", + "components": [ + { + "id": "tf1", + "component": "TextField", + "label": "L", + "value": "", + "checks": [{ "call": "numeric", "args": [{ "path": "/val" }, { "min": "10" }], "message": "Msg" }] + } + ] + } + } + }, + { + "description": "numeric: Invalid max type (string)", + "valid": false, + "data": { + "updateComponents": { + "surfaceId": "test", + "components": [ + { + "id": "tf1", + "component": "TextField", + "label": "L", + "value": "", + "checks": [{ "call": "numeric", "args": [{ "path": "/val" }, { "max": "20" }], "message": "Msg" }] + } + ] + } + } + }, + { + "description": "regex: Invalid pattern type (number)", + "valid": false, + "data": { + "updateComponents": { + "surfaceId": "test", + "components": [ + { + "id": "tf1", + "component": "TextField", + "label": "L", + "value": "", + "checks": [{ "call": "regex", "args": [{ "path": "/val" }, 123], "message": "Msg" }] + } + ] + } + } + }, + { + "description": "email: Invalid args count (too many)", + "valid": false, + "data": { + "updateComponents": { + "surfaceId": "test", + "components": [ + { + "id": "tf1", + "component": "TextField", + "label": "L", + "value": "", + "checks": [{ "call": "email", "args": [{ "path": "/val" }, "extra"], "message": "Msg" }] + } + ] + } + } + }, + { + "description": "formatString: Invalid format string type (number)", + "valid": false, + "data": { + "updateComponents": { + "surfaceId": "test", + "components": [ + { + "id": "txt1", + "component": "Text", + "text": { + "call": "formatString", + "args": [123, { "path": "/val" }], + "returnType": "string" + } + } + ] + } + } + }, + { + "description": "formatNumber: Invalid precision type (boolean)", + "valid": false, + "data": { + "updateComponents": { + "surfaceId": "test", + "components": [ + { + "id": "txt1", + "component": "Text", + "text": { + "call": "formatNumber", + "args": [123.456, true], + "returnType": "string" + } + } + ] + } + } + }, + { + "description": "formatCurrency: Invalid currency code type (number)", + "valid": false, + "data": { + "updateComponents": { + "surfaceId": "test", + "components": [ + { + "id": "txt1", + "component": "Text", + "text": { + "call": "formatCurrency", + "args": [123.45, 840], + "returnType": "string" + } + } + ] + } + } + }, + { + "description": "formatDate: Invalid pattern type (null)", + "valid": false, + "data": { + "updateComponents": { + "surfaceId": "test", + "components": [ + { + "id": "txt1", + "component": "Text", + "text": { + "call": "formatDate", + "args": ["2023-01-01", null], + "returnType": "string" + } + } + ] + } + } + }, + { + "description": "openUrl: Invalid URL format (not a URI)", + "valid": false, + "data": { + "updateComponents": { + "surfaceId": "test", + "components": [ + { + "id": "btn1", + "component": "Button", + "child": "txt1", + "action": { + "functionCall": { + "call": "openUrl", + "args": [{ "url": "not a uri" }], + "returnType": "void" + } + } + }, + { "id": "txt1", "component": "Text", "text": "Go" } + ] + } + } + } + ] +}