Skip to content

feat: add flutter_codec for portable Flutter value JSON codecs - #138

Draft
leoafarias wants to merge 64 commits into
mainfrom
feat/flutter-codec
Draft

feat: add flutter_codec for portable Flutter value JSON codecs#138
leoafarias wants to merge 64 commits into
mainfrom
feat/flutter-codec

Conversation

@leoafarias

Copy link
Copy Markdown
Member

Summary

  • Adds the unpublished flutter_codec package with ACK codecs for Flutter painting, rendering, and a small widget surface (Container, Text, Key), plus goldens and workspace/CI integration.
  • Codecs validate JSON-safe constructor fields, fail loudly on unencodable state, and re-enforce Flutter debug asserts at the codec boundary so they hold in release.
  • The latest pass treats an empty StrutStyle fallback as omitted when package is set, and encoding Text.rich reports a dedicated unsupported-span error.

Test plan

  • cd packages/flutter_codec && dart analyze . --fatal-infos
  • cd packages/flutter_codec && flutter test
  • Confirm {"package": "my_pkg", "fontFamilyFallback": []} is rejected by strutStyleCodec
  • Confirm textWidgetCodec.safeEncode(Text.rich(...)) mentions unsupported span trees

Made with Cursor

leoafarias added 30 commits May 20, 2026 17:17
Bulk-copy schemas/*, ack.dart, datetime_constraint.dart, discriminated_branch_utils.dart, and supporting types (common_types, context, schema_error, helpers) from claude/ack-typed-codecs-pre-rebase-backup. Delete transformed_schema.dart (backup design replaces it with WrapperSchema mixin + CodecSchema/DefaultSchema/InstanceSchema).

Library down from 47 errors to 4 \xe2\x80\x94 remaining failures are in ack_schema_model_builder.dart (TransformedSchema reference, effectiveBranch missing, defaultValue removed from base). Test side still ~200 errors (API drift). Not a working checkpoint yet.
- Add copyWithInner to WrapperSchema mixin + CodecSchema/DefaultSchema
- Adapt main's effectiveDiscriminatedBranch util to walk WrapperSchema instead of TransformedSchema; add discriminatorPropertyAcceptsValue and effectiveDiscriminatedObjectBranch helpers
- Loosen DiscriminatedObjectSchema constructor for union-owned discriminator: branches without the discriminator are allowed (union synthesizes literal); branches with it must accept the label
- Add effectiveBranch method to DiscriminatedObjectSchema
- Update ack_schema_model_builder: DefaultSchema precheck for defaults, generic WrapperSchema precheck, InstanceSchema case
- Drop legacy _withDefaultAndWarnings base-schema defaultValue access

Library compiles cleanly (dart analyze lib: no issues found). Tests still pending.
- Pull backup's versions of overlap test files (any_of_null_and_default, core_schema, comprehensive_json_schema, discriminated_object_schema, path_preservation, schema_equality, documentation/*, integration/discriminated_child_transform)
- Add backup's new test files: consolidation_test, typed_codecs_characterization_test, polish_test
- Delete obsolete tests backup removed: composite_default, default_mutation, transformed_schema_default
- Add JsonMap to package:ack public API exports
- Fix ack_schema_model_builder_test to use AnyAckSchema instead of AckSchema<T>

dart analyze passes cleanly. dart test: +838 passed / -27 failed (97% pass rate). Remaining failures are behavioral drift from PR #108's Draft-7 strictness (propertyOrdering, formatMinimum/Maximum, nested anyOf nullables); to be retargeted next.
Library changes:
- ListSchema constructor rejects nullable item schemas (moved from Ack.list factory)
- ack_schema_model_builder: unify DefaultSchema into the WrapperSchema path so description/nullable propagate; suppress 'x-transformed' for DefaultSchema (defaults don't transform)
- ack_schema_model: AckObjectSchemaModel.toJsonSchema() omits default-bearing keys from 'required'; hoist user-facing metadata (title/description/default) to anyOf envelope top-level
- ListSchema is no longer a const constructor (validation requires a body)

Test alignment:
- Delete strict-rejection tests for branches missing the discriminator literal (we adopted #107's union-owned discriminator)
- Update 'rejects incompatible discriminator' test to expect construction-time throw
- Drop _OneOfNullableStringSchema custom-schema test (toJsonSchema is now non-overridable per-schema)
- Update Zod reference fixtures to omit default-bearing keys from 'required'

dart analyze: clean. dart test: 862 passed / 0 failed.
- date min/max test uses local DateTime (backup's Ack.date() requires local midnight)
- 'codec overrides are applied' test asserts description hoists to envelope per #108's Zod v4 convention; branches carry type info
- discriminator-reject test expects construction-time throw (PR #107)

48 passed / 0 failed.
… members

- Hide AnyAckSchema, Refinement, SchemaOperation from package:ack/ack.dart
  alongside WrapperSchema; they are internal traversal plumbing and
  consumers use .refine(...), safeParse, and concrete schema types instead.
- Delete dead members: AckSchema.getSchemaType, AckSchemaModel.withTitle,
  InvalidTypeConstraint.withTypes, and the coerceJsonMap deprecated alias.
- Mark CodecSchema.copyWith @internal; it duplicates copyWithRuntimeConfig
  and exists only to satisfy the wrapper protocol.
- Add refine/constrain overrides to FluentSchema so primitive schemas
  preserve their concrete type through the fluent chain (matches the
  existing WrapperSchema behavior). Unlocks removal of two redundant
  `as StringSchema` casts in string_schema_extensions.
- Update internal tests to consume now-hidden symbols via the src/ path.
…ugh toJsonSchema

- Rename DefaultSchema._validateDefaultWithContext to resolveDefaultWithContext
  and use it from ObjectSchema and the schema model builder so the call site
  expresses intent ("resolve a default") rather than the side effect of
  parsing null.
- Drop the unused hasMatchingDiscriminatorLiteral helper from
  discriminated_branch_utils.dart.
- Collapse toSchemaModel().toJsonSchema() to toJsonSchema() in the
  ack_firebase_ai and ack_json_schema_builder adapters; update the
  schema-converter guide and api-reference docs accordingly.
- Rework _dateTimeJsonFormat to read the format directly off the input
  constraints, removing the dependency on AckSchemaModel construction in
  datetime_schema_extensions.dart.
- Document _defaultExportContext as throwaway so the rooted error path is
  intentional.
- Add focused tests: consolidation_test covers default-on-encode regressions,
  polish_test distinguishes optional+codec from optional+nullable+codec,
  datetime_validation_test gates date/date-time formats, and
  typed_codecs_characterization_test characterizes resolveDefaultWithContext.
AnyOfSchema previously short-circuited null at the union level before
trying members, so a DefaultSchema branch never got to supply its
fallback. Parse now tries branches first when the input is null; the
union-level null gate runs only as a fallback. Runtime validation and
encode keep the original union-level gate.

Also sweeps documentation for stale references caused by the
typed-codec refactor: drops the removed strictParsing API, the
deprecated tryParse/validate entries, and the primitive coercion
language; clarifies that primitives are strict (and that integer and
double do not overlap), documents Ack.number() alongside them, and
updates transform examples to receive non-null runtime values.
Returns CodecSchema<String, T> wrapping EnumSchema<T> with identity
decode/encode. Use when downstream code expects every value-shape to
be a CodecSchema (e.g. a registry of codecs across many value shapes).
The underlying EnumSchema still does the String <-> .name mapping.
Make Ack.double() and Ack.number() reject NaN and infinities by default, add fluent NumberSchema numeric constraints, and export Ack.number() as a JSON Schema number.

BREAKING CHANGE: Ack.double() and Ack.number() now reject non-finite double values during parse and encode.
The .model<T>() extension on ObjectSchema duplicated the .codec<T>()
API — same decode/encode signatures, with one extra behavior
(omitNullOptionals=true) that silently dropped null entries from
encoded JSON when the property was marked optional.

That default-true null stripping violated Zod/JSON-Schema semantics:
optional (key absent) and nullable (value null) are distinct states,
and conflating them caused asymmetric round-trips and silent data
loss. It was also redundant with ObjectSchema.encodeWithContext,
which already drops null values for non-nullable properties.

Removed:
- ObjectSchemaModelExtension and its omitNullOptionals flag.

Migrated callers:
- 6 test sites switched to .codec<T>() (identical signatures).
- Shadow / BoxShadow leaf codecs (lib/src/shadows.dart) composing
  colorCodec, offsetCodec, and blurStyleCodec; blurRadius enforces
  non-negativity via Ack.number().min(0).
- LinearGradient / RadialGradient / SweepGradient and a Gradient union
  (lib/src/gradients.dart), tagged with a 'type' discriminator via
  Ack.literal. colors.minItems(2); stops/focal are .nullable().optional().
  transform (GradientTransform) is documented as unsupported.
- Switch all flutter_codec sites from .model<T>() to .codec<T>() for
  uniform API; .codec already defaults output to InstanceSchema<T>.
  Null-optional fields now encode explicitly rather than being stripped.
leoafarias and others added 30 commits May 26, 2026 20:41
The synthesized discriminator schema produced by effectiveDiscriminatedObjectBranch
now carries withDefault(discriminatorValue), so a discriminated branch whose runtime
encode lambda omits the discriminator key still emits it via the default. Closes
the encode side of PR #107's union-owned discriminator story (parse already
synthesized the literal; encode previously required the branch to emit the key).

ack_schema_model_builder.\_discriminated wraps each branch's exported model with
_withRequiredDiscriminator so the JSON Schema marks the discriminator as required
and strips the synthetic default from the output.
- rectCodec (lib/src/primitives/rect.dart): {left, top, right, bottom} via
  Rect.fromLTRB.
- imageProviderCodec (lib/src/image_providers.dart): discriminated union over
  NetworkImage and AssetImage. Recursive providers, FileImage, MemoryImage, and
  custom asset bundles are intentionally rejected for JSON-safety.
- networkImageCodec / assetImageCodec exported as the typed branch codecs.
- webHtmlElementStrategyCodec added to enums.dart (used by NetworkImage).
- decorationImageCodec (lib/src/decoration_image.dart): composite codec for
  DecorationImage. Composes imageProviderCodec, BoxFit/ImageRepeat/FilterQuality
  enums, alignmentGeometryCodec, and rectCodec. opacity is range-validated [0,1]
  (stricter than Flutter, which only clamps at paint time). colorFilter and
  onError are intentionally unsupported and excluded from DecorationImage's ==.
- boxDecorationCodec: 'image' field now uses decorationImageCodec.nullable()
  .optional() instead of the null-only placeholder; the deferral comment is gone.
- README updated to advertise ImageProvider and the BoxDecoration.image gap is
  no longer applicable.
- Doc-comment polish per Effective Dart: private helpers in font_weight,
  text_decoration, locale, and text_style switched from /// to //; identifier
  references throughout now use [Symbol] form; broken [_decodeLocale] reference
  in locale.dart removed.

Tests: 418/418 pass (added rect_test, decoration_image_test, and image_providers
groups; updated box_decoration_test to drop 'image deferral' and add 'image
integration').
Both serialize as {tag, value} maps with a 4-character printable-ASCII
tag pattern; FontFeature.value defaults to 1, matching the constructor.
Wired into textStyleCodec so TextStyle now covers every JSON-safe
constructor field.
…ation union

shapeBorderCodec discriminates the five concrete OutlinedBorder subtypes
(CircleBorder, StadiumBorder, RoundedRectangleBorder,
BeveledRectangleBorder, ContinuousRectangleBorder) via the 'type' key.
The three rectangle-with-radius branches share a single private object
schema, so the wire shape is defined exactly once.

shapeDecorationCodec composes the JSON-safe ShapeDecoration constructor
fields and leaves the color-XOR-gradient assert to Flutter's constructor.

decorationCodec unions BoxDecoration and ShapeDecoration under the abstract
Decoration type, mirroring the gradientCodec / imageProviderCodec
discriminated pattern. OvalBorder (which extends CircleBorder) is
documented as round-tripping to a plain CircleBorder.

Also consolidates box_decoration.dart, shape_decoration.dart, and the new
decoration union into a single decorations.dart, matching how gradients
and image providers each live in one file with their union.
…GELOG

Replaces the stale README (which incorrectly said BoxDecoration.image was
deferred) with a coverage table grouped by family, a quick example, the
discriminated-union summary, an intentionally-excluded list, and a short
roadmap. Adds a CHANGELOG.md seeded with the 0.1.0 release scope so
pub.dev renders a populated Changelog tab.

No code or schema changes.
Adds roundedSuperellipseBorderCodec sharing the {side, borderRadius}
schema with the other three rectangle border codecs, and a
"roundedSuperellipse" branch on shapeBorderCodec. Bumps the Flutter
SDK floor to >=3.27.0 since RoundedSuperellipseBorder was introduced
there.

README and CHANGELOG updated; the type is no longer listed under
"intentionally excluded" or the roadmap.
…minated union branches

Ack.discriminated<T> takes Map<String, AckSchema<JsonMap, T>>, and Dart's
default covariance on generic parameters already accepts a
CodecSchema<JsonMap, ConcreteBranchType> in place of
AckSchema<JsonMap, ParentType>. The runtime encode dispatch (each branch's
own validateRuntimeWithContext) checks 'value is BranchT' itself, so the
.codec<Parent>(decode: (v) => v, encode: (v) => v as Concrete) wrappers
were always redundant.

Applies the simplification across all four union sites:
  - gradientCodec (3 branches)
  - imageProviderCodec (2 branches)
  - shapeBorderCodec (6 branches)
  - decorationCodec (2 branches)

Net: ~44 lines of boilerplate removed; behavior identical. The upstream
Ack.discriminatedOf<T> feature request is no longer needed.
…luded

Both types keep all constructor state in library-private fields with no
public getters anywhere in dart:ui or package:flutter. ColorFilter is a
single class with shared runtimeType across all four constructor variants;
ImageFilter is abstract with a private constructor and library-private
subclasses. The only state-revealing surface is toString(), which is a
debug format with no stability contract.

A bidirectional codec is therefore not achievable via the public API.
DecorationImage.colorFilter stays permanently excluded for the same
reason.

Adds a dedicated 'Opaque dart:ui state' bullet to the README's
intentionally-excluded section, removes the now-obsolete Roadmap entry,
and tightens decoration_image.dart's dartdoc with the concrete reason.
- textHeightBehaviorCodec covers TextHeightBehavior's three constructor
  fields (applyHeightToFirstAscent, applyHeightToLastDescent, leadingDistribution)
  with Flutter's defaults baked in.
- strutStyleCodec mirrors textStyleCodec for StrutStyle, covering every
  JSON-safe constructor parameter. debugLabel is excluded (matches the
  TextStyle policy).
- Extracted the package-prefix unfolding heuristic from text_style.dart
  into a shared lib/src/font_family_packing.dart helper, used by both
  textStyleCodec and strutStyleCodec.

README and CHANGELOG updated. 482/482 tests pass (15 new).
…odecs

Three new codecs on lib/src/shape_borders.dart:
  - starBorderCodec covers all seven StarBorder constructor parameters
    (side, points, innerRadiusRatio, pointRounding, valleyRounding,
    rotation in degrees, squash). StarBorder.polygon round-trips through
    the regular StarBorder constructor with the computed innerRadiusRatio.
  - linearBorderEdgeCodec covers LinearBorderEdge's (size, alignment),
    both range-validated.
  - linearBorderCodec covers LinearBorder's (side, start, end, top, bottom)
    where each edge is a nullable linearBorderEdgeCodec.

shapeBorderCodec now spans eight discriminator branches (added 'star' and
'linear'). The dartdoc lists Material InputBorder subtypes as the only
remaining 'separate plan' exclusion; StarBorder and LinearBorder are
removed from the README's intentionally-excluded section.

496/496 tests pass (14 new).
- Introduced `_LazyCodec` for lazy schema resolution.
- Added `boxConstraintsCodec` and `constraintsCodec` for handling Flutter's BoxConstraints.
- Implemented codecs for `Matrix4`, `Container`, `Key`, and `Text` widgets.
- Updated `flutter_codec.dart` to export new codecs.
- Added tests for lazy codec, box constraints, matrix4, container, key, and text codecs.
Replace the local _LazyCodec shim (and its test) with Ack.lazy, now
available upstream in ack. The recursion mechanic for Container.child is
unchanged; only the lazy primitive moves from a private shim to the
shared Ack.lazy API.
Bring branch up to current main (adds Ack.lazy and ack-core refactors).
Branch contributes only the flutter_codec package; all ack core and docs
are taken from main unchanged.
Required for the package to resolve under resolution: workspace.
… unencodable inputs

Review-driven hardening of the flutter_codec package.

- Bump Flutter floor to >=3.32.0: the package uses RoundedSuperellipseBorder
  and Text.semanticsIdentifier (3.32-only) and WebHtmlElementStrategy (3.29)
  unconditionally, so the prior >=3.27.0 would not compile on 3.27-3.31.
- Fix font-family packing corruption: never infer a package when the recovered
  family is null (decode was folding it into the literal 'packages/<pkg>/null').
- fontWeightCodec: accept and emit integer variable-font weights [1,1000] and
  drop the deprecated FontWeight.index path; canonical weights still emit "wNNN".
- Throw on encode for unrepresentable, equality-affecting inputs instead of
  dropping them silently: Gradient.transform and DecorationImage.colorFilter
  (and fix the false "colorFilter excluded from ==" doc).
- Enforce Container / BoxDecoration / ShapeDecoration cross-field invariants with
  .refine so validation holds in release builds (constructor asserts are stripped).
- Add readDoubleList; document the 8-bit-sRGB color loss and the StarBorder
  rotation/polygon lossy narrowing; sync README/CHANGELOG coverage to include the
  widgets/constraints/matrix4 codecs and their unions.
- Add 26 regression/characterization tests (556 total).

flutter analyze: clean; flutter test: 556 pass; dart format: clean.
Add a golden-fixture harness covering all 102 public codecs (56 structured
+ 46 enums). For each type it records the exact JSON the codec emits in a
reviewable per-family fixture under test/golden/fixtures/, then parses that
JSON back and asserts the round-trip:
  - value equality for painting/rendering value types,
  - stability (encode(parse(json)) == json) for the widget types that have
    no value equality, and
  - the documented narrowing for lossy types (e.g. OvalBorder -> circle).

Fixtures are kept as plain, dependency-free JSON and are byte-stable across
platforms (matrix transforms avoid trig; the only long decimal is 2*pi).
StarBorder.polygon is intentionally excluded because its encoded
innerRadiusRatio is a libm value. Regenerate with UPDATE_GOLDENS=true.

A README documents the conventions: the top-level keys are test-case
identifiers (not wire data), encode emits explicit nulls while decode treats
absent == null, unsupported fields are omitted entirely, and BoxConstraints
deliberately distinguishes an absent min bound (0) from null (infinity).
…ble state

Address confirmed codec-review findings by tightening the codec boundary
instead of silently dropping or admitting invalid runtime state:

- TextStyle.foreground/background and Text.textScaler now throw UnsupportedError
  on encode rather than being silently dropped to a colorless/unscaled value.
- keyCodec matches the exact ValueKey<T> runtime type, so ValueKey subclasses
  (e.g. PageStorageKey) are rejected instead of re-encoded as a plain ValueKey.
- Reject inverted BoxConstraints, negative Container padding/margin,
  BoxShape.circle combined with a borderRadius, and gradient stops whose length
  differs from colors.
- Constrain FontVariation.value to the [-32768, 32768) 16.16 fixed-point range.
- AlignmentDirectional center-column constants (topCenter/center/bottomCenter)
  encode as {start, y} through alignmentGeometryCodec so they round-trip as
  directional values.
- Tighten colorCodec rgb/rgba patterns to 0-255 so the generated JSON Schema no
  longer admits out-of-range channels.

Each fix has focused regression tests. The discriminated-union ambiguity fix
lands separately via ack #115 (merged into this branch).
Adds the missing LICENSE, fixes fontFamily encoding a literal "null"
string instead of JSON null, inherits the shared workspace lint config,
and rounds out test coverage for gradient stop ordering and mixed
geometry encode-rejection.
Disables prefer-shorthands-with-static-fields (needs Dart 3.10; the
package floor is 3.8) and removes 141 explicit type arguments that
the analyzer already infers, clearing the shared workspace lint
config's remaining actionable findings in lib/.
Co-authored-by: Cursor <cursoragent@cursor.com>
Reconcile SDK requirements and preserve minimum Flutter CI coverage using the pinned setup action. Adapt codec validation exceptions and schema goldens to the updated ACK runtime.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant