restage_a2ui 0.1.3 copy "restage_a2ui: ^0.1.3" to clipboard
restage_a2ui: ^0.1.3 copied to clipboard

Fail-closed pre-render capability checks and the Restage capability sidecar for apps rendering cached A2UI (Google genui) surfaces.

restage_a2ui #

pub package ci license

The app-side half of Restage's A2UI emit target: a fail-closed, pre-render capability check plus a capability sidecar for cached A2UI payloads.

Restage's build-time toolchain can emit your widget catalog as an A2UI component catalog (a catalog of widget schemas a generative-UI model renders against, via Google's genui SDK). restage_a2ui is what an app uses on the other end: it checks an A2UI payload against the catalog the app actually registered before handing it to genui, so a payload your build can't render faithfully fails with a clean, actionable diagnostic instead of throwing mid-render.

What this is — and is not #

  • It is an app-side safety wrapper: a pre-render check + a capability sidecar you can wrap your cached payloads in.
  • It is not an A2UI delivery mechanism. It does not fetch, host, or stream A2UI — your app (or your model session) owns that. It does not generate the catalog either; that is the build-time toolchain.
  • It depends on genui. Your app already depends on genui to render A2UI, so this adds no new render stack — only the check.

Generate an A2UI catalog from your widgets — step by step #

This is the whole loop: write a normal Flutter widget, annotate it, run build_runner, and get a genui A2UI catalog — no hand-written CatalogItems, no hand-authored JSON schemas. The emitter is the build-time toolchain (restage_codegen); restage_a2ui (this package) is the optional production-safe check you add at the end.

There are two paths — you pick at step 1:

Path What you add Use it when
Minimal restage_codegen (build-time) + genuizero Restage runtime in your app you render your own surfaces and control the payloads
Production-safe the above plus restage_a2ui — a fail-closed pre-render check + version stamp you render payloads you did not author (model- or server-generated)

1. Add dependencies. The codegen is a build-time tool — it does not ship in your app; genui is the renderer; rfw_catalog_schema holds the annotations.

dependencies:
  genui: ^0.9.2                 # the renderer the generated catalog targets
  json_schema_builder: ^0.1.3   # the generated catalog's data schemas are built with this
  rfw_catalog_schema: ^1.0.1    # the @RestageWidget / @RestageProperty / @RestageLibrary annotations
  # Production-safe path only — the app-side pre-render check + capability sidecar (step 8):
  # restage_a2ui: ^0.1.2

dev_dependencies:
  restage_codegen: ^1.0.3       # the build-time A2UI emitter (not shipped in your app)
  build_runner: ^2.4.0

2. Annotate a widget. A normal Flutter widget plus @RestageWidget; mark its inputs with @RestageProperty. A value property paired with a matching-type ValueChanged callback wires the two-way binding automatically — no pairing annotation needed.

import 'package:flutter/widgets.dart';
import 'package:rfw_catalog_schema/rfw_catalog_schema.dart';

@RestageWidget(
  name: 'RatingPicker',
  library: WidgetLibrary.custom('acme.widgets'),
  category: WidgetCategory.input,
  description: 'A 1–5 star rating control bound to an integer value.',
  fires: [WidgetEventName.onChanged],
)
class RatingPicker extends StatelessWidget {
  const RatingPicker({required this.rating, required this.onRatingChanged, super.key});

  @RestageProperty(description: 'The selected rating, 1 through 5.')
  final int rating;

  @RestageProperty(description: 'Reports the newly selected rating.')
  final ValueChanged<int> onRatingChanged;

  @override
  Widget build(BuildContext context) {
    /* … your widget … */
    return const SizedBox.shrink();
  }
}

(A property typed as your own data class auto-generates a rich nested schema — see Rich data.)

3. Declare the library. A barrel that declares your custom library — its namespace and capability version — and re-exports the widgets that belong to it. The build phase reads capabilityVersion off this declaration and stamps it into the generated catalog.

// lib/restage_imports.dart
import 'package:rfw_catalog_schema/rfw_catalog_schema.dart';

export 'widgets/rating_picker.dart';
// … export your other @RestageWidget files

@RestageLibrary(
  library: WidgetLibrary.custom('acme.widgets'),
  capabilityVersion: 1, // bump when you add a widget or make a render-affecting change
)
const restageLibrary = 0;

4. Enable the A2UI builder. It is opt-in (auto_apply: none) because the generated catalog imports genui. If you target A2UI only (not RFW delivery), also turn the two RFW builders off — otherwise they emit unused .g.dart files and a spurious "not mechanically generatable for RFW" warning.

# build.yaml in the package that declares your @RestageWidget libraries
targets:
  $default:
    builders:
      restage_codegen:user_a2ui_catalog:
        enabled: true
      restage_codegen:user_catalog:    # RFW builder — off for an A2UI-only target
        enabled: false
      restage_codegen:user_factories:  # RFW builder — off for an A2UI-only target
        enabled: false

5. Generate:

dart run build_runner build

6. Your two outputs appear (under lib/):

  • …catalog.g.dartbuildRestageCatalogItems(): the genui CatalogItems (each with its data schema and widget builder) that genui renders against.
  • …catalog.a2ui.json — the A2UI-standard catalog document ({ restageCapability, a2uiCatalog }). Each a2uiCatalog.components.<Name> carries that component's full data schema — the same schema genui's own Catalog.toCapabilitiesJson() would emit (the data fields plus the injected component discriminator) — so a producer can generate payloads against this document alone. Two notes that follow genui's conventions: each component schema is a standalone schema resource (a recursive component keeps genui's component-root-relative #/$defs/… pointers, so resolve $ref within the component schema, not against the whole file); and id + component are reserved A2UI envelope keys — genui strips them from a component's data, so don't name a top-level @RestageWidget field either (nest it in a data class if you need that name).

7. Render with genui:

import 'package:genui/genui.dart';

final catalog = Catalog(buildRestageCatalogItems());
// hand `catalog` to your genui surface — it renders your widgets from an A2UI payload.

That's the whole loop. On the minimal path you now depend on genui at runtime and on no Restage packagerestage_codegen is build-time only. You've replaced hand-written CatalogItems with auto-generated ones, with zero runtime lock-in.

8. (Production-safe) Add the fail-closed pre-render check. If you render payloads you did not author, add restage_a2ui (uncomment it in step 1) and gate each payload before handing it to genui — a bad payload then fails closed with a diagnostic instead of throwing mid-render, and you get the capability-version check. That is the Quickstart below.

A worked version of steps 1–7 lives in example/ — the annotated widgets, the library barrel, the build.yaml, the committed generated catalog, and a test that renders them against genui 0.9.2. It has no app entrypoint: run dart run build_runner build to regenerate, then flutter test to see it render. (example/'s own README shows the step-8 pre-render check.)

Quickstart #

Add the dependency:

dependencies:
  restage_a2ui: ^0.1.2
  genui: ^0.9.2

Build the check once (it is immutable — reuse it for every payload), then gate each cached payload before handing it to genui:

import 'dart:convert';

import 'package:flutter/foundation.dart';
import 'package:genui/genui.dart';
import 'package:restage_a2ui/restage_a2ui.dart';

// 1. The genui catalog your build emitted — the `CatalogItem` set genui renders
//    against. `buildRestageCatalogItems()` is generated by the toolchain (see
//    "Producing the catalog and stamp" below).
final catalog = Catalog(buildRestageCatalogItems());

// 2. What that catalog PROVIDES, parsed from the `restageCapability` block the
//    toolchain emits next to the catalog. This is the available side of the
//    version check; supply it so Restage-stamped payloads can be verified.
final installed = A2uiInstalledCapability.fromStampJson(restageCapability);

// 3. The check — one instance, reused.
final check = RestageA2uiPreRenderCheck(catalog: catalog, installed: installed);

// 4. Gate every payload before render. `check.check` accepts either a raw A2UI
//    payload or a Restage sidecar wrapping one.
Widget? renderCached(String cachedJson) {
  final cached = jsonDecode(cachedJson) as Map<String, Object?>;
  switch (check.check(cached)) {
    case A2uiRenderable():
      // Safe to render. If you cached the sidecar (recommended — it carries the
      // version stamp), unwrap it and hand the inner payload to genui:
      final payload = RestageA2uiSidecar.isRestageSidecar(cached)
          ? RestageA2uiSidecar.fromJson(cached).a2ui
          : cached;
      return renderWithGenui(payload); // your genui render call
    case A2uiRejected(:final diagnostic, :final gap):
      // Do NOT render. Fall back to a built-in surface and log why.
      debugPrint('A2UI rejected: $diagnostic${gap == null ? '' : ' ($gap)'}');
      return null;
  }
}

The check fails closed at every path: a malformed envelope, an unknown component, an unmet version, or a stamped payload with no installed descriptor all yield an A2uiRejected — never a throw at the render seam.

If you do not cache the sidecar and only have raw A2UI payloads, you can omit installed; then only the existence walk runs and any Restage-stamped payload is rejected as unverifiable (fail-closed). Caching the sidecar is what enables the version check.

Producing the catalog and stamp #

The two inputs above — buildRestageCatalogItems() and the restageCapability block — are emitted by Restage's build-time toolchain (restage_codegen), which projects your widget catalog (the built-ins plus any @RestageWidget libraries) into two artifacts:

Artifact What it is Who consumes it
The generated CatalogItem Dart (buildRestageCatalogItems()) the functional contract genui renders against — one CatalogItem per widget, with its data schema and widget builder genui, at render time
The stamped catalog document ({ restageCapability, a2uiCatalog }) the A2UI catalog JSON plus the two-axis capability stamp (built-in floor + custom-library versions) this package's check (the restageCapability block → A2uiInstalledCapability.fromStampJson)

Both are emitted over the same A2UI-emittable widget set, so they agree by construction — a widget the emitter scopes out is absent from both.

The build wiring that produces them — the opt-in build_runner builder, the build.yaml settings, and the generate command — is the step-by-step walkthrough above. (The lower-level emit entrypoints emitA2uiCatalogDart(catalog) and emitA2uiCatalog(catalog).toJson() are also available for custom pipelines.)

RFW remains Restage's native delivery path; A2UI emission is additive.

Rich data — structured @RestageWidget properties #

A @RestageWidget property typed as your own data class generates a rich A2UI schema automatically — no shim types, no hand-authored schema. The emitter walks the data shape and emits a genui schema that reconstructs the value at render. The supported rich shapes are:

  • nested data classes (a data class whose fields are themselves data classes),
  • lists of objects (List<YourType>),
  • String-keyed maps (Map<String, V>),
  • named records (({double width, double height})),
  • alongside scalars, enums, scalar lists, and the two-way value/event interactivity.
class Money {
  const Money({required this.amount, required this.currency});
  final double amount;
  final String currency;
}

@RestageWidget(name: 'PriceTag', library: WidgetLibrary.custom('acme.widgets'), /* … */)
class PriceTag extends StatelessWidget {
  const PriceTag({required this.price, super.key});

  // The whole nested value arrives as one property; the generated catalog
  // reconstructs it from the payload and renders it.
  @RestageProperty(description: 'The price to render.')
  final Money price;
  // …
}

A required value that is missing from the payload fails the widget safe — the surface degrades, never renders a fabricated value. Sealed-class unions are not yet recognized; a union-typed property scopes out with a clear diagnostic rather than rendering wrong (recognition is a tracked follow-up).

Note: rich structured properties target the A2UI catalog. Native (RFW) paywall delivery of a custom data class is planned, not yet available.

Why a pre-render check #

genui resolves a payload's component types against the catalog you registered at render time. A component the catalog lacks throws CatalogItemNotFoundException part-way through building the surface. And a payload your app cached against an older catalog can reference a component whose shape has since changed — a drift the app, not genui, owns (genui has no built-in payload cache; you cache the serializable A2UI JSON yourself).

restage_a2ui moves that check before render and makes it explicit:

  1. Existence walk (any payload). Every component type the payload references must exist in the catalog you registered. Works for any A2UI payload — including one generated live by a model — and catches a missing component before genui would throw.
  2. Version satisfaction (Restage-stamped payloads). A payload wrapped in a Restage sidecar carries the catalog version it was generated against. The check verifies your installed catalog meets that version — across both the built-in widgets and any custom widget libraries — so a payload that needs more than your build provides is rejected up front rather than rendered wrong.

Both fail closed: a malformed payload, an unknown component, or an unmet version yields a Rejected result with a diagnostic. The check never throws at the render seam.

The capability sidecar #

A2UI's envelope has no place for a per-payload version stamp, so Restage wraps a cached payload:

{
  "restageCapability": {
    "builtInFloor": 2,
    "requiredLibraries": [{ "namespace": "acme.widgets", "minVersion": 3 }],
    "perItemSinceVersion": { "Text": 1, "AcmeBanner": 3 }
  },
  "a2ui": { "...": "the A2UI payload" }
}

Cache the wrapper; run the check on it before rendering a2ui. The version comparison rests on the catalog's cumulative-render-support invariant: an incompatible change to a component forks a new identity, so an existence walk plus a version compare together are sound. (Without the stamp, name-existence alone is not — a same-name shape change would slip through.)

App Review #

A2UI emission is declarative-data-only — a catalog of widget schemas plus version metadata, no server-shipped executable code. This package adds only a pre-render check over that data.

Status #

Pre-1.0, tracking genui ^0.9.2 (A2UI protocol v0.9). genui is alpha and its API is expected to change; this package isolates the integration so a churn moves one place.

1
likes
0
points
580
downloads

Publisher

verified publisherrestage.dev

Weekly Downloads

Fail-closed pre-render capability checks and the Restage capability sidecar for apps rendering cached A2UI (Google genui) surfaces.

Homepage
Repository (GitHub)
View/report issues

Topics

#server-driven-ui #remote-ui #flutter

License

unknown (license)

Dependencies

flutter, genui, json_schema_builder, restage_shared

More

Packages that depend on restage_a2ui