/*
Copyright 2025 Google LLC
Licensed under the Apache License , Version 2 . 0 ( the " License " ) ;
you may not use this file except in compliance with the License .
You may obtain a copy of the License at
https : //www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing , software
distributed under the License is distributed on an " AS IS " BASIS ,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND , either express or implied .
See the License for the specific language governing permissions and
limitations under the License .
*/
import { SurfaceUpdateSchemaMatcher } from "./surface_update_schema_matcher" ;
import { SchemaMatcher } from "./schema_matcher" ;
export function validateSchema(
data: any,
schemaName: string,
matchers?: SchemaMatcher[],
): string[] {
const errors: string[] = [];
if (data.surfaceUpdate) {
validateSurfaceUpdate(data.surfaceUpdate, errors);
} else if (data.dataModelUpdate) {
validateDataModelUpdate(data.dataModelUpdate, errors);
} else if (data.beginRendering) {
validateBeginRendering(data.beginRendering, errors);
} else if (data.deleteSurface) {
validateDeleteSurface(data.deleteSurface, errors);
} else {
errors.push(
"A2UI Protocol message must have one of: surfaceUpdate, dataModelUpdate, beginRendering, deleteSurface." ,
);
}
if (matchers) {
for (const matcher of matchers) {
const result = matcher.validate(data);
if (!result.success) {
errors.push(result.error!);
}
}
}
return errors;
}
function validateDeleteSurface(data: any, errors: string[]) {
if (data.surfaceId === undefined) {
errors.push("DeleteSurface must have a 'surfaceId' property." );
}
const allowed = ["surfaceId" ];
for (const key in data) {
if (!allowed.includes(key)) {
errors.push(`DeleteSurface has unexpected property: ${key}`);
}
}
}
function validateSurfaceUpdate(data: any, errors: string[]) {
if (data.surfaceId === undefined) {
errors.push("SurfaceUpdate must have a 'surfaceId' property." );
}
if (!data.components || !Array.isArray(data.components)) {
errors.push("SurfaceUpdate must have a 'components' array." );
return ;
}
const componentIds = new Set<string>();
for (const c of data.components) {
if (c.id) {
if (componentIds.has(c.id)) {
errors.push(`Duplicate component ID found: ${c.id}`);
}
componentIds.add(c.id);
}
}
for (const component of data.components) {
validateComponent(component, componentIds, errors);
}
}
function validateDataModelUpdate(data: any, errors: string[]) {
if (data.surfaceId === undefined) {
errors.push("DataModelUpdate must have a 'surfaceId' property." );
}
const allowedTopLevel = ["surfaceId" , "path" , "contents" ];
for (const key in data) {
if (!allowedTopLevel.includes(key)) {
errors.push(`DataModelUpdate has unexpected property: ${key}`);
}
}
if (!Array.isArray(data.contents)) {
errors.push("DataModelUpdate must have a 'contents' array." );
return ;
}
const validateValueProperty = (
item: any,
itemErrors: string[],
prefix: string,
) => {
const valueProps = [
"valueString" ,
"valueNumber" ,
"valueBoolean" ,
"valueMap" ,
];
let valueCount = 0 ;
let foundValueProp = "" ;
for (const prop of valueProps) {
if (item[prop] !== undefined) {
valueCount++;
foundValueProp = prop;
}
}
if (valueCount !== 1 ) {
itemErrors.push(
`${prefix} must have exactly one value property (${valueProps.join(", " )}), found ${valueCount}.`,
);
return ;
}
if (foundValueProp === "valueMap" ) {
if (!Array.isArray(item.valueMap)) {
itemErrors.push(`${prefix} 'valueMap' must be an array.`);
return ;
}
item.valueMap.forEach((mapItem: any, index: number) => {
if (!mapItem.key) {
itemErrors.push(
`${prefix} 'valueMap' item at index ${index} is missing a 'key' .`,
);
}
const mapValueProps = ["valueString" , "valueNumber" , "valueBoolean" ];
let mapValueCount = 0 ;
for (const prop of mapValueProps) {
if (mapItem[prop] !== undefined) {
mapValueCount++;
}
}
if (mapValueCount !== 1 ) {
itemErrors.push(
`${prefix} 'valueMap' item at index ${index} must have exactly one value property (${mapValueProps.join(", " )}), found ${mapValueCount}.`,
);
}
const allowedMapKeys = ["key" , ...mapValueProps];
for (const key in mapItem) {
if (!allowedMapKeys.includes(key)) {
itemErrors.push(
`${prefix} 'valueMap' item at index ${index} has unexpected property: ${key}`,
);
}
}
});
}
};
data.contents.forEach((item: any, index: number) => {
if (!item.key) {
errors.push(
`DataModelUpdate 'contents' item at index ${index} is missing a 'key' .`,
);
}
validateValueProperty(
item,
errors,
`DataModelUpdate 'contents' item at index ${index}`,
);
const allowedKeys = [
"key" ,
"valueString" ,
"valueNumber" ,
"valueBoolean" ,
"valueMap" ,
];
for (const key in item) {
if (!allowedKeys.includes(key)) {
errors.push(
`DataModelUpdate 'contents' item at index ${index} has unexpected property: ${key}`,
);
}
}
});
}
function validateBeginRendering(data: any, errors: string[]) {
if (data.surfaceId === undefined) {
errors.push("BeginRendering message must have a 'surfaceId' property." );
}
if (!data.root) {
errors.push("BeginRendering message must have a 'root' property." );
}
}
function validateBoundValue(
prop: any,
propName: string,
componentId: string,
componentType: string,
errors: string[],
) {
if (typeof prop !== "object" || prop === null || Array.isArray(prop)) {
errors.push(
`Component '${componentId}' of type '${componentType}' property '${propName}' must be an object.`,
);
return ;
}
const keys = Object.keys(prop);
const allowedKeys = [
"literalString" ,
"literalNumber" ,
"literalBoolean" ,
"path" ,
];
let validKeyCount = 0 ;
for (const key of keys) {
if (allowedKeys.includes(key)) {
validKeyCount++;
}
}
if (validKeyCount !== 1 || keys.length !== 1 ) {
errors.push(
`Component '${componentId}' of type '${componentType}' property '${propName}' must have exactly one key from [${allowedKeys.join(", " )}]. Found: ${keys.join(", " )}`,
);
}
}
function validateComponent(
component: any,
allIds: Set<string>,
errors: string[],
) {
if (!component.id) {
errors.push(`Component is missing an 'id' .`);
return ;
}
if (!component.component) {
errors.push(`Component '${component.id}' is missing 'component' .`);
return ;
}
const componentTypes = Object.keys(component.component);
if (componentTypes.length !== 1 ) {
errors.push(
`Component '${component.id}' must have exactly one property in 'component' , but found ${componentTypes.length}.`,
);
return ;
}
const componentType = componentTypes[0 ];
const properties = component.component[componentType];
const checkRequired = (props: string[]) => {
for (const prop of props) {
if (properties[prop] === undefined) {
errors.push(
`Component '${component.id}' of type '${componentType}' is missing required property '${prop}' .`,
);
}
}
};
const checkRefs = (ids: (string | undefined)[]) => {
for (const id of ids) {
if (id && !allIds.has(id)) {
errors.push(
`Component '${component.id}' references non-existent component ID '${id}' .`,
);
}
}
};
switch (componentType) {
case "Heading" :
checkRequired(["text" ]);
if (properties.text)
validateBoundValue(
properties.text,
"text" ,
component.id,
componentType,
errors,
);
break ;
case "Text" :
checkRequired(["text" ]);
if (properties.text)
validateBoundValue(
properties.text,
"text" ,
component.id,
componentType,
errors,
);
break ;
case "Image" :
checkRequired(["url" ]);
if (properties.url)
validateBoundValue(
properties.url,
"url" ,
component.id,
componentType,
errors,
);
break ;
case "Video" :
checkRequired(["url" ]);
if (properties.url)
validateBoundValue(
properties.url,
"url" ,
component.id,
componentType,
errors,
);
break ;
case "AudioPlayer" :
checkRequired(["url" ]);
if (properties.url)
validateBoundValue(
properties.url,
"url" ,
component.id,
componentType,
errors,
);
if (properties.description)
validateBoundValue(
properties.description,
"description" ,
component.id,
componentType,
errors,
);
break ;
case "TextField" :
checkRequired(["label" ]);
if (properties.label)
validateBoundValue(
properties.label,
"label" ,
component.id,
componentType,
errors,
);
if (properties.text)
validateBoundValue(
properties.text,
"text" ,
component.id,
componentType,
errors,
);
break ;
case "DateTimeInput" :
checkRequired(["value" ]);
if (properties.value)
validateBoundValue(
properties.value,
"value" ,
component.id,
componentType,
errors,
);
break ;
case "MultipleChoice" :
checkRequired(["selections" , "options" ]);
if (properties.selections) {
if (
typeof properties.selections !== "object" ||
properties.selections === null ||
(!properties.selections.literalArray && !properties.selections.path)
) {
errors.push(
`Component '${component.id}' of type '${componentType}' property 'selections' must have either 'literalArray' or 'path' .`,
);
}
}
if (Array.isArray(properties.options)) {
properties.options.forEach((option: any, index: number) => {
if (!option.label)
errors.push(
`Component '${component.id}' option at index ${index} missing 'label' .`,
);
if (option.label)
validateBoundValue(
option.label,
"label" ,
component.id,
componentType,
errors,
);
if (!option.value)
errors.push(
`Component '${component.id}' option at index ${index} missing 'value' .`,
);
});
}
break ;
case "Slider" :
checkRequired(["value" ]);
if (properties.value)
validateBoundValue(
properties.value,
"value" ,
component.id,
componentType,
errors,
);
break ;
case "CheckBox" :
checkRequired(["value" , "label" ]);
if (properties.value)
validateBoundValue(
properties.value,
"value" ,
component.id,
componentType,
errors,
);
if (properties.label)
validateBoundValue(
properties.label,
"label" ,
component.id,
componentType,
errors,
);
break ;
case "Row" :
case "Column" :
case "List" :
checkRequired(["children" ]);
if (properties.children && Array.isArray(properties.children)) {
const hasExplicit = !!properties.children.explicitList;
const hasTemplate = !!properties.children.template;
if ((hasExplicit && hasTemplate) || (!hasExplicit && !hasTemplate)) {
errors.push(
`Component '${component.id}' must have either 'explicitList' or 'template' in children, but not both or neither.`,
);
}
if (hasExplicit) {
checkRefs(properties.children.explicitList);
}
if (hasTemplate) {
checkRefs([properties.children.template?.componentId]);
}
}
break ;
case "Card" :
checkRequired(["child" ]);
checkRefs([properties.child]);
break ;
case "Tabs" :
checkRequired(["tabItems" ]);
if (properties.tabItems && Array.isArray(properties.tabItems)) {
properties.tabItems.forEach((tab: any) => {
if (!tab.title) {
errors.push(
`Tab item in component '${component.id}' is missing a 'title' .`,
);
}
if (!tab.child) {
errors.push(
`Tab item in component '${component.id}' is missing a 'child' .`,
);
}
checkRefs([tab.child]);
if (tab.title)
validateBoundValue(
tab.title,
"title" ,
component.id,
componentType,
errors,
);
});
}
break ;
case "Modal" :
checkRequired(["entryPointChild" , "contentChild" ]);
checkRefs([properties.entryPointChild, properties.contentChild]);
break ;
case "Button" :
checkRequired(["child" , "action" ]);
checkRefs([properties.child]);
if (!properties.action || !properties.action.name) {
errors.push(
`Component '${component.id}' Button action is missing a 'name' .`,
);
}
break ;
case "Divider" :
// No required properties
break ;
case "Icon" :
checkRequired(["name" ]);
if (properties.name)
validateBoundValue(
properties.name,
"name" ,
component.id,
componentType,
errors,
);
break ;
default :
errors.push(
`Unknown component type '${componentType}' in component '${component.id}' .`,
);
}
}
Messung V0.5 in Prozent C=96 H=99 G=97
¤ Dauer der Verarbeitung: 0.6 Sekunden
¤
*© Formatika GbR, Deutschland