[blueprint + dart] Scope validator to single catalog. - #2538
Conversation
There was a problem hiding this comment.
Code Review
This pull request refactors the validation logic to scope each A2uiValidator to a single catalog rather than a list of catalogs, updating MessageProcessor to manage and cache a validator per catalog. This ensures components are strictly validated against their surface's specific catalog. A review comment correctly notes an inconsistency between the blueprint matrix, which references A2uiValidator.parseMessages(), and the Dart implementation, which introduces parseMessagesFor as the static method.
| | **Protocol Envelope** | Single update type per message (`createSurface`, `updateComponents`, etc.) | `A2uiValidator.parseMessages()` (envelope schema, no catalog needed) | `A2uiValidationError` | | ||
| | **Protocol Envelope** | Valid `version` tag (`v0.8`, `v0.9`, `v1.0`) & required envelope keys | `A2uiValidator.parseMessages()` (envelope schema, no catalog needed) | `A2uiValidationError` | |
There was a problem hiding this comment.
The blueprint matrix refers to 'A2uiValidator.parseMessages()' for envelope validation, but the static method added in the Dart implementation is named 'parseMessagesFor' (and 'parseMessages' is kept as an instance method). Consider aligning the blueprint or documenting this language-specific naming difference to avoid confusion.
There was a problem hiding this comment.
The comment suggests a fresh validator per batch. This memoizes one per catalog id instead. The validator caches its catalog's resolved component schemas, and resolveSchemaRefs runs over every component in the catalog; a fresh instance per batch would rebuild that on every streamed update. Same scoping, no per-message rebuild.
As schemas are not supposed to change in runtime, caching is ok.
Contributes to #2356, #2373.
Follow up for: #2439 (comment)
What this PR changes
A2uiValidatorholds one catalog instead of a map of them:A component belongs to exactly one catalog, and v0.9 declares
catalogIdoncreateSurfacealone, so a validator holding several cannot tell which one applies to a payload that only updates a surface. #2439 worked around that with asurfaceCatalogsmap threaded throughvalidate,validateStructureandvalidateAgainstCatalogs, plus a_catalogForladder that fell back to the sole catalog and otherwise threw. Scoping the validator to one catalog removes the question, and all of that with it.validateComponent,validateThemeandvalidateComponentBatchlose theirCatalogparameter for the same reason. A payload that creates a surface against any other catalog now raisesA2uiCatalogError.MessageProcessorkeeps one validator per catalog, built on first use and reused, reachable throughvalidatorFor(catalog). EachSurfaceModelalready recorded the catalog it was created with, so every check routes to the validator for that catalog. Thevalidator:constructor parameter is replaced byprotocolVersionandcommonTypesSchema: injecting a single validator means nothing once there are several, and taking the configuration directly keeps the version consistent between envelope parsing and catalog checks.Envelope parsing moves to
A2uiValidator.parseMessagesFor, a static that takes no catalog. The version tag and the single-update-type rule read no catalog, so a renderer can parse a payload before matching each message to a surface, and so to a catalog.processPayloadparses this way, then dispatches.On the "block mixed update types" prerequisite
#2439 (comment) asks for this first. Dart already enforces it, in
A2uiMessage.fromJson, which throwsA2uiValidationErrorwhen an envelope carries more than one ofcreateSurface,updateComponents,updateDataModel,deleteSurface.processMessagestakes sealed typed messages, so it cannot mix by construction. Nothing pinned the rule, so this adds a test.What actually had to change first was making the envelope stage catalog-independent, since that is the step that has to happen before a message can be matched to its catalog. Batch-level grouping by surface turned out to be unnecessary: the processor dispatches per message, and each message names one surface, hence one catalog.
Tests
processPayload rejects an envelope mixing update typespins the rule above.MessageProcessor catalog scopepins per-surface catalog isolation, which nothing covered:plus that
validatorForreturns the same instance per catalog, since the schema cache depends on it.A2uiValidator catalog scopereplaces thesurfaceCatalogsgroup: an incremental payload is checked against the validator's catalog rather than skipped, a component from another catalog is rejected, and acreateSurfacenaming another catalog raisesA2uiCatalogErrorfrom bothvalidateandvalidateStructure.The conformance harness derives one catalog per case instead of registering the document under every id the payload names.
Blueprint
New "Catalog Scope" section under the validation layer, written as instructions for implementers in other languages: scope the validator to one catalog, expose catalog-free envelope parsing, build one validator per catalog in
MessageProcessorand reuse it, record the catalog on the surface, check each surface against the catalog it was created with. TheA2uiValidatorsignature, the renderer/agent table and the validation matrix are updated, with two new Catalog Scope rows.Verification
dart analyzeindart/a2ui_coreanddart/a2ui_agent: no issuesdart testindart/a2ui_core: 297 passed, 25 skipped (pre-existing v0.8 cases)dart format: clean